Search code examples
swiftuiswiftui-navigationsplitview

Where does the navigation destination go in an iOS 16 NavigationSplitView?


I've found this great video from Sean Allen https://www.youtube.com/watch?v=oxp8Qqwr4AY on having two different structs in a NavigationStack, but I cannot figure out how to make this work if I am using a NavigationSplitView.

This is the code, which compiles, but I get the console error:

A NavigationLink is presenting a value of type “Game” but there is no matching navigation destination visible from the location of the link. The link cannot be activated.

So, Xcode seems to think I should be able to give it a navigation destination, but where? Or is this limited to the NavigationStack?

struct ContentView: View {
    
    var platforms: [Platform] = [
        .init(name: "Xbox", imageName: "xbox.logo", color: .green),
        .init(name: "Playstation", imageName: "playstation.logo", color: .indigo),
        .init(name: "PC", imageName: "pc", color: .yellow),
        .init(name: "Mobile", imageName: "iphone", color: .mint),
    ]
    
    var games: [Game] = [
        .init(name: "Minecraft", rating: "5"),
        .init(name: "Gof of War", rating: "15"),
        .init(name: "Fortnite", rating: "25"),
        .init(name: "Civ 5", rating: "20"),
    ]
    
    var body: some View {
        NavigationSplitView {
            List {
                Section("Platforms"){
                    ForEach(platforms, id: \.name) { platform in
                        NavigationLink(value: platform){
                            Label(platform.name, systemImage: platform.imageName)
                                .foregroundColor(platform.color)
                        }
                    }
                }
                Section("Games"){
                    ForEach(games, id: \.name) { game in
                        NavigationLink(value: game) {
                            Label(game.name, systemImage: "\(game.rating).circle.fill")
                        }
                    }
                }
            }
            .navigationTitle("Gaming")
            .navigationDestination(for: Platform.self) { platform in
                ZStack {
                    platform.color.ignoresSafeArea()
                    Label(platform.name, systemImage: platform.imageName)
                }
            }
            .navigationDestination(for: Game.self) { game in
                Text("\(game.name)  Rating \(game.rating) ")
            }
        } detail: {
            // ???
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct Platform: Hashable {
    let name: String
    let imageName: String
    let color: Color
}


struct Game: Hashable {
    let name: String
    let rating: String
}

Solution

  • It's limited to NavigationStack, however you can workaround by:

    1. using an enum type for the selection binding.
    2. making use of the fact that Swift enums support associated values.
    3. using .tag for custom List row IDs.
    4. using a switch statement for the detail.
    5. switching an optional goes to default case when nil.

    e.g.

    struct ContentView: View {
        
        enum Selection: Hashable {
            case platform(id: Platform.ID)
            case game(id: Game.ID)
        }
        
        var platforms: [Platform] = [
            .init(name: "Xbox", imageName: "xbox.logo", color: .green),
            .init(name: "Playstation", imageName: "playstation.logo", color: .indigo),
            .init(name: "PC", imageName: "pc", color: .yellow),
            .init(name: "Mobile", imageName: "iphone", color: .mint),
        ]
        
        var games: [Game] = [
            .init(name: "Minecraft", rating: "5"),
            .init(name: "Gof of War", rating: "15"),
            .init(name: "Fortnite", rating: "25"),
            .init(name: "Civ 5", rating: "20"),
        ]
        
        @State var selection: Selection?
        
        var body: some View {
            NavigationSplitView {
                List(selection: $selection) {
                    Section("Platforms"){
                        ForEach(platforms) { platform in
                            Label(platform.name, systemImage: platform.imageName)
                                .foregroundColor(platform.color)
                                .tag(Selection.platform(id: platform.id))
                            
                        }
                    }
                    Section("Games"){
                        ForEach(games) { game in
                            Label(game.name, systemImage: "\(game.rating).circle.fill")
                                .tag(Selection.game(id: game.id))
                        }
                    }
                }
                .navigationTitle("Gaming")
            } detail: {
                switch(selection) {
                    case .platform(let id):
                        if let platform = platforms.first(where: { $0.id == id }) {
                            ZStack {
                                platform.color.ignoresSafeArea()
                                Label(platform.name, systemImage: platform.imageName)
                            }
                        }
                    case .game(let id):
                        if let game = games.first(where: { $0.id == id }) {
                            Text("\(game.name)  Rating \(game.rating) ")
                        }
                    default:
                        Text("Make a selection")
                }
            }
        }
    }
    
    struct Platform: Identifiable {
        let id = UUID()
        let name: String
        let imageName: String
        let color: Color
    }
    
    
    struct Game: Identifiable {
        let id = UUID()
        let name: String
        let rating: String
    }
    

    I got the idea from the use of enums with Focusable demonstrated in this blog post.