はじめに
意外とそのまま検索しても出なかったので、NavigationLink
で遷移先となるdestinationを条件で変更する方法について書いておきたいと思います。
iOS14、15での違い
その前に、NavigationLink
のイニシャライザでiOS15とiOS14以前で、destinationが取る型に変更があり、推奨される記法が異なっています。
上記は、NavigationLink Apple公式ドキュメントより、代表的なイニシャライザを抜粋しました。
iOS14以前では destination 単にViewインスタンスを渡す形でしたが、iOS15以降向けでは ViewBuilder を利用したclosureを引数に取れるようになっています。なので、次のその1の項目で示すサンプルコードにバージョンで違いがあります。
その1: クロージャを利用する方法
サンプルコードは[再開する]といったボタンがある、といったアプリを想定しています。
iOS15は現時点でbetaですので、画像はiOS14での実行キャプチャーです。
struct RootView: View {
...
var body: some View {
NavigationView {
// iOS15
NavigationLink("再開する", destination: {
if isLoggedIn {
MainView()
} else {
LoginView()
}
})
.buttonStyle(.bordered) // これもiOS15以降で利用可能
}
}
}
// 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
にします。
var body: some View {
NavigationView {
NavigationLink("再開する", destination: nextView())
}
}
@ViewBuilder
private func nextView() -> some View {
if isLoggedIn {
MainView()
} else {
LoginView()
}
}
ちなみに、次の例のように(先ほどのクロージャの例のメソッド版ですが)コンテナUIで囲むことで some View となるインスタンスを返すこともできます。(書き方としては@ViewBuilder
が明示されている先の例の方が良いと思います。)
private func nextView() -> some View {
VStack { // @ViewBuilderの付加なしで some Viewとなるものを返してくれる
if isLoggedIn {
MainView()
} else {
LoginView()
}
}
}
余談ですが、Viewプロトコルのvar body: some View
も、実装時のシグニチャは@ViewBuilder
属性が省略されていますので、宣言型UIの記法やViewを用いるメソッドには@ViewBuilder
を付加する、ということですね。
また、以下のようにAnyViewを利用することも出来ます。(おすすめされない記法です)
private func nextView() -> AnyView {
if isLoggedIn {
return AnyView(MainView())
} else {
return AnyView(LoginView())
}
}
AnyView
に関しては、WWDC21のセッションにて解説があり、
要旨を抜粋すると、
・一般的には、可能な限りAnyView
を避けることがお勧めです。
・可能であればAnyView
でなく、型情報を保持する為ジェネリックを使用してください。
(理由1)あまりにも多くのAnyView
があると、コードが読みにくく理解しにくくなります。
(理由2)必要のないときにAnyView
を使用すると、パフォーマンスが低下します。
ーーー
とのこと。確かにAnyView
を利用したくなる時は命令型っぽい記述にもなりがちに思うので、(現在時点の見方では)避ける方法があれば利用しない方がパフォーマンス的に良いようです。
その3: カスタムビューに切り出す方法
最後は、先ほどの例の関数部分を、別のView(DestinationView
)として切り出しました。
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より↓
おわりに
NavigationLinkでの例を題材に、様々な書き方と@ViewBuilder
周りの利用との考察についてまとめました。
iOSあるあるですが、SwiftUIが発表されて3年目となり、今後業務での採用例も増えてくると思われ、本腰を入れ始めた方も多いと思います。コード例は一長一短があると思いますので、宣言型UIの表現方法を探りつつ、アプリのロジックに合わせてスッキリすると思う方法で書けると良いのではと思います。
自分が試していて、上手くいった例を紹介させていただきました。