Reputation: 22958
In a simple test project at Github I display a List of NavigationLink items:
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'
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.
UPDATE:
The users jnpdx and Dhaval have both suggested interesting solutions (thanks!) - however they only work for short Lists, when the NavigationLinks are visible at the screen.
For longer Lists when a NavigationLink should be activated for the game number which is scrolled offscreen - they do not work!
I have tried implementing my own solution, by using a NavigationLink/EmptyView at the top of the screen, to ensure that it is always visible and can be triggered to transition to the vm.displayedGame
number.
However my code does not work, i.e. only works once (maybe I need to set displayedGame = 0
somehow after navigating back to the main screen?) -
Here 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") }
)
}
}
}
}
Upvotes: 2
Views: 626
Reputation: 52312
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 {
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")
}
}
}
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.
Upvotes: 2
Reputation: 129
You also pass like this
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)")
}
}
Upvotes: 1