首页 > 解决方案 > SwiftUI NavigationLink - “懒惰”的目的地?或者如何不编码重复的“标签”?

问题描述

(在带有 Xcode 12 beta 的 macOS Big Sur 上)我在 SwiftUI 中一直在努力解决的一件事是从例如 a 导航,SetUpGameView它需要Game根据用户在此视图中输入的玩家名称创建一个结构,然后继续导航到GameViewvia 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!)destionationNavigationLink 初始化程序中不会延迟评估,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游戏非零之前启用。

标签: swiftswiftuixcode12

解决方案


您可以尝试使用@Binding来跟踪您的Game. 这意味着无论何时导航到GameView,您都拥有最新Game的,因为它是通过@Binding而不是通过初始化程序设置的。

我制作了一个简化版本来展示这个概念:

struct ContentView: View {
    
    @State private var playerName = ""
    @State private var game: Game?
    
    var body: some View {
        NavigationView {
            VStack {
                Form {
                    TextField("Player Name", text: $playerName) {
                        setGame()
                    }
                }
                
                NavigationLink("Go to Game", destination: GameView(game: $game))
                
                Spacer()
            }
        }
    }
    
    private func setGame() {
        game = Game(title: "Some title", playerName: playerName)
    }
}



struct Game {
    let title: String
    let playerName: String
}



struct GameView: View {
    
    @Binding var game: Game!
    
    var body: some View {
        VStack {
            Text(game.title)
            Text(game.playerName)
        }
    }
}

推荐阅读