I am fairly new to iOS development and have been experimenting with SwiftUI for a bit.
I am stuck for a while with list navigation and can't seem to fix the issue in any way.
Here is what I've done:
struct BlogNavigationGraph: View {
@State private var path = [PostUiModel]()
var body: some View {
NavigationStack(path: $path) {
PostListView(viewModel: /* pass view model here */)
.navigationDestination(for: PostUiModel.self) { post in
PostDetailsView(post: post, viewModel: /* pass view model here */)
}
}
}
}
struct PostListView: View {
@ObservedObject var viewModel: PostListViewModel
var body: some View {
Group {
switch viewModel.state {
case .loading: LoadingView()
case .error: ErrorView()
case .hasPosts(let data): PostList(data)
}
}.onAppear { viewModel.onViewDidAppear() }
}
}
struct PostList: View {
let data: [PostUiModel]
var body: some View {
List(data, id: \.id) { item in
PostItem(post: item)
}
}
}
struct PostItem: View {
let post: PostUiModel
var body: some View {
NavigationLink(value: post) {
PostItemContent(post: post)
}
}
}
struct PostItemContent: View {
var body: some View {
// complex content here
}
}
The problem is that when I first tap on a list item in PostListView
, the PostDetailsView
is instantiated twice for the same post and this breaks my logic. When I tap on a list item again, the PostDetailsView
is instantiated only once and all works as expected.
If I use a NavigationLink
with destination and label in the PostListView
, then it works as expected, but I need to be able to navigate to nested PostDetailsView
s from the PostDetailsView
(if there are link to other posts in the same blog) and I can't really do this without the programmatic navigation using path
and NavigationStack
.
Any ideas what I might be doing wrong?
EDIT - I'm adding a simple view with hardcoded data that can be used to reproduce the issue.
EDIT 2 - I've extended the example to demonstrate the full extent of what I'm doing
import SwiftUI
struct UiModel: Identifiable, Hashable {
let id: Int
let title: String
}
struct TestView: View {
@State private var path = [UiModel]()
var body: some View {
NavigationStack(path: $path) {
TestList()
.navigationDestination(for: UiModel.self) { item in
TestItemDetails(viewModel: TestItemDetailsViewModel(),
item: item) { target in
path.append(target)
}
}
}
}
}
struct TestList: View {
let data = [
UiModel(id: 1, title: "Title 1"),
UiModel(id: 2, title: "Title 2"),
UiModel(id: 3, title: "Title 3")
]
var body: some View {
List(data) { item in
TestItem(item: item)
}
}
}
struct TestItem: View {
let item: UiModel
init(item: UiModel) {
self.item = item
}
var body: some View {
NavigationLink(value: item) {
TestItemContent(item: item)
}
}
}
struct TestItemContent: View {
let item: UiModel
init(item: UiModel) {
self.item = item
}
var body: some View {
Text(item.title)
}
}
struct TestItemDetails: View {
@ObservedObject var viewModel: TestItemDetailsViewModel
let item: UiModel
let onRedirect: (UiModel) -> Void
var body: some View {
VStack {
Text("Details for: {\(item.id), \(item.title)}")
switch viewModel.state {
case .normal: Text("Normal")
case .error: Text("Error")
}
Button("Go to next") {
onRedirect(
UiModel(
id: 5,
title: "Redirected item title"
)
)
}
}.onAppear { viewModel.onViewDidAppear() }
}
}
enum TestItemDetailsState {
case normal
case error
}
class TestItemDetailsViewModel: ObservableObject {
@Published var state: TestItemDetailsState = .normal
func onViewDidAppear() {
state = .error
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
TestItem.init
is called twice for each item.
TestItemContent.init
is called once for each item.
TestItemDetails.init
is called twice for each item you tap on.
When the TestItemDetails.onAppear
gets called, the TestItemDetailsViewModel.onViewDidAppear
gets called, but the change in state is not propagated to the TestItemDetails
view. If I don't use the .navigationDestination
modifier, it all works, but then pushing another TestItemDetails
view on top of the current one is something I can't figure out how to do.