iOS13からのUIModalPresentationStyle.pageSheetでユーザにプルダウンで閉じられないようにする方法

# 概要

iOS13からのページシート（ハーフモーダルちっくな標準のモーダル）で、ユーザにプルダウンで閉じられたくない場合にどのようにするかというのを書いておきます。

手順は

- iOS13からの`UIViewController`のフラグ`var isModalInPresentation: Bool`をtrueにすると閉じられないようにできる
- `var viewWillLayoutSubviews: Bool`がtrueの際に`UIAdaptivePresentationControllerDelegate`のメソッドでユーザが閉じようとした際に自前のアラートを表示するようにできる

<img width="375" alt="IMG_0911.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/7001d8b6-e43f-1196-5c1b-9517d62d39f2.png">

これについてはAppleのサンプルコードが下記にあります

https://developer.apple.com/documentation/uikit/view_controllers/disabling_pulling_down_a_sheet

このコードはWWDC19のセッション「Modernizing Your UI for iOS 13」で解説されていました

https://developer.apple.com/videos/play/wwdc2019/224


# 詳しく

最小限のコードで

```swift
class EditViewController: UIViewController, UIAdaptivePresentationControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // フラグをtureにするとpull down時に下記delegateが動作する
        isModalInPresentation = true
        presentationController?.delegate = self
    }

    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        
        // UIAlertControllerで閉じてよいか自前で表示する
        ...
    }
}  
```


# その他

やりたいことはそこで終わりですが、気になったことがあるので書いておきます

## サンプルコードにある編集中を示すやり方

サンプルコードでは`UITextView`が変更されたかどうか、つまり`isModalInPresentation: Bool`をtrueにしたりfalseにするタイミングを`UIViewController`の`viewWillLayoutSubviews()`でやっています。

これはもし複数`UITextView`がある場合に無駄がないようにしているのだと思います。


```swift
    override func viewWillLayoutSubviews() {

        // Ensure textView.text is up to date
        textView.text = editedText

        // If our model has unsaved changes, prevent pull to dismiss and enable the save button
        let hasChanges = self.hasChanges
        isModalInPresentation = hasChanges
        saveButton.isEnabled = hasChanges
    }
```

これ、テキストが変更されたら`editedText`プロパティを書き換えてそこの`didSet`でsetNeedsLayoutさせてViewのレイアウトを更新するタイミングを要求しています。それが次。

```swift
    var editedText = "" {
        didSet {
            viewIfLoaded?.setNeedsLayout()
        }
    }
```

こうすることで流れとして次のようにテキストの値を変えてフラグを変更しています。

- ユーザがUITextViewでテキストを変更
  - UITextViewDelegateでプロパティ`editedText`を書き換え
    - `editedText`が書き換わると`viewWillLayoutSubviews()`が動作
      - （`textView.text = editedText`でUITextViewを上書き）
      - `isModalInPresentation`をtrueかfalseに

気付きとしては`viewIfLoaded?.setNeedsLayout()`なんて短い時間に何回読んでもそれらがまとめて実行されるので、もし複数`UITextView`がある場合もフラグ`isModalInPresentation`の変更が無駄に書き換わらないのでしょう。`hasChanges`フラグは`UITextView`ごとに必要になるでしょうが、その結果のフラグはまとめて切り替えているというやり方な気がします。

- `hasChanges`: ガンガン変わっていい
- `isModalInPresentation`: ガンガン変わらないように`viewWillLayoutSubviews`のタイミングで書き換えられている

## presentationControllerShouldDismiss

サンプルコードにはありませんが、

「Modernizing Your UI for iOS 13」では`var viewWillLayoutSubviews: Bool`がfalseでも、デリゲート`optional func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool
`でfalseを返せば閉じるのをブロックできる、というような説明をしています。

https://developer.apple.com/documentation/uikit/uiadaptivepresentationcontrollerdelegate/3229890-presentationcontrollershoulddism?language=objc

<img width="1900" alt="スクリーンショット 2019-10-11 11.03.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/302dea72-8638-6a7e-08eb-0f8f0b28569d.png">


つまり、編集中ではないので`var isModalInPresentation: Bool`をfalseにするとしても、アニメーション中などは`optional func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool`でfalseにしておくことで、プルダウンさせてもアラートを表示させられるよ、ということでしょう...。