iOS13~は、デフォルトでハーフモーダル的な挙動(modalPresentationStyle = .pageSheet
)が追加されており、スワイプで閉じれるようになった。
それ用にデリゲートメソッドも複数用意されており、用途に応じて使い分けることで、誤操作の防止やモーダル表示元のViewControllerへイベントの伝搬ができる。
追加されたUIAdaptivePresentationControllerDelegateのインスタンスメソッド
- presentationControllerDidAttemptToDismiss(_:)
- presentationControllerShouldDismiss(UIPresentationController) -> Bool
- presentationControllerWillDismiss(UIPresentationController)
- presentationControllerDidDismiss(UIPresentationController)
.pageSheetなmodalをスワイプで閉じる時のライフサイクル
デリゲートメソッドを使う前に、そのデリゲートメソッドがいつ呼ばれるのか、dismiss周りのライフサイクルについて理解しておく必要がある。
WWDC19のスライドのこの図が一番分かりやすい
- isModalPresentationがtrueなら
DidAttemptToDismiss
が呼ばれる - isModalPresentationがfalse(および設定していない) 場合、
- ShouldDismiss -> WillDismiss -> DidDismissが呼ばれる
補足(isModalPresentationについて)
iOS13~用意された、UIViewControllerのプロパティ。
trueの場合、インタラクティブにmodalyなViewControllerがdismissされないようになる。
↓
要は下スワイプしてもモーダルが閉じなくなる。
その下スワイプ時のイベントをキャッチして dismiss(animated: true)
を実行したり、ダイアログを出したいときに使えるデリゲートメソッドが DidAttemptToDismiss
isModalPresentationのドキュメント
The default value of this property is false. When you set it to true, UIKit ignores events outside the view controller's bounds and prevents the interactive dismissal of the view controller while it is onscreen.
.pageSheetなmodalの下スワイプ時の制御パターンは2つ
前提として、
.pageSheetなmodalがdismissしたときは、表示元のVCのviewWillAppearが呼ばれないので、スワイプしてモーダルを閉じたときに、表示元VCにイベントなり値を渡す必要がある場合、上述したデリゲートメソッドを使って制御する必要がある。
実装の仕方は大きく2通りに分かれる
- .pageSheetなmodalのdismissの挙動を制御しない場合
- .pageSheetなmodalのdismissの挙動を制御する場合(確認ダイアログを出すなど)
1.1 .pageSheetなmodalのdismissの挙動を制御しない場合
.isModalPresentation
を指定しない。
ShouldDismiss -> WillDismiss -> DidDismiss(iOS13~)が呼ばれるので、WillDismiss or DidDismissのタイミングで親のViewControllerにデリゲートやクロージャでイベントを渡す
protocol EditViewControllerDelegate: class {
func editViewControllerDidCancel(_ editViewController: EditViewController)
func editViewControllerDidFinish(_ editViewController: EditViewController)
}
extension EditViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// 表示元のVCでDelegateに準拠してtableViewをreloadしたり、値渡しする
self.delegate?.editViewControllerDidFinish(self)
}
}
(追記) 1.2 .pageSheetなmodalのdismissの挙動を制御しない場合
上記の例でDidDismissを使用した場合、完全にモーダルが閉じ切ってからデリゲートメソッドが呼ばれるため、後続の処理によってはテンポが遅い場合がある。
その場合は、 DidAttemptToDismiss(_:)
内で dismiss
を実行し親のViewControllerにデリゲートやクロージャでイベントを渡した方が体感0.5sほどテンポが早い
isModalInPresentation = true
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
// dismissを実行
dismiss(animated: true)
// 表示元のVCでDelegateに準拠してtableViewをreloadしたり、値渡しする
self.delegate?.editViewControllerDidFinish(self)
}
2 .pageSheetなmodalのdismissの挙動を制御する場合(Appleのサンプル抜粋)
.isModalPresentation
を使う。
presentationControllerDidAttemptToDismiss(_:)
デリゲートメソッドを使う。
サンプルの挙動
- モーダル内でテキストを編集していたときに、モーダルを下スワイプすると編集破棄の確認ダイアログを出す。
- 確認ダイアログ内でOKをタップすると
dismiss(animated: true)
を実行する
- 表示元VCへの値渡し、イベントの伝搬もこのタイミングで行う
// 1: オリジナルのテキストと編集中のテキストの差異をBool値で返す
var hasChanges: Bool {
return originalText != editedText
}
override func viewWillLayoutSubviews() {
textView.text = editedText
// 2: isModalInPresentationに1: のプロパティを代入
let hasChanges = self.hasChanges
isModalInPresentation = hasChanges
saveButton.isEnabled = hasChanges
}
// 3: isModalInPresentationがtrueの場合、DidAttemptToDismissが呼ばれる
// メソッド内で確認ダイアログを出す
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
// The user pulled down with unsaved changes
// Clarify the user's intent by asking whether they intended to cancel or save
confirmCancel(showingSave: true)
}
まとめ
新モーダルのdismissの挙動については、下記を抑えておく必要がある
- isModalPresentationで大きくライフサイクルが変わること
- dismissしても表示元VCのviewWillAppearが呼ばれないこと
参考URL
WWDC19 - Modernizing Your UI for iOS 13
https://developer.apple.com/videos/play/wwdc2019/224
公式サンプル
https://developer.apple.com/documentation/uikit/view_controllers/disabling_pulling_down_a_sheet