Open11

NavigationView

🍤🍤

基本形

NavigationView {
( 表示したいものを入れる )
}

NavigationViewはViewのトップレベルでコンテナのようなイメージで利用されている。
これを使うことで、Viewを横断的に使用することが可能になる。
NavigationViewの中には、ページViewとNavigationLinkが巻かれている。
このページViewも入ってるって話、忘れがち…忘れないかな。
NavigationLinkの中には、クリックによりプッシュされるViewが定義されている。
これにより、目的のViewへ移動することができる。

struct SwiftUIView: View {
    var body: some View {
        NavigationView{
            NavigationLink(destination: SecondView()) {
/// 遷移先への移動イベントを発火させるボタン
                       Text("Transition!")
                   }
        }
    }
}

struct SecondView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

例えば以下みたいなコードを実行すると、NavigationViewが一つのコンテナだというイメージがわかりやすくなるような気がする。

struct SwiftUIView: View {
    var body: some View {
        VStack{
        NavigationView{
            NavigationLink(destination: SecondView()) {
                       Text("Transition!")
                    .navigationTitle("Navigation")
                   }
            }
            NavigationView{
                NavigationLink(destination: SecondView()) {
                           Text("移動するよ")
                        .navigationTitle("次の画面")
                       }
            }
        }
    }
}

結果として表示される挙動は以下。

🍤🍤

タイトルをつける

NavigationViewのデフォルトの表示設定では、ルートViewにはタイトルバーがなく、プッシュされるViewにはデフォルトでリターンバーがつく。

上記のコードに.navigationTitleを追加すると、ルートViewのタイトルバーにタイトルを設定することができるようになる。

NavigationView{
       NavigationLink(destination: SecondView()) {
/// 遷移先への移動イベントを発火させるボタン
                 Text("Transition!")
                 .navigationTitle("Navigation")
                   }
        }
    }
}


NavigationViewではなくテキスト(NavigationView内部にあるView)に付属していることが不思議な感じがする。
ちなみにタイトルをNavigationViewに直に添付した場合、エラーにはならないがTitleが表示されなくなる。(想定外の書き方をしているということになる)

タイトルに修飾子をつける

navigationBarTitleDisplayMode()修飾子をつけることで、タイトルの表示方法をカスタマイズすることができる。
選択肢は3つ。

  1. .large :大きいバージョン。
  2. .inline : 小さいタイトル。もしnavigation stackを積み重ねるとしたら、2次とか3次とかで使うと便利だよって感じっぽい。
  3. .automatic : システムのデフォルト

    largeとautomaticの違いがよくわからない問題。。
    navigation stackを積み重ねるってどういうことだ?
🍤🍤

.navigationViewStyle()

これはNavigationViewにスタイルを設定できるメソッド。
iPadやiPhone13のような画面の広い状態だと、デフォルトでMaster-Detail(Split view)の挙動になる。

struct SwiftUIView: View {
    var body: some View {
        VStack{
            NavigationView{
                NavigationLink(destination: SecondView()) {
                    Text("Transition!")
                        .navigationTitle("Navigation")
                        .navigationBarTitleDisplayMode(.automatic)
                }
            }
        }
    }
}

このコードをiPadで走らせると、こうなる。

こうした時の解決策は2つある。
ひとつは、masterViewを表示する手法。
二つめは、.navigationViewStyle(.stack)を活用する手法。masterViewを表示させたくない(iPhoneと同じような挙動にしたい)時に使用する。画面あたり一つのViewを表示することを強制するスタイル。

masterViewをつかう場合は、NavigationViewに挿入すれば良い。

            NavigationView{
                NavigationLink(destination: SecondView()) {
                    Text("Transition!")
                        .navigationTitle("Navigation")
                        .navigationBarTitleDisplayMode(.automatic)
                }
             MasterView()
            }

            NavigationView{
                NavigationLink(destination: SecondView()) {
                    Text("Transition!")
                        .navigationTitle("Navigation")
                        .navigationBarTitleDisplayMode(.automatic)
                    
                }
            }
            .navigationViewStyle(.stack)
    }
}

🍤🍤

navigation stackを積み重ねるってどういうことかいまいちわかんない。

NavigationLinkはpush操作、戻るはpop操作らしい。

NavigationViewを押すと、Viewの上に遷移先Viewが乗っかる。
戻るを押すと、乗っかっていた遷移先Viewが取り出される。というイメージ。らしい。

https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui

https://developer.apple.com/documentation/swiftui/navigationstack

🍤🍤

