
モーダルを開いている最中に、ユーザーが下スワイプでうっかり閉じてしまう――フォーム入力や未保存データがあるときには避けたい事態です。
そんな“誤閉じ”を防止できるのが .interactiveDismissDisabled(_:)
です。
.sheet
や .fullScreenCover
などのインタラクティブな閉じ動作(スワイプや背景タップ)を無効化し、意図したタイミングでだけ閉じられるようにします。
.interactiveDismissDisabled(_:)とは?
.interactiveDismissDisabled(_:)
は、表示中のシートなどに対してユーザー操作によるインタラクティブな閉じ(ドラッグ・背景タップ等)を禁止または許可するSwiftUIの修飾子です。
true
を渡すとスワイプや背景タップで閉じられなくなり、明示的な「閉じる」ボタンや保存完了後のプログラムによるDismissに限定できます(iOS 15以降)。
.interactiveDismissDisabled(_:)の使い方
.sheet
内の表示側ビューに対して .interactiveDismissDisabled(true)
を付けるだけで、下スワイプで閉じられなくなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct ContentView: View { @State private var show = false var body: some View { Button("編集を開く") { show = true } .sheet(isPresented: $show) { EditorSheet() .interactiveDismissDisabled(true) // スワイプや背景タップで閉じられない } } } struct EditorSheet: View { @Environment(\.dismiss) private var dismiss var body: some View { VStack(spacing: 16) { Text("ここで編集…") Button("保存して閉じる") { dismiss() } // 明示的に閉じる導線は用意する } .padding() } } |
ポイント
true
でインタラクティブな閉じのみを無効化します。dismiss()
などのプログラムによる閉じは引き続き可能です。- 「閉じる手段をなくす」のではなく、閉じ方を制御するイメージです。
具体例
ここからは実務でよく使うパターンを段階的に紹介します。
コードの後に動きと使いどころを解説します。
1. 未保存フラグでロックを自動切替
未保存のときだけロックし、保存や破棄で解除します。自然な体験に繋がります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
struct EditorSheet: View { @Environment(\.dismiss) private var dismiss @State private var text = "" @State private var hasUnsaved = false var body: some View { VStack(spacing: 12) { TextField("メモを入力", text: $text) .textFieldStyle(.roundedBorder) .onChange(of: text) { hasUnsaved = true } HStack { Button("破棄") { text = "" hasUnsaved = false dismiss() } Button("保存") { // 保存処理… hasUnsaved = false dismiss() } .buttonStyle(.borderedProminent) } } .padding() .interactiveDismissDisabled(hasUnsaved) // 変更があれば閉じをロック } } |
解説
hasUnsaved
が true
の間だけスワイプ閉じを禁止します。
保存・破棄で false
に戻すと、通常の閉じ方に戻ります。
閉じられない理由が分かるUI(保存/破棄ボタン)を必ず用意しましょう。
2. 確認ダイアログを挟んで安全に離脱
ロックに加えて「閉じる」を押したときだけ確認を挟みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
struct GuardedEditor: View { @Environment(\.dismiss) private var dismiss @State private var text = "" @State private var hasUnsaved = false @State private var askDiscard = false var body: some View { VStack { TextEditor(text: $text) .onChange(of: text) { hasUnsaved = true } Button("閉じる") { if hasUnsaved { askDiscard = true } else { dismiss() } } } .padding() .interactiveDismissDisabled(hasUnsaved) .confirmationDialog("変更を破棄しますか?", isPresented: $askDiscard) { Button("変更を破棄", role: .destructive) { hasUnsaved = false dismiss() } Button("キャンセル", role: .cancel) { } } } } |
解説
ユーザーの意思で閉じたいときは尊重しつつ、未保存ならワンクッション置けます。
誤操作をさらに減らせます。
3. Dragインジケータとの整合を取る
ドラッグできそうな見た目のままだと混乱を招きやすいので、見た目も合わせます。
1 2 3 4 5 6 7 |
.sheet(isPresented: $show) { EditorSheet() .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) // 「ドラッグで閉じられそう」な見た目を抑える .interactiveDismissDisabled(true) // 実際のインタラクティブ閉じを禁止 } |
解説
見た目(ドラッグインジケータ)と挙動(ロック)を一致させると、意図が伝わりやすくなります。
.presentationDetents を併用するケースでは特に有効です。
活用シーン
.interactiveDismissDisabled(_:)
は、安易に閉じてほしくない場面で力を発揮します。
- 未保存フォーム:プロフィール編集、投稿作成、長文入力などでの入力破棄防止に向きます。
- 重要フローの途中:支払い・削除・送信の確認など、確実に完了まで進めたい場面に適しています。
- レビューや承認タスク:チェックが完了するまで画面内に留めたいときに有効です。
- ガイド・チュートリアル:初回体験での途中離脱を避け、意図した順序で学んでもらえます。
注意点
便利な一方で、行き止まりUIにならないよう配慮が必要です。
次のポイントを押さえて設計しましょう。
- 無効化するのは“インタラクティブな閉じ”だけです。
@Environment(\.dismiss)
などコードからの閉じは常に可能です。 - ロック中は明確な退出導線(保存/破棄/戻る)を必ず配置します。導線がないとユーザーが抜け出せません。
- ドラッグ可能そうな見た目が残っていると混乱します。必要に応じて
.presentationDragIndicator(.hidden)
を併用してください。 - OSやプレゼンテーションの種類で挙動に差があります(
sheet
、fullScreenCover
、一部popover
など)。対象OS・デバイスで実機検証を行ってください。 - iOS 15以降が前提です。
まとめ
.interactiveDismissDisabled(_:)
は、モーダルの誤閉じを防ぎ、データ損失を避けるための要となる修飾子です。
未保存フラグに応じてロックし、保存/破棄などの明示的なボタンで安全に閉じる――これが現場での基本パターンです。
.presentationDetents
や .presentationDragIndicator
と組み合わせ、見た目と挙動の整合性を保ちながら、安心して使えるシート体験を設計しましょう。
参考リンク
- interactiveDismissDisabled(_:) | Apple Developer Documentation
- presentationDetents(_:selection:) | Apple Developer Documentation
- presentationDragIndicator(_:) | Apple Developer Documentation