【iOS13】.pageSheetなmodalのdismiss時のライフサイクル・制御について

iOS13~は、デフォルトでハーフモーダル的な挙動(`modalPresentationStyle = .pageSheet`)が追加されており、スワイプで閉じれるようになった。
それ用にデリゲートメソッドも複数用意されており、用途に応じて使い分けることで、誤操作の防止やモーダル表示元のViewControllerへイベントの伝搬ができる。

**追加されたUIAdaptivePresentationControllerDelegateのインスタンスメソッド**

* presentationControllerDidAttemptToDismiss(_:)
* presentationControllerShouldDismiss(UIPresentationController) -> Bool
* presentationControllerWillDismiss(UIPresentationController)
* presentationControllerDidDismiss(UIPresentationController)

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/d516fa80-d0cf-ad03-c972-4e16a0408a5e.png">

## .pageSheetなmodalをスワイプで閉じる時のライフサイクル
デリゲートメソッドを使う前に、そのデリゲートメソッドがいつ呼ばれるのか、dismiss周りのライフサイクルについて理解しておく必要がある。
WWDC19のスライドのこの図が一番分かりやすい

![iOS13_modal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/aa2d1107-c644-5eb1-1115-e918810e6c8b.png)

* 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つ
前提として、
**:warning: .pageSheetなmodalがdismissしたときは、表示元のVCのviewWillAppearが呼ばれない**ので、スワイプしてモーダルを閉じたときに、表示元VCにイベントなり値を渡す必要がある場合、上述したデリゲートメソッドを使って制御する必要がある。

(WWDC19のスライドより)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/23b2ca58-7958-ca21-c236-b058c68edcf1.png)

実装の仕方は大きく2通りに分かれる
1. .pageSheetなmodalのdismissの挙動を制御しない場合
2. .pageSheetなmodalのdismissの挙動を制御する場合(確認ダイアログを出すなど)

### 1.1 .pageSheetなmodalのdismissの挙動を制御しない場合
`.isModalPresentation` を指定しない。
ShouldDismiss -> WillDismiss -> DidDismiss(iOS13~)が呼ばれるので、WillDismiss or DidDismissのタイミングで親のViewControllerにデリゲートやクロージャでイベントを渡す

``` swift
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ほどテンポが早い

``` swift
isModalInPresentation = true

func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
    // dismissを実行
    dismiss(animated: true)
    // 表示元のVCでDelegateに準拠してtableViewをreloadしたり、値渡しする
    self.delegate?.editViewControllerDidFinish(self)
}
```

### 2 .pageSheetなmodalのdismissの挙動を制御する場合(Appleのサンプル抜粋)

[Appleのサンプル](https://developer.apple.com/documentation/uikit/view_controllers/disabling_pulling_down_a_sheet)

`.isModalPresentation` を使う。
`presentationControllerDidAttemptToDismiss(_:)` デリゲートメソッドを使う。

サンプルの挙動

1. モーダル内でテキストを編集していたときに、モーダルを下スワイプすると編集破棄の確認ダイアログを出す。
2. 確認ダイアログ内でOKをタップすると `dismiss(animated: true)` を実行する
  * 表示元VCへの値渡し、イベントの伝搬もこのタイミングで行う

```swift
// 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