NavigationViewでプログラムによる遷移を実装する。

画面遷移は、なにもユーザーがタップするというイベントのみによって発生するものではない。
プログラムによって遷移を行う手法もある。

NavigationViewを用いてプログラムの遷移を行う方法はNavigationLinkに依存する。

NavigationLinkをBool値の状態変化により操作する

SwiftUI は、プログラムによるナビゲーションを行うときでさえ、ある種のビューを NavigationLink に渡すことを要求する。
ここではEmptyView()が渡されている。

 NavigationView {
            VStack {
                NavigationLink(destination: Text("Second View"), isActive: $isShowingDetailView) { EmptyView() }

                Button("Tap to show detail") {
                    isShowingDetailView = true
                }
            }

みてわかるように、実質真偽判定した上でViewの移動をしているだけ。
こうした手法の長所として、Buttonがプログラムナビゲーションを発火させる前に他の作業をいくらでも行うことができる点がある。

通常NavigationLinkはひとつしかNavigationViewに含まれない。
しかし、いくつかの目的地がある場合、複数のNavigationLinkをある選択状態にバインドして、それぞれにタグを与えることができる。
Bool値を更新し、タグを一致させると適切なNavigationLinkがアクティブになり、多くのブール値を使用せずに複数の目的地のプログラムによるナビゲーションが可能になる。

    @State private var selection: String? = nil
    var body: some View {
            NavigationView {
                VStack {
                    NavigationLink(destination: Text("View A"), tag: "A", selection: $selection) { EmptyView() }
                    NavigationLink(destination: Text("View B"), tag: "B", selection: $selection) { EmptyView() }
                    
                    Button("Tap to show A") {
                        selection = "A"
                    }
                    
                    Button("Tap to show B") {
                        selection = "B"
                    }
                }
            }

🍤🍤

dismiss

var dismiss: DismissAction { get }
  • これを使うと現在の環境のDismissActionインスタンスを取得することができる。

https://developer.apple.com/documentation/swiftui/dismissaction

  • dismiss environment value を使用してDismissActionのインスタンスを取得する。インスタンスは解放を行う。(取り出すみたいな感じ)
  • この解放されるインスタンスというのは、現在表示しているViewのことを指している。という感じで良いのか。
  • インスタンスを直接呼び出すときにcallAsFunction() メソッドが動いているらしい。
  • callAsFunction()はインスタンスメソッド
func callAsFunction()
  • DismissActionを呼び出すボタンを作ってみる。
private struct SheetContents: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Button("Done") {
            dismiss()
        }
    }
}
  • dismissアクションは現在表示されていないビューに影響を与えない。

  • dismissはViewそのものに適応するようにすることに注意する。Viewの詳細にくっつけない。

https://developer.apple.com/documentation/swiftui/environmentvalues/dismiss

🍤🍤

SwiftUIのViewって基本Stackなのかな…dismissのところで.sheetが出てきたので、ちょっと気になった。

🍤🍤

navigationViewでViewを包む。
だから、くるんだ先のViewはnavigationViewを入れる必要はない。継承されている。

🍤🍤

NavigationViewで遷移した先のViewにおいて、.toolbar {}の表示が侵食されているような現象が起こっていたので、検証した。

NavigationViewによる遷移で複数Viewから到達できる画面だが、これは特定のViewからの遷移時のみ発生する現象だった。

結論から言うと、おそらく原因は.navigationBarの表示領域の上に

.toolbar {
            ToolbarItem(placement: .automatic) {}
}

がかかっていたこと。
toolbarの表示領域がnavigationBarにより制限されていたことにあるような感じがする。

  • ポイントは"画面によって描画がことなっていた"こと。

  • 最初はSpacer(minLength: 50)を使って制限をかけていることが原因なのではないかと考えた。

  • そこで、動的に余白を形成するpadding(.horizontal,50)を使用してみた。

  • ところが、上下の余白がうまく形成されない。

  • そもそもView領域自体に上下の制限がかかっているようだった。

遷移前のコードを比較した際、

        .navigationBarHidden(true)

がある時は、大元のViewの上に重ねるようにtoolbarを設置できた(表示領域の制限がなかった。)

🍤🍤

BackButtonを消したいとき、遷移先Viewで

.navigationBarBackButtonHidden(true)

としていたが、そういうことしなくても.navigationBarHidden(true)を付け足せば消してくれるっぽい。

NavigationLink(isActive: $isEmpty) {
            DetailPageView()
                .navigationBarHidden(true)   /// ここが大事 
          } label: {
            EmptyView()
  }