[SwiftUI 3.0] 条件でNavigationLinkの遷移先Viewを変更するアレコレ [@ViewBuilder]

## はじめに
意外とそのまま検索しても出なかったので、`NavigationLink`で遷移先となるdestinationを条件で変更する方法について書いておきたいと思います。

## iOS14、15での違い
その前に、`NavigationLink`のイニシャライザでiOS15とiOS14以前で、destinationが取る型に変更があり、推奨される記法が異なっています。

■ iOS14以前の定義は非推奨となった
<img width=650 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/c9141062-9c54-0a1d-7648-0a7e95382e19.png">
<img width=650 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/95b5ff64-ad4f-8467-2e57-3311525f8c58.png">

■ iOS15
<img width=650 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/821eb8d0-8ecf-6c34-7ee2-41e8e40159e2.png">

上記は、[NavigationLink Apple公式ドキュメント](https://developer.apple.com/documentation/swiftui/navigationlink)より、代表的なイニシャライザを抜粋しました。

iOS14以前では destination 単にViewインスタンスを渡す形でしたが、iOS15以降向けでは ViewBuilder を利用したclosureを引数に取れるようになっています。なので、次のその1の項目で示すサンプルコードにバージョンで違いがあります。

## その1: クロージャを利用する方法

サンプルコードは[再開する]といったボタンがある、といったアプリを想定しています。
<img width=200 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/94e7d296-8484-8f69-d446-8ef0ff536540.png">
iOS15は現時点でbetaですので、画像はiOS14での実行キャプチャーです。

```swift:iOS15以後向け
struct RootView: View {
　　...
    var body: some View {
        NavigationView {
            // iOS15
            NavigationLink("再開する", destination: {
                if isLoggedIn {
                    MainView()
                } else {
                    LoginView()
                }
            })
            .buttonStyle(.bordered) // これもiOS15以降で利用可能
        }
    }
}
```

```swift:iOS14以前
        // iOS14
        NavigationLink("再開する", destination: {
            VStack { // コンテナUIに入れ解決させる
                if isLoggedIn {
                    MainView()
                } else {
                    LoginView()
                }
            }
        }())

        もしくは

        NavigationLink("再開する", destination: 
            VStack {
                if isLoggedIn {
                    MainView()
                } else {
                    LoginView()
                }
            }
        )
```

iOS14以前の例では、`@ViewBuilder`属性があるコンテナUIで囲むことでViewインスタンスが解決されるようにします。

## その2: Viewを返すメソッドを作る方法
条件分岐の処理が大きくなって、それをメソッドに括りだしたくなる場合もあると思います。下記のように`nextView()`メソッドに処理を括り出せます。
関数には`@ViewBuilder`属性を付け、戻り値を`some View`にします。

```swift:destinationの取得をnextView()関数に変更
    var body: some View {
        NavigationView {
            NavigationLink("再開する", destination: nextView())
        }
    }

    @ViewBuilder
    private func nextView() -> some View { 
        if isLoggedIn {
            MainView()
        } else {
            LoginView()
        }
    }
```

ちなみに、次の例のように（先ほどのクロージャの例のメソッド版ですが）コンテナUIで囲むことで [some View](https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html) となるインスタンスを返すこともできます。（書き方としては`@ViewBuilder`が明示されている先の例の方が良いと思います。）

```swift:コンテナ系のUIコンポーネントを用いて下記のようにも書けます
    private func nextView() -> some View {
        VStack { // @ViewBuilderの付加なしで some Viewとなるものを返してくれる
            if isLoggedIn {
                MainView()
            } else {
                LoginView()
            }
        }
    }
```

余談ですが、Viewプロトコルの`var body: some View`も、実装時のシグニチャは`@ViewBuilder`属性が省略されていますので、宣言型UIの記法やViewを用いるメソッドには`@ViewBuilder`を付加する、ということですね。

また、以下のように[AnyView](https://developer.apple.com/documentation/swiftui/anyview)を利用することも出来ます。（おすすめされない記法です）

```swift:AnyViewを使う例（非推奨）
    private func nextView() -> AnyView { 
        if isLoggedIn {
            return AnyView(MainView())
        } else {
            return AnyView(LoginView())
        }
    }
```

`AnyView`に関しては、[WWDC21のセッションにて解説](https://developer.apple.com/videos/play/wwdc2021/10022/?time=701)があり、
要旨を抜粋すると、
・一般的には、可能な限り`AnyView`を避けることがお勧めです。
・可能であれば`AnyView`でなく、型情報を保持する為ジェネリックを使用してください。
（理由1）あまりにも多くの`AnyView`があると、コードが読みにくく理解しにくくなります。
（理由2）必要のないときに`AnyView`を使用すると、パフォーマンスが低下します。
ーーー
とのこと。確かに`AnyView`を利用したくなる時は命令型っぽい記述にもなりがちに思うので、（現在時点の見方では）避ける方法があれば利用しない方がパフォーマンス的に良いようです。

## その3: カスタムビューに切り出す方法
最後は、先ほどの例の関数部分を、別のView（`DestinationView`）として切り出しました。

```swift:
struct RootView: View {
    ...
    var body: some View {
        NavigationView {
            NavigationLink("再開する", destination: DestinationView(isLoggedIn: isLoggedIn))
        }
    }
}

struct DestinationView: View {

    let isLoggedIn: Bool

    var body: some View {
        if isLoggedIn {
            MainView()
        } else {
            LoginView()
        }
    }
}
```

この`DestinationView`は、NavigationLinkも含めたカスタムビューにするなどの案も考えられます。
WWDC19 Introducing SwiftUIより↓
![スクリーンショット 2021-07-18 9.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/1851e583-aa48-3dc8-82be-2e1a0e6d8b67.png)

## おわりに
NavigationLinkでの例を題材に、様々な書き方と`@ViewBuilder`周りの利用との考察についてまとめました。

iOSあるあるですが、SwiftUIが発表されて3年目となり、今後業務での採用例も増えてくると思われ、本腰を入れ始めた方も多いと思います。コード例は一長一短があると思いますので、宣言型UIの表現方法を探りつつ、アプリのロジックに合わせてスッキリすると思う方法で書けると良いのではと思います。
自分が試していて、上手くいった例を紹介させていただきました。