无法在 List ForEach 中以编程方式激活 NavigationLink

Unable to activate NavigationLink programmatically, in a List ForEach

simple test project at Github 中,我显示 NavigationLink 项目列表:

文件 GamesViewModel.swift 模拟了一个数字游戏 ID 列表,在我的真实应用中通过 Websockets 获得:

class GamesViewModel: ObservableObject /*, WebSocketDelegate */ {
    @Published var currentGames: [Int] = [2, 3]
    @Published var displayedGame: Int = 0
    
    func updateCurrentGames() {
        currentGames = currentGames.count == 3 ?
            [1, 2, 3, 4] : [2, 5, 7]
    }

    func updateDisplayedGame() {
        displayedGame = currentGames.randomElement() ?? 0
    }
}

我的问题是,当单击“加入随机游戏”按钮时,我试图以编程方式激活 ContentView.swift 中的随机 NavigationLink:

struct ContentView: View {
    @StateObject private var vm:GamesViewModel = GamesViewModel()

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(vm.currentGames, id: \.self) { gameNumber in
                        NavigationLink(destination: GameView(gameNumber: gameNumber)
                                       /* , isActive: $vm.displayedGame == gameNumber */ ) {
                            Text("Game #\(gameNumber)")
                        }
                    }
                }
                Button(
                    action: { vm.updateCurrentGames() },
                    label: { Text("Update games") }
                )

                Button(
                    action: { vm.updateDisplayedGame() },
                    label: { Text("Join a random game") }
                )
          }
       }
    }
}

然而,代码

ForEach(vm.currentGames, id: \.self) { gameNumber in
    NavigationLink(destination: GameView(gameNumber: gameNumber),
                   isActive: $vm.displayedGame == gameNumber ) {
        Text("Game #\(gameNumber)")
    }
}

不编译:

Cannot convert value of type 'Bool' to expected argument type 'Binding'

甚至可以在 ForEach 中使用 $ 吗?

我的上下文是,在我的真实应用程序中,Jetty 后端在 PostgreSQL 数据库中创建了一个新游戏,然后将该新游戏的数字 ID 发送到该应用程序。该应用程序应显示该游戏,即以编程方式导航至 GameView.swift

更新:

用户 jnpdx 和 Dhaval 都提出了有趣的解决方案(谢谢!)- 但是当 NavigationLinks 在屏幕上可见时,它们只适用于短列表。

对于较长的列表,当应为滚动到屏幕外的游戏编号激活 NavigationLink - 它们不起作用!

我已经尝试通过在屏幕顶部使用 NavigationLink/EmptyView 来实现我自己的解决方案,以确保它始终可见并且可以被触发以转换到 vm.displayedGame 号码。

但是我的代码不起作用,即只起作用一次(也许我需要在导航回主屏幕后以某种方式设置 displayedGame = 0?)-

这里ContentView.swift:

struct ContentView: View {
    @StateObject private var vm:GamesViewModel = GamesViewModel()

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(
                        destination: GameView(gameNumber: vm.displayedGame),
                        isActive: Binding(get: { vm.displayedGame > 0 }, set: { _,_ in })
                ) {
                    EmptyView()
                }
                List {
                    ForEach(vm.currentGames, id: \.self) { gameNumber in
                        NavigationLink(
                                destination: GameView(gameNumber: gameNumber)
                            ) {
                            Text("Game #\(gameNumber)")
                        }
                    }
                }
                Button(
                    action: { vm.updateCurrentGames() },
                    label: { Text("Update games") }
                )
                Button(
                    action: { vm.updateDisplayedGame() },
                    label: { Text("Join a random game") }
                )
          }
       }
    }
}

问题是您需要一个 Binding<Bool> -- 而不仅仅是一个简单的布尔条件。添加 $ 可以绑定到 displayedGame(即 Binding<Int?>),而不是 Binding<Bool>,这是 NavigationLink 所期望的。

一种解决方案是为您要查找的条件创建自定义绑定:

class GamesViewModel: ObservableObject /*, WebSocketDelegate */ {
    @Published var currentGames: [Int] = [2, 3]
    @Published var displayedGame: Int?
    
    func navigationBindingForGame(gameNumber: Int) -> Binding<Bool> {
        .init {
            self.displayedGame == gameNumber
        } set: { newValue in
            self.displayedGame = newValue ? gameNumber : nil
        }
    }
    
    func updateCurrentGames() {
        currentGames = currentGames.count == 3 ? [1, 2, 3, 4] : [2, 5, 7]
    }

    func updateDisplayedGame() {
        displayedGame = currentGames.randomElement() ?? 0
    }
}


struct ContentView: View {
    @StateObject private var vm:GamesViewModel = GamesViewModel()

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(isActive: vm.navigationBindingForGame(gameNumber: vm.displayedGame ?? -1)) {
                    GameView(gameNumber: vm.displayedGame ?? 0)
                } label: {
                    EmptyView()
                }
                List {
                    ForEach(vm.currentGames, id: \.self) { gameNumber in
                        NavigationLink(destination: GameView(gameNumber: gameNumber),
                                       isActive: vm.navigationBindingForGame(gameNumber: gameNumber)
                        ) {
                            Text("Game #\(gameNumber)")
                        }
                    }
                }
                Button(
                    action: { vm.updateCurrentGames() },
                    label: { Text("Update games") }
                ).padding(4)

                Button(
                    action: { vm.updateDisplayedGame() },
                    label: { Text("Join a random game") }
                ).padding(4)
                
          }.navigationBarTitle("Select a game")
       }
    }
}

我将 displayedGame 更改为 Int?,因为我认为从语义上讲,如果没有显示的游戏,将其设置为 Optional 比设置为 0 更有意义,但是如果需要,可以很容易地改回来。

你也是这样通过的

ForEach(vm.currentGames, id: \.self) { gameNumber in
    NavigationLink(destination: GameView(gameNumber: gameNumber),
                   isActive: Binding(get: {
                            vm.displayedGame == gameNumber
                        }, set: { (v) in
                            vm.displayedGame = v ? gameNumber : nil
                        })) {
        Text("Game #\(gameNumber)")
    }
}