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

761 Views Asked by At

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
}
1

There are 1 best solutions below

3
On BEST ANSWER

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.