UISheetPresentationController を使ってみた


# はじめに

WWDC2021 で発表のあったハーフモーダルを実装することができる [UISheetPresentationController](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller?changes=_2_1) を使ってみたので、
使用方法と使ってみた感想などを綴っておきたいと思います。

これまでは [PanModal](https://github.com/slackhq/PanModal) や [FloatingPanel](https://github.com/scenee/FloatingPanel) 等のライブラリを使って実装していましたが、
公式でもサポートされたようなので使い勝手を調べてみました。

## 環境

Xcode: 13.0 beta 4 (13A5201i)
Swift: Swift5
iOS: iOS15(public beta 4)

# 使用方法

以下のようにするだけで簡単にモーダル画面を実装することができます。

```swift:ViewController
final class ViewController: UIViewController {
    private lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("open", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .black
        button.layer.cornerRadius = 8.0
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.heightAnchor.constraint(equalToConstant: 30.0),
            button.widthAnchor.constraint(equalToConstant: 60.0)
        ])
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(button)
        NSLayoutConstraint.activate([
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])

        button.addTarget(self, action: #selector(tapButton), for: .touchUpInside)
    }

    @objc
    private func tapButton() {
        let vc = ModalViewController(nibName: nil, bundle: nil)
        /* ここが実装のキモ */
        if let sheet = vc.sheetPresentationController {
            // ここで指定したサイズで表示される
            sheet.detents = [.medium(), .large()]
        }
        present(vc, animated: true, completion: nil)
    }
}
```


```swift:ModalViewController
/// UISheetPresentationController に載せたい ViewController
final class ModalViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
    }
}
```

上記のように記述するだけで簡単に使用することができます。
![sample1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/665c9d38-a135-edd6-b491-95e218e3d794.gif)


`sheet.detents` にセットする値を `.medium()` だけにすると、
以下の gif のように上まで上がらず、真ん中までのモーダル画面にすることができます。

![sample2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/91209235-2471-ab3c-9b2e-2f8e64869443.gif)

## 初期表示位置の変更

`sheet.detents = [.medium(), large()]` と設定した場合、初期表示位置は `.medium()` になります。

以下のように `sheet.selectedDetentIdentifier` を指定する、
もしくは `sheet.detents = [.large(), .medium()]` のように並び順を変更することで初期表示位置を変更することができます。

```swift:ViewController
let vc = ModalViewController(nibName: nil, bundle: nil)
if let sheet = vc.sheetPresentationController {
    sheet.detents = [.medium(), .large()]
    sheet.selectedDetentIdentifier = UISheetPresentationController.Detent.Identifier.large
}
present(vc, animated: true, completion: nil)
```

## 高さの変更

高さの指定は `UISheetPresentationController.detents` で指定可能ですが、
現状だと `.medium()` と `.large()` しか指定することができず、自由に高さを変更することはできないようです。

自由に高さを変更することはできませんが、以下のスクリーンショットのように iPad と iPhone で最適な UI になるように自動で調節してくれるようなので、この辺りはとても便利かなと思います。

| .medium() | .large() |
|:-:|:--:|
|![スクリーンショット 2021-08-09 16.43.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/c5a25329-0fbb-320e-20ac-e13bada5f2f7.png)|![スクリーンショット 2021-08-09 16.43.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/2b8fbace-9389-8ec9-af3f-4abaef04762a.png)|


## ハンドルの表示方法

ハーフモーダルを表示した際に画面の上部に灰色のバーが表示されていることを見かけると思います。
`UISheetPresentationController` では `prefersGrabberVisible` というプロパティを `true`　にすることで表示することができます。
（デフォルトは `false` のようです）

```swift
sheet.prefersScrollingExpandsWhenScrolledToEdge = true // ハンドルを表示
```

| prefersGrabberVisible = false (default) | prefersGrabberVisible = true |
|:-:|:-:|
|![Simulator Screen Shot - iPhone 12 - 2021-08-10 at 22.15.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/ada5b1fc-0b9e-2c45-2de7-0bfe05798351.png)|![Simulator Screen Shot - iPhone 12 - 2021-08-10 at 22.13.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/808b7a52-2dee-fdd9-0afc-7ab23a064dbf.png)|

## UIScrollView を載せた際の挙動

半モーダルに `UIScrollView` や `UITableView`、`UICollectionView` を載せたいといった状況は多いかと思います。
そういった場合にどのような挙動をするのか、スムーズに動いてくれるのか確認してみたいと思います。

今回は UITableView を載せてみた際の挙動を確認してみたいと思います。

| .medium() & .large() の場合| .medium() のみの場合|
|:-:|:-:|
|![sample4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/fd0589ce-2186-f0f8-f785-0a76e0ec19c1.gif)| ![sample5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/4d449f33-0e25-06a9-9ad7-16168071eeaa.gif)

`.medium()` → `.large()` への流れや `.medium()` → dismiss の流れなど、とてもスムーズに動いてくれました。

また、`UIScrollView` の上部にタイトルのような View を追加したいという要件も多いと思いますが、
そういった場合でも問題なくスムーズに動作してくれました。



![sample6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/04db30a2-7069-5fe6-4af6-bfa29429596e.gif)


内部のコンテンツを上方向にスクロールした場合に `.large()` のサイズまで拡張させたくない場合もあるかと思います。
そういった要件を満たすために `prefersScrollingExpandsWhenScrolledToEdge` といったプロパティが用意されていました。
このプロパティを `false` にすることで、内部のコンテンツをスクロールしても `.large()` のサイズにはならず、
ユーザが意図的にハーフモーダルのサイズを変更した場合のみ `.large()` のサイズへ拡張できるといった実装ができるようです。

```swift
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
```

![sample7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53717/78ed3076-63f1-0548-f8d5-cb7773d05d3d.gif)


## モーダルアニメーションの速度の変更

モーダルの表示までのアニメーションやアニメーション速度を変更したい場合もあるかと思います。
現状 `UISheetPresentationController` にはそういった変更ができそうなプロパティ等は定義されていなさそうなので、
今後のアップデートに期待しましょう。

# 終わりに

iOS15 以降からでしか使用できないかつ、まだまだ細かいところは調節ができないといったネックはありますが、
今までライブラリを導入して実装しないと面倒だったハーフモーダルの実装が簡単にできるところ、
iPad での表示の最適化、Landscape への対応なども踏まえると取り入れてみるのはアリなんじゃないかなと思いました。

今後より詳細な設定ができるようなアップデートに期待しています。