Search code examples
iosswiftuiswiftui-navigationlinkswiftui-navigationstack

NavigationStack does not render if child view contains a conditional


I am trying to use SwiftUI's new NavigationStack/NavigationLink pattern but am running into an issue if the view I am navigating to has a conditional render (just a simple if statement) in it, it does shows the yellow triangle error icon and does not render the child view. If I remove this conditional statement, the view renders fine and the navigation works correctly.

I am able to reproduce this with a minimal example:

import SwiftUI

struct Thing: Identifiable, Hashable {
    let id: String = UUID().uuidString
    let name: String
    var description: String = ""
}

struct ContentView: View {
    let things: [Thing] = [
        Thing(name: "Thing 1", description: "Something new"),
        Thing(name: "Thing 2"),
        Thing(name: "Thing 3", description: "Something blue")
    ]
    
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                ForEach(things, id: \.self) { thing in
                    NavigationLink(value: thing) {
                        Text(thing.name)
                    }
                }
            }
            .navigationDestination(for: Thing.self) { thing in
                DetailView(path: $path, thing: thing)
            }
            .padding()
        }
    }
}

struct DetailView: View {
    @Binding var path: NavigationPath
    
    let thing: Thing
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Text(thing.id)
                Text(thing.name)
                if !thing.description.isEmpty {
                    Text(thing.description)
                }
            }
        }
    }
}

The issue seems to be with this block:

if !thing.description.isEmpty {
    Text(thing.description)
}

If I remove this, the navigation works correctly. With this simple if statement, the navigation will not render the child view.

This is reproducible using Xcode 15.2 with the simulator running iOS 17.2

Behavior with conditional:

Behavior with conditional

Without conditional:

Behavior without conditional

What am I doing wrong? Is there a piece of this puzzle that I am missing or misusing?


Solution

  • You are using multiple NavigationStack. Ideally, you should use the NavigationStack on the parent/root view only. Try the following:

    
    import SwiftUI
    
    struct Thing: Identifiable, Hashable {
        let id: String = UUID().uuidString
        let name: String
        var description: String = ""
    }
    
    struct ContentView: View {
        let things: [Thing] = [
            Thing(name: "Thing 1", description: "Something new"),
            Thing(name: "Thing 2"),
            Thing(name: "Thing 3", description: "Something blue")
        ]
        
        @State private var path = NavigationPath()
        
        var body: some View {
            NavigationStack(path: $path) {
                VStack {
                    ForEach(things, id: \.self) { thing in
                        NavigationLink(value: thing) {
                            Text(thing.name)
                        }
                    }
                }
                .navigationDestination(for: Thing.self) { thing in
                    DetailView(path: $path, thing: thing)
                }
                .padding()
            }
        }
    }
    
    struct DetailView: View {
        @Binding var path: NavigationPath
        
        let thing: Thing
        
        var body: some View {
            VStack {
                Text(thing.id)
                Text(thing.name)
                if !thing.description.isEmpty {
                    Text(thing.description)
                }
            }
        }
    }
    
    

    One more suggestion, which is out of scope of your question. But I see you are using NavigationPath as @State var in your parent view. As your child views need the path so you need to pass the path during view initialization each time.

    I would suggest you to use EnvironmentObject so that you don't have to pass the path as argument each time. You can follow the link to know more.