モーダル表示は、現在のコンテキストの上に画面を表示し、以前のインターフェースとの操作をブロックする方法です。モバイル開発では、モーダルウィンドウは集中タスク(データ入力、アクション確認、認証、オプション選択)に使用されます。Apple HIG、2025によると、モーダル表示はアプリケーションのナビゲーションシナリオの20%を超えないようにする必要があります。Androidでは、モーダル性はDialogFragment、BottomSheet、および特定の起動フラグを持つActivityを通じて実装されます。
重要なポイント
モーダル表示は、現在の画面の上に新しい画面が表示され、一時的に親コンテンツとの操作をブロックするナビゲーションパターンです。ユーザーは以前の状態に戻るために、モーダルタスク(キャンセル、保存、閉じる)を明示的に完了する必要があります。
モーダル性は認知タスクを解決します。ユーザーの注意をインターフェースの他の部分から気を散らすことなく、単一のアクションに集中させます。これは、登録フォーム、確認ダイアログ、ファイル選択、サードパーティサービスによる認証にとって極めて重要です。AppleのHuman Interface Guidelinesは、続行する前に完了が必要なタスクにのみモーダル性を使用することを推奨しています。
Webのモーダルウィンドウとは異なり、モバイルのモーダル表示は全画面(画面全体を占有)または部分表示(Page Sheet、Bottom Sheet)にすることができます。タイプの選択は、タスクのコンテキストとプラットフォームの慣例によって異なります。iOSはほとんどのシナリオでPage Sheetを採用し、Full Screenはビデオプレーヤーやフォトエディターに残しています。
Push表示(スタックナビゲーション)は、ナビゲーションスタックに画面を追加し、自動的に戻るボタンを表示します。ユーザーはいつでも前の画面に戻ることができます。一方、モーダル表示は明示的な完了が必要です。戻るボタンは存在しないか、前の画面に戻る代わりにモーダルウィンドウを閉じます。
モーダル表示とPush表示の主な違い:モーダル表示はデータを失うことなく後方ナビゲーションをブロックし、閉じるためのアクション(Save、Cancel、Done)が必要で、通常は個別のタスクを表します。Push表示はナビゲーション階層を保持し、自動的に戻るボタンを追加し、順次コンテンツ表示に適しています。
| 特徴 | モーダル表示 | Push表示 |
|---|---|---|
| 戻るブロック | はい、明示的なアクションが必要 | いいえ、戻るボタンは常に利用可能 |
| 典型的な使用法 | フォーム、認証、選択 | 詳細表示、ナビゲーション |
| アニメーション | 下から上へ(iOS)、スライド(Android) | 右から左へ(iOS) |
| ナビゲーションスタック | メインスタックに追加されない | スタックに追加される |
実際には、ModalとPushの選択はコンテキストによって異なります。続行する前にユーザーが完了する必要があるタスクにはモーダル性を使用し、順次コンテンツ探索にはPushを使用することをお勧めします。同じ画面でパターンを混在させると混乱を招き、UXが低下します。
iOSはUIModalPresentationStyle列挙型を通じて複数のモーダル表示スタイルを提供します。UIKitは.fullScreen(全画面)、.pageSheet(上部マージン付きカード)、.formSheet(iPadの中央ウィンドウ)、.automatic(コンテキストに基づいてシステムが選択)をサポートしています。iOS 13以降、デフォルトスタイルは.automaticになり、iPhoneでは.pageSheetを選択します。
モーダル表示のための基本的なUIKitメソッドはpresent(_:animated:completion:)です。メソッドを呼び出すコントローラーがpresentingViewControllerになり、新しいコントローラーがpresentedViewControllerになります。閉じるにはdismiss(animated:completion:)を使用します。SwiftUIは同様の動作のために.sheetモディファイアを提供します。
SwiftUIの宣言的アプローチは.sheetおよび.fullScreenCoverモディファイアを使用します。前者はPage Sheetを作成し、後者は全画面モーダル表示を作成します。どちらも、モーダルウィンドウの表示を制御するBoolまたは識別可能なオブジェクトへのバインディングを受け入れます。バインディングがfalseに設定されるか、環境からdismissが呼び出されると閉じます。
struct ContentView: View {
@State private var showModal = false
var body: some View {
Button("フォームを開く") {
showModal = true
}
.sheet(isPresented: $showModal) {
RegistrationForm()
}
}
}
struct RegistrationForm: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("保存") { dismiss() }
}
}
AndroidにはiOSのようなモーダル表示のための単一APIはありません。代わりに、プラットフォームはいくつかのメカニズムを提供しています:ダイアログウィンドウ用のDialogFragment、下部パネル用のBottomSheetDialogFragment、モーダル画面用のNEW_TASKおよびCLEAR_TOPフラグ付きActivity。Jetpack Composeはすべてのタイプのモーダルウィンドウに対して統一されたDialogコンポーネントを導入しました。
DialogFragmentはAndroid SDKのモーダルウィンドウの基本クラスです。ダイアログのライフサイクルを管理し、画面回転を処理し、状態を保存します。フラグメントはナビゲーションスタックをブロックせずにActivityの上に表示されます。閉じるにはdismiss()を使用するか、setCancelable(true)の場合はダイアログ領域外をタップします。
BottomSheetDialogFragmentは、下部から上昇するパネルとしてモーダルウィンドウを表示します。このパターンはMaterial Designでオプション選択、共有、クイックアクションのために人気があります。BottomSheetは固定高さまたは拡張可能(ピーク高さ+全高さ)にすることができます。Composeでは、Material3ライブラリのModalBottomSheetが使用されます。
@Composable
fun ModalScreen(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier.padding(16.dp)
) {
Column {
Text("モーダルフォーム", style = MaterialTheme.typography.headlineSmall)
Button(onClick = onDismiss) {
Text("閉じる")
}
}
}
}
}
モーダルウィンドウは強力なUXツールですが、過剰に使用するとユーザー体験が低下します。Apple HIGとGoogle Material Designは推奨事項で一致しています:モーダル性は集中タスクに使用し、全ナビゲーションアクションの20%を超えないようにする必要があります。
モーダルウィンドウは以下のシナリオに適しています:データ入力(登録フォーム、プロフィール)、確認(削除、送信)、選択(日付ピッカー、ファイルマネージャー)、認証(OAuth、Firebase Auth)。タスクが30秒未満でコンテキストブロックが必要な場合は、モーダル性を選択してください。
モーダル表示を次の用途に使用しないでください:順次コンテンツ表示(Pushを使用)、エラー表示(ToastまたはSnackbarを使用)、ユーザーの明示的な要求なしの広告やプロモーション。 Material Designはネストされたモーダルウィンドウを避けることを推奨しています。これはユーザーの混乱を招き、ナビゲーション階層を乱すためです。
テキストフィールドのあるモーダルウィンドウの場合は、キーボードフォーカス損失を必ず処理してください。キーボードが表示されたとき、モーダルウィンドウは上に移動して、ユーザーが入力中のテキストを確認できるようにする必要があります。iOSのUIKeyboardWillShowNotificationとAndroidのadjustResizeがこのタスクを解決します。
両方のプラットフォームでのモーダル表示の実装を見てみましょう。Swiftの例は、閉じる処理のためのデリゲート付きUIModalPresentationStyle.pageSheetの設定を示しています。Kotlinの例は、カスタムレイアウトと状態保存を備えたDialogFragmentを示しています。
let modalVC = ModalViewController()
modalVC.modalPresentationStyle = .pageSheet
if let sheet = modalVC.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.prefersGrabberVisible = true
}
modalVC.presentationController?.delegate = self
present(modalVC, animated: true)
// MARK: - UIAdaptivePresentationControllerDelegate
extension ViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
print("モーダルが閉じられました")
}
}
class ModalDialogFragment : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_modal, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.closeButton).setOnClickListener {
dismiss()
}
}
}
よくある質問
続行する前にユーザーが完了する必要がある集中タスク(フォーム、確認、認証)にはモーダル表示を使用してください。Pushは、ユーザーが自由に戻れる順次コンテンツ表示に適しています。モーダルウィンドウには内部にナビゲーションを含めるべきではありません。
iOS 13以降、デフォルトの.automaticスタイルはiPhoneで.pageSheetを選択します。.pageSheetはほとんどのシナリオ(フォーム、詳細)に適しています。.fullScreenはメディアコンテンツ(ビデオ、フォトエディター)用です。.formSheetは中央ウィンドウが必要なiPadアプリ用です。
Jetpack Composeは、シンプルなモーダルウィンドウ用のDialogコンポーネントと、下部パネル用のModalBottomSheetを提供します。DialogはonDismissRequestとComposeスタイルのコンテンツを受け入れます。状態保存にはダイアログ内でrememberSaveableを使用してください。
Apple HIGおよびMaterial Designはネストされたモーダルウィンドウを推奨していません。ユーザーが別のモーダルウィンドウの上にモーダルウィンドウを開くと、コンテキストを失い、階層について混乱する可能性があります。ネストする代わりに、単一のモーダルウィンドウでステップインジケーターまたはウィザードパターンを使用してください。
iOSではUIAdaptivePresentationControllerDelegate(presentationControllerShouldDismissメソッド)、AndroidではOnBackPressedDispatcherを使用してください。未保存の変更がある場合は、オプション(保存、変更を破棄、画面に留まる)を含むAlertDialogを表示します。これにより、ユーザーによる偶発的なデータ損失を防ぎます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。