(在带有 Xcode 12 beta 的 macOS Big Sur 上)我在 SwiftUI 中一直在努力解决的一件事是从例如 a 导航,SetUpGameView
它需要Game
根据用户在此视图中输入的玩家名称创建一个结构,然后继续导航到GameView
via NavigationLink,类似于按钮,当var game: Game?
为 nil时应禁用。在用户输入所有必要的数据之前,无法初始化游戏,但在下面的示例中,我只需要playerName: String
非空即可。
我有一个不错的解决方案,但它似乎太复杂了。
下面我将介绍多种解决方案,所有这些似乎都太复杂了,我希望你能帮助我想出一个更简单的解决方案。
游戏结构
struct Game {
let playerName: String
init?(playerName: String) {
guard !playerName.isEmpty else {
return nil
}
self.playerName = playerName
}
}
设置游戏视图
天真的(非工作)实现是这样的:
struct SetUpGameView: View {
// ....
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
// ...
NavigationLink(
destination: GameView(game: game!),
label: { Label("Start Game", systemImage: "play") }
)
.disabled(game == nil)
// ...
}
}
// ...
}
但是,这不起作用,因为它会使应用程序崩溃。它使应用程序崩溃,因为表达式:GameView(game: game!)
在destionation
NavigationLink 初始化程序中不会延迟评估,game!
早期评估,并且一开始会为零,因此强制展开它会导致崩溃。这对我来说真的很困惑......感觉只是......错了!因为在使用所述导航之前不会使用它,并且在单击 NavigationLink 之前使用此特定初始化程序不会导致使用目标。所以我们必须用一个if let
,但现在它变得有点复杂。我希望 NavigationLink 标签看起来相同,除了禁用/启用渲染,对于两个状态游戏 nil/non nil。这会导致代码重复。或者至少我没有想出比我在下面介绍的解决方案更好的解决方案。下面是两个不同的解决方案和第二个的第三个改进(重构为自定义视图ConditionalNavigationLink
视图)版本......
struct SetUpGameView: View {
@State private var playerName = ""
init() {
UITableView.appearance().backgroundColor = .clear
}
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
VStack {
Form {
TextField("Player name", text: $playerName)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
// All these three solution work
// navToGameSolution1
// navToGameSolution2
navToGameSolution2Refactored
}
}
}
// MARK: Solutions
// N.B. this helper view is only needed by solution1 to avoid duplication in if else, but also used in solution2 for convenience. If solution2 is used this code could be extracted to only occur inline with solution2.
var startGameLabel: some View {
// Bug with new View type `Label` see: https://stackoverflow.com/questions/62556361/swiftui-label-text-and-image-vertically-misaligned
HStack {
Image(systemName: "play")
Text("Start Game")
}
}
var navToGameSolution1: some View {
Group { // N.B. this wrapping `Group` is only here, if if let was inline in the `body` it could have been removed...
if let game = game {
NavigationLink(
destination: GameView(game: game),
label: { startGameLabel }
)
} else {
startGameLabel
}
}
}
var navToGameSolution2: some View {
NavigationLink(
destination: game.map { AnyView(GameView(game: $0)) } ?? AnyView(EmptyView()),
label: { startGameLabel }
).disabled(game == nil)
}
var navToGameSolution2Refactored: some View {
NavigatableIfModelPresent(model: game) {
startGameLabel
} destination: {
GameView(game: $0)
}
}
}
NavigatableIfModelPresent
相同的解决方案,navToGameSolution2
但重构,这样我们就不需要重复标签,或者构造多个AnyView
...
struct NavigatableIfModelPresent<Model, Label, Destination>: View where Label: View, Destination: View {
let model: Model?
let label: () -> Label
let destination: (Model) -> Destination
var body: some View {
NavigationLink(
destination: model.map { AnyView(destination($0)) } ?? AnyView(EmptyView()),
label: label
).disabled(model == nil)
}
}
感觉就像我在这里遗漏了一些东西......我不想在游戏变为非零时自动导航,并且我不希望在NavigationLink
游戏非零之前启用。