admin 管理员组

文章数量: 1086019

I have seen many questions where this happens and they are using NavigationLink(destination: but this case here feels unique. Apologies because it is a bit convoluted but what is interesting is there are a multiple different ways to "fix" the problem.

In the example code when you select a Dog NavigationLink the app opens that Dog double deep. Root->Dog(id: 0, "Spot")->Dog(id: 0, "Spot")

I know this set up is a bit ridiculous but I have narrowed this down to its most minimal form.

As far as models we have

struct Dog: Identifiable, Hashable {
    var id = UUID()
    var name: String
}

struct Cat: Identifiable, Hashable {
    var id = UUID()
    var name: String
}
class NavigationController: ObservableObject {
    @Published var path = NavigationPath()
}

class Model: ObservableObject {
    @Published var dogs: [Dog] = []
    @Published var isLoading: Bool = false
    
    func loadDogs() {
        guard dogs.isEmpty else { return }
        Task { @MainActor in
            isLoading = true
            try? await Task.sleep(for: .seconds(1))
            
            self.dogs = [Dog(name: "Fifi"), Dog(name: "Spot")]
            isLoading = false
        }
    }
}

As far as views we have

struct ContentView: View {
    @StateObject private var navigationController = NavigationController()
    @StateObject private var model = Model()
    
    var body: some View {
        TabView {
            MainView(model: model)
                .tabItem {
                    Label("Analytics", systemImage: "chart.bar")
                }
        }
        .environmentObject(navigationController)
    }
}

struct MainView: View {
    @EnvironmentObject var navigationController: NavigationController
    @ObservedObject public var model: Model
    
    var body: some View {
        NavigationStack(path: $navigationController.path) {
            if model.isLoading {
                ProgressView()
            } else {
                List {
                    Section("Good Boys") {
                        ForEach(model.dogs) { dog in
                            NavigationLink(value: dog) {
                                Text(dog.name)
                            }
                        }
                    }
                }
                .navigationTitle("Dogs")
                .navigationDestination(for: Dog.self) { dog in
                    Text(dog.name)
                }
                .navigationDestination(for: Cat.self) { cat in
                    CatScreen(cat: cat, model: model)
                }
            }
        }
        .onAppear {
            model.loadDogs()
        }
    }
}

Whenever you press the NavigationLink the view gets pushed into the stack twice. There are a couple ways to prevent this. And they all work independently:

  1. You can get rid of the NavigationController and @Environment and just have the path be a @State in MainView
  2. Get rid of the TabView in MainView (yes I know in this example there is only one tab. In my app there are multiple so removing this is not an option)
  3. Get rid of the navigationDestination for Cat (In my app there is a section where you can select a cat but again this is over simplified)
  4. Remove the model from CatScreen(). If the view built in the navigationDestination does not take in model for some reason everything works fine.
  5. Remove the if branch with the ProgressView. Similarly you can put that whole thing in a Group and put the navigationDestination up there.

5 is the only solution that really makes sense to me. But even then I do not think this behavior is documented?

In the documentation Apple says

Add this view modifier to a view inside a NavigationStack to describe the view that the stack displays when presenting a particular kind of data.

Do not put a navigation destination modifier inside a “lazy” container, like List or LazyVStack. These containers create child views only when needed to render on screen. Add the navigation destination modifier outside these containers so that the navigation stack can always see the destination.

However in this case there is no empty container. I understand under the hood I get a

/// Provides support for "if" statements in multi-statement closures, producing
/// ConditionalContent for the "then" branch.
public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -›
_ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent: View

But even given that the navigationDestination and the structured path should be visible and stable no matter the value of loading. They would've needed to mention in the documentation that it does not work for conditional content. I also tested and there seems to be no reason that navigationDestination needs to be in the top view under a NavigationStack.

Also why do 1-4 change anything here?

本文标签:

Error[2]: Invalid argument supplied for foreach(), File: /www/wwwroot/roclinux.cn/tmp/view_template_quzhiwa_htm_read.htm, Line: 58
File: /www/wwwroot/roclinux.cn/tmp/route_read.php, Line: 205, include(/www/wwwroot/roclinux.cn/tmp/view_template_quzhiwa_htm_read.htm)
File: /www/wwwroot/roclinux.cn/tmp/index.inc.php, Line: 129, include(/www/wwwroot/roclinux.cn/tmp/route_read.php)
File: /www/wwwroot/roclinux.cn/index.php, Line: 29, include(/www/wwwroot/roclinux.cn/tmp/index.inc.php)