Home > Software design >  Unable to activate NavigationLink programmatically, in a List ForEach
Unable to activate NavigationLink programmatically, in a List ForEach

Time:11-01

In a screenshot

The file GamesViewModel.swift simulates a list of numerical game ids, coming via Websockets in my real app:

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

My problem is that I am trying to activate a random NavigationLink in the ContentView.swift programmatically, when the Button "Join a random game" is clicked:

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

However, the code

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

does not compile:

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

And when I try changing to (removing the dollar sign):

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

it still does not compile!

Is it even possible to use a $ inside of a ForEach?

My context is that in my real app the Jetty backend creates a new game in the PostgreSQL database and then sends the numerical id of that new game to the app. And the app should display that game, i.e. navigate to the GameView.swift programmatically.

CodePudding user response:

The issue is you need a Binding<Bool> -- not just a simple boolean condition. Adding $ gives you a binding to the displayedGame (ie an Binding<Int?>), but not a Binding<Bool>, which is what the NavigationLink expects.

One solution is to create a custom Binding for the condition you're looking for:

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

I changed displayedGame to an Int? because I think semantically it makes a little more sense to be an Optional rather than set to 0 if there's no displayed game, but that could easily be changed back if need be.

CodePudding user response:

You also pass like this

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