The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

SwiftUIのモーダルの表示・非表示

※ iOS16でのやり方です

モーダルの非表示

説明の都合上、先に非表示(モーダルを閉じるとき)についてです。

モーダルを閉じる場合はこのようにEnvironmentのdismissを使って画面を閉じます。

// モーダルで表示する画面
struct ModalContents: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Button("Done") {
            dismiss()
        }
    }
}

このコードを最初見たときに違和感感じませんでしたか?わたしは感じました。

dismissってぱっと見valueっぽいですがdismiss()で呼んでいるのでclosureなのでしょうか。

dismissの正体

EnvironmentValuesにdismissは定義してあり、型は DismissAction でした

public var dismiss: DismissAction { get }

DismissAction はなにかというとstructです。

struct DismissAction

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

structなのになぜ関数のように呼ぶことができるかというとcallAsFunction()が定義されているからです。 https://developer.apple.com/documentation/swiftui/dismissaction/callasfunction()

つまり、dismiss()とかくと暗黙的にdismissのcallAsFunction()を呼んでいることになるのです。

struct ModalContents: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Button("Done") {
            dismiss() // Implicitly calls dismiss.callAsFunction()
        }
    }
}

DismissActionのcallAsFunction()内で画面を閉じる処理が実行されていると考えられます。

モーダルの表示

モーダルを画面を開く場合は、 完全に全画面の場合fullScreenCover, 完全に全画面ではなく上にちょっと浮いた感じにする場合はsheetをを使います。

https://developer.apple.com/documentation/swiftui/view/fullscreencover(ispresented:ondismiss:content:) https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)

struct ContentView: View {
    @State private var isFullScreenCoverViewPresented = false

    var body: some View {
        NavigationStack {
            Group {
                    Button {
                        isFullScreenCoverViewPresented = true
                    } label: {
                        Text("+")
                    }
            }
            .fullScreenCover(isPresented: $isFullScreenCoverViewPresented) {
                ModalContents()
            }
        }
    }

モーダルで開いた画面のナビゲーションバーに閉じるボタンをつける

モーダルで表示する画面側をNavigationStackでwrapしてToolbarItemを追加します。

struct ModalContents: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
          Group {
          }
          .toolbar {
              ToolbarItem(placement: .navigationBarLeading) {
                  Button {
                      dismiss()
                  } label: {
                      Label("Close", systemImage: "xmark")
                  }
              }
          }
      }
    }
}

同じような画面が何個もある場合はこういったContainerViewを用意しておくと ModalView { ModalContents() } とするだけで閉じるボタンが追加できるので便利かもしれません。

struct ModalView<Content: View>: View {
    @Environment(\.dismiss) var dismiss

    var content: () -> Content

    var body: some View {
        NavigationStack {
            content()
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button {
                            dismiss()
                        } label: {
                            Label("Close", systemImage: "xmark")
                        }
                    }
                }
        }
    }
}