无法在 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
?)-
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)")
}
}
在 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
?)-
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)")
}
}