遊びから始めるハーフモーダル

こんにちは:christmas_tree:

[ZOZOテクノロジーズ #5 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/zozo) 21日目の記事を担当します:snowflake: 
今年もスノーボードの季節が来て、大はしゃぎの@koiwai2020です:snowboarder: 

よろしくお願いします:bow: 



#今回は
大好きなスノーボードシーズンに使えそうな話題が、、、作れませんでした。
なので、個人的にWWDC以降気になっていたけどあまり触れることがなかった、[Customize and resize sheets in UIKit](https://developer.apple.com/videos/play/wwdc2021/10063/)を復習します。
その中で今回は、WEARの画像によるアイテム検索を題材に挑戦してみて、主に躓いた点をまとめてみました。

#画像によるアイテム検索について
題材にした機能は、コーディネート画面の画像上にある「検索アイコン」にて使えますので、試してみてください:bow: 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368129/61441ef3-3b85-81b3-c758-957bec43c69c.jpeg" width=30%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368129/7c01b333-a934-06cb-d350-32f19a9bdc7b.jpeg" width=30%>


#動作イメージ
今回実際に、サンプルコードを元にこんな画面を作ってみました。
(本当は雪山での映え写真を使いたかったのですが、素材がなかったので諦めました。)
<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/14e7H0xxKU">pic.twitter.com/14e7H0xxKU</a></p>&mdash; 小岩井 (@WfODXAd0jmop1Ev) <a href="https://twitter.com/WfODXAd0jmop1Ev/status/1472828983968698370?ref_src=twsrc%5Etfw">December 20, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#実装について
まず取り組むにあたって、次の画面を作っています。
##使用するViewController
今回は、検索したいアイテムを選択する画面と結果を表示する画面の2つを作って実装してみました。
###①検索したいアイテムが選択できる画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368129/629004ca-0349-6370-a3dd-200a4b3653cf.png" width=25%>
###②検索結果を表示する画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368129/15d267ea-5f83-bf63-fa55-52b87109e163.png" width=25%>

##検索結果を表示する画面をモーダルで表示させる設定
今回の挙動は、以下の設定でほとんど終わりです。

```swift
func showItemsModal() {
    itemsVC.modalPresentationStyle = .popover
    itemsVC.isModalInPresentation = true // a
    if let popover = itemsVC.popoverPresentationController {
        let sheetPresentationController = popover.adaptiveSheetPresentationController
        sheetPresentationController.detents = [.medium(), .large()] // b
        sheetPresentationController.largestUndimmedDetentIdentifier = .medium // c
    }
}
```

###a) isModalInPresentationの設定[^1]
今回は、常にアイテム検索結果を表示しておきたいので、こちらを設定しています。

> 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.


`true`に設定することで、スワイプによってモーダルが閉じてしまうのを阻止するために設定しています。

> The default value of this property is false. 

デフォルトは`false`ですので、必要がない場合の設定は不要になります。


###b) detentの設定[^2]
ここでは、ハーフモーダルの状態が使えるように設定します。

> The default value is an array that contains the value large(). 

と説明されており、何も設定しないと拡大表示のみしか対応されません。

> This array must contain at least one element. When you set this value, specify detents in order from smallest to largest height.

の記載に従って、小さい順に並べて設定します。
(大きい順に設定すると、拡大時にバウンドするようなおかしな挙動が見られました。)

###c) largestUndimmedDetentIdentifierの設定[^3]
ここの設定は、今回のUIのようにモーダルが出ている状態で、検索したいアイテムが選択できる画面の操作を有効にしたい場合に使います。

>For example, set this property to medium to add the dimming view at the large detent.
Without a dimming view, the undimmed area around the sheet responds to user interaction, allowing for a nonmodal experience. You can use this behavior for sheets with interactive content underneath them.

説明より、今回のように `medium`を設定することでシート(ハーフモーダル)下の領域に対する、ユーザーの操作を検知することが可能になります。
(設定しないとタップしても何も反応せず、ちょっと躓いてしまいました。)

###デフォルトでハーフモーダルを表示させる
あとは、表示させるだけです。

```swift
override func viewDidLoad() {
    super.viewDidLoad()

    showItemsModal()
    present(itemsVC, animated: true, completion: nil)
}
```

表示は設定後に`present(_:animated:completion:)`を呼ぶだけです。
今回は、モーダルが出た状態で「検索したいアイテムが選択できる画面」を表示させたかったので、`viewDidLoad()`で呼んでいます。

##検索結果を表示する画面を拡大/縮小させる設定
###アイテム一覧スクロールで拡大
検索結果の一覧のスクロールに合わせて、モーダルを操作させます。
今回は簡易的に、scrollView.contentOffsetが垂直方向に少しでもスクロールされたら、拡大させています。

```swift
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if scrollView.contentOffset.y > 0 {
        if let sheetPresentationController = popoverPresentationController?.adaptiveSheetPresentationController {
            sheetPresentationController.animateChanges {
                sheetPresentationController.selectedDetentIdentifier = .large
            }
        }
    }
}
```

###モーダルのヘッダー部分のタップで拡大/縮小の切り替え
今回は、UIButtonを用意して試してみました。

```swift
@IBAction func tapHeader(_ sender: Any) {
    guard let sheetPresentationController = popoverPresentationController?.adaptiveSheetPresentationController else {
        return
    }

    if sheetPresentationController.selectedDetentIdentifier == .large {
        sheetPresentationController.animateChanges {
            sheetPresentationController.selectedDetentIdentifier = .medium
        }
    } else {
        sheetPresentationController.animateChanges {
            sheetPresentationController.selectedDetentIdentifier = .large
        }
    }
}
```

####selectedDetentIdentifierの設定[^4]
> The default value is nil, which means the sheet displays at the smallest detent you specify in detents.

デフォルトはnilなので、`large`状態かどうかを判定基準において、切り替えています。




#実際に試してみて
以前は、[CollectionViewに複数のジェスチャーを認識させてマップアプリのような動きのUIを作った](https://qiita.com/koiwai2020/items/56a2368f77afa2f5a522)のように

- UIPanGestureRecognizer
- UIView.transform
- UIView.animate
- UIGestureRecognizerDelegate

と機能を組み合わせいました。
機能の状態を監視しながらでないと実現できなかったものを、大分簡単にできるようになったという印象です。

一方で、モーダルの高さの指定のような細かい調整の方法が見つけることができませんでした。
なので、実現したい画面の仕様に応じて、使っていけたら良いなと思いました。

[^1]: https://developer.apple.com/documentation/uikit/uiviewcontroller/3229894-ismodalinpresentation
[^2]: https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801903-detents
[^3]: https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3858107-largestundimmeddetentidentifier
[^4]: https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801908-selecteddetentidentifier