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

Posted

技术标签:

【中文标题】无法在 List ForEach 中以编程方式激活 NavigationLink【英文标题】:Unable to activate NavigationLink programmatically, in a List ForEach 【发布时间】:2021-12-15 18:57:27 【问题描述】:

在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)")
    

无法编译:

无法将类型“Bool”的值转换为预期的参数类型“Binding”

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

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

更新:

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

对于更长的列表,当应为滚动到屏幕外的游戏编号激活 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") 
                )
          
       
    

【问题讨论】:

【参考方案1】:

问题是您需要一个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 更有意义,但如果需要,可以很容易地改回来.

【讨论】:

IIRC,这仅适用于屏幕上可见的 NavigationLinks @AlexanderFarber 我已经更新了我的答案——你在正确的轨道上添加了一个 NavigationLink 在顶部,但你还必须使用绑定的 set 侧——否则它'只会工作一次。 也许……我必须做一些测试。在 iPad(或 Mac)上可能存在显示两列的情况,并且突出显示仍然正确是很重要的。 两列中的突出显示似乎并不能真正在两列导航中可靠地工作......也许还有另一个技巧 当用户从游戏页面返回时调用。【参考方案2】:

你也这样过

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)")
    

【讨论】:

真的有用吗? .constant 可能永远不会改变?我在my test project 中尝试过您的建议,单击“加入随机游戏”按钮几次后,它会产生不稳定的行为。 @AlexanderFarber 我检查我的代码并更新答案请检查 这也行得通! (遗憾的是,也仅适用于屏幕上显示的 NavigationLinks)。谢谢,点赞

以上是关于无法在 List ForEach 中以编程方式激活 NavigationLink的主要内容,如果未能解决你的问题,请参考以下文章

在 Unity 中以编程方式创建动画?

无法在 WebView(网页)中以编程方式单击按钮

在 Woocommerce 3 中以编程方式设置自定义运费

无法在 UITableViewCell 中以编程方式创建的 UIButton 上设置图像

无法在 Android 中以编程方式应用系统屏幕亮度

为啥我无法在 AppDelegate 中以编程方式更改我的初始视图控制器?