公式「ハーフモーダル」がやってきた！ #wwdc21

iOS 15からハーフモーダルっぽい表示が標準APIを使ってできるようになりました。この記事ではその「ハーフモーダル」について紹介している[Customize and resize sheets in UIKit](https://developer.apple.com/videos/play/wwdc2021/10063/)という動画を紹介してみます。

# 概要

## 従来の場合

今までモーダルは全画面表示でした（スクショは`.pageSheet`の場合）。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/45dc8998-b19c-9360-db93-408286049ed0.png" width="280px">

## iOS 15 〜

iOS 15では「medium」モードにすると画面の半分だけを覆うシートが作成できるようになりました。

![スクリーンショット 2021-06-28 18.55.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/e2668cd4-7ebe-ae82-c13d-548ab1e0ef0e.png)

landscapeやiPadのときの表示はこのようになります。

![スクリーンショット 2021-06-28 9.27.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/cef444ea-1efb-f24a-2382-d34a1494d121.png)
![スクリーンショット 2021-06-28 9.27.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/d19ddcfb-b11c-cacc-91cc-8d6a96704869.png)
![スクリーンショット 2021-06-28 9.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/3f078232-5fb8-e8d3-e960-75b65c966c92.png)

# detentsとは

まず最初に`detents`というものを理解する必要があります。`detents`とはシートが自然に止まる高さのことを表し、`.medium()`と`.large()`が用意されています。

![スクリーンショット 2021-06-28 11.25.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/547abe49-4ef6-f0c6-f926-6f1de68107ea.png)
![スクリーンショット 2021-06-28 11.25.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/00d1b22f-843f-f066-54c8-756ff25e0e98.png)

コードで書くとこんなかんじです。この指定により、シートの覆い具合を調整することができます。まず`viewController`の`sheetPresentationController`を取得します。このシートに対し`detents`を設定してあげると、サイズの指定ができます。デフォルトでは`.large()`のみが設定された状態だと解釈されます。設定とその結果の対応は以下の通りです。

* `.large()`のみ
  * フルサイズのシートが表示される（リサイズ不可）
* `.medium()`と`.large()`の両方
  * mediumとlargeでリサイズ可能なシートが表示される
* `.medium()`のみ
  * ハーフサイズのシートが表示される（リサイズ不可）

![スクリーンショット 2021-06-28 11.37.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/4465d914-5c80-855f-895f-190c8ed7846c.png)

# detentsを用いた実装例

## 従来の場合

例えば写真ピッカーは次のような実装をして表示していたと思います。
![スクリーンショット 2021-06-29 7.51.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/c597ee65-a268-be83-f47d-dd3af483e494.png)
![スクリーンショット 2021-06-28 11.50.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/fad9d31f-c51a-4357-4f0a-13ac52841601.png)

## detentsでサイズ指定

iOS 15から、例えば以下のコード、表示ができます。

![スクリーンショット 2021-06-29 7.55.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/bd8e0bc5-8d01-9515-a59d-f7f359d4c302.png)

* `PHPickerViewController`の`sheetPresentationController`を取得して`detents`に`.medium()`と`.large()`を設定
  * リサイズ可能なシートを表示する
* ピッカーの`didFinishPicking`デリゲートからdismiss行を削除
  * 写真を選択し終わってもシートが完全に閉じないようにする

これにより、シートを半分にした状態で写真を選択することも、ドラッグしてフルサイズにしたシートから写真を選択することもできます。

![スクリーンショット 2021-06-28 12.00.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/327df2cf-993e-6823-e8a8-ef0f098d3b1b.png)

## スクロール制御

ただし、このままだと上にスクロールした時にシート自体もスクロールしてフルサイズになってしまい、「スクロールすると、シートは半分のままで、ピッカー内の写真一覧だけがスクロールされる」状態にならなくなってしまいます。バーをドラッグした時にシートのサイズが変わるようにするには`prefersScrollingExpandsWhenScrolledToEdge`を`false`にする必要があります。
![スクリーンショット 2021-06-28 13.02.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/600c465a-9d85-63ce-eb75-3e991a902202.png)

## 選択時のサイズ指定

次の改善ポイントは写真選択時のアクションです。`.large()`の状態で写真を選択しても、選択された写真が表示されるエリアは見れないので、ちゃんと写真が選択されたのかその場でわかりません。そこで、写真が選択された時はシートが`.medium()`の大きさになるよう変更してみます。デリゲートメソッド内で`selectedDetentIdentifier`を`.medium`に設定します。
![スクリーンショット 2021-06-28 13.14.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/2f383e13-879e-26ec-9ca8-a148374f615a.png)

## リサイズ時にアニメーションをつける

写真選択時に`.large()`から`.medium()`に変更できたのはいいものの、このままだとアニメーションがないのでいきなりサイズが変わってしまいます。次のように`animateChanges`ブロックで囲むと、自然なアニメーションをつけることができます。
![スクリーンショット 2021-06-28 13.21.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/c455aff0-2e40-46eb-49d6-ea08d7e849ee.png)

## dimmingを消す

![スクリーンショット 2021-06-29 8.11.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/0dfcda31-9164-66b8-dbbc-b2c7bc46ef6c.png)
選択された写真エリアが薄暗いので、dimmingを解除してみます。`smallestUndimmedDetentIdentifier`の設定をいじることで解除が可能です。このプロパティはデフォルト`nil`で、すべてのdetentsでdimmingがかかっています。dimmingを解除したい場合はdimmingさせたくない最小のdetentsを設定します。
![スクリーンショット 2021-06-28 13.32.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/ee4ffa41-c960-ddf7-97ce-dad2ea6947dc.png)

このプロパティは視覚的に「薄暗さ」を取り除くだけではなく、シート外のコンテンツにアクセスできるような「ノンモーダル状態」を作ることができます。ので、`.medium()`の時に写真選択エリアに触れて操作することもできます。

## キーボードとの併用

![スクリーンショット 2021-06-29 8.11.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/21bd22f5-549d-d327-9454-61bb05b455cd.png)
`.medium()`の時にキーボードを表示すると、自動で`.large()`サイズになります。キーボードが消えると`.medium()`サイズに戻ります。

# 視覚的なカスタマイズ

`detents`を操作する以外のところでもいろいろできます。

## landscape時のシートサイズ

今までは全画面表示にするしかありませんでした。
![スクリーンショット 2021-06-28 14.26.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/01a1fede-8e84-51b7-f6d7-22966c691d7c.png)

iOS 15からは、シートの下端だけが画面の縁についている状態で表示できる`prefersEdgeAttachedInCompactHeight`が追加されました。これを`true`にするとSafe Areaと同じ幅のシートを表示できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/d16e0758-675a-0600-6fef-0743189f3ddb.png)

