NavigationSplitView with List selection of different types

Published by malhal on

I came across an interesting Stack Overflow question about how to support List selection of different types when using NavigationSplitView when in regular width, e.g. on iPad landscape. This problem doesn’t arise with NavigationStackView because multiple ForEach can simply iterate an array of each value and then there is a .navigationDestination for each type. In the SplitView, we have to make use of the List(selection:) binding which can only be one type. I recently discovered this blog post which is using an enum when implementing Focusable and thought it could also be applied to this problem and seems to work. It was implemented by:

  1. using an enum type for the selection binding that wraps each selectable type.
  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. Below is a copy of the code I shared in my answer.
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
}
Categories: SwiftUI