1

(Xcode 12 ベータ版の macOS Big Sur で) SwiftUI で苦労していたことの 1 つは、たとえば、このビューでユーザーが入力したプレーヤー名に応じて構造体SetUpGameViewを作成する必要があるa からの移動であり、次にvia へGameのナビゲーションに進みます。が nil のGameView場合は無効にする必要がある、ボタンのような NavigationLink 。var game: Game?ユーザーが必要なすべてのデータを入力するまで、ゲームを初期化することはできませんが、以下の例では、playerName: String空でないことが必要です。

まともな解決策がありますが、複雑すぎるようです。

以下に複数の解決策を紹介しますが、どれも複雑すぎるようです。さらに簡単な解決策を考え出すのを手伝ってくれることを願っていました.

ゲーム構造

struct Game {
    let playerName: String
    init?(playerName: String) {
        guard !playerName.isEmpty else {
            return nil
        }
        self.playerName = playerName
    }
}

SetUpGameView

単純な (動作しない) 実装は次のとおりです。

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!早期に評価され、最初は nil になるため、ラップを強制的に解除するとクラッシュが発生します。これは私にとって本当に紛らわしいです...それはただ...間違っているように感じます! 上記のナビゲーションが使用されるまで使用されず、この特定の初期化子を使用しても、NavigationLink がクリックされるまで目的地が使用されないためです。したがって、これをif let、しかし今はもう少し複雑になります。ゲーム nil/非 nil の両方の状態で、無効/有効なレンダリングを除いて、NavigationLink ラベルを同じに見せたいです。これにより、コードの重複が発生します。または、少なくとも、以下に提示するものよりも優れた解決策を思いつきませんでした. 以下は、2 つの異なるソリューションと、2 つ目の改善された (カスタム ビューConditionalNavigationLinkビューにリファクタリングされた) バージョンの 3 つ目です...

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

ここに何かが欠けているように感じます...ゲームが非nilになったときに自動的にナビゲートしたくなく、NavigationLinkゲームが非nilになるまで有効にしたくありません。

4

1 に答える 1

1

@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)
        }
    }
}
于 2020-06-28T11:09:46.617 に答える