`presentedViewControllers`の`preferredContentSize`に沿った幅のシートにしたい場合は、`widthFollowsPreferredContentSizeWhenEdgeAttached`を`true`に設定します。`preferredContentSize`を設定することで、幅をさらにカスタマイズすることもできます。
![スクリーンショット 2021-06-28 14.35.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/5e9bb9f5-8119-1379-e703-e0e80d73d839.png)

## grabberの表示非表示

`prefersGrabberVisible`の設定で、grabberの表示非表示を操作することができます。
![スクリーンショット 2021-06-28 15.40.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/a672e1a7-43ed-acd9-9886-a69542c27616.png)

シートのリサイズが可能であることがわかりづらい場合にはgrabberを表示すると役に立ちます。

## シートのradius変更

シートの角丸具合を調整することもできます。
![スクリーンショット 2021-06-28 17.18.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/0d555433-26d9-819c-8a5b-7c749753055a.png)

## iPadでポップオーバーを表示する場合

写真ピッカーをポップオーバーにするにはまず`modalPresentationStyle`に`.popover`を設定します。次に、先程のように`sheetPresentationController`を取得するのではなく、`popoverPresentationController`を取得します。ポップオーバーのsourceを`barButtonItem`に設定し、`adaptiveSheetPresentationController`というポップオーバーの新しいプロパティを取得します。このプロパティはポップオーバーがcompactサイズのクラスに適応するシートのインスタンスを返してくれます。
![スクリーンショット 2021-06-28 17.23.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/e86f30a2-0713-9852-128a-3b0252528358.png)

これで写真ボタンをタップするとポップオーバーに写真ピッカーが表示され、領域のサイズを小さくすると、`.medium()`サイズのシートが表示されるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/2d15fe9a-bdb8-3e4a-054f-e87330def8a5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/c91760b4-bb85-2169-abdc-20d5926326d7.png)

ただし、このままだと`.large()`状態で写真を選択した時に自動で`.medium()`サイズになってくれません。`popoverPresentationController`の`adaptiveSheetPresentationController`を取得して先程と同じように設定する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/1367591c-8bb8-e8e0-4060-ac09dc66c001.png)

# まとめ

* iOS 15からハーフモーダルっぽいUIが標準APIを使って作れるようになったよ
* ちょこちょこカスタマイズできるよ