iOSアプリでそろそろセミモーダルビューを使ってみたい人に「FloatingPanel」をおすすめする

## 前段
iOSのマップアプリやミュージックアプリなどで見られる **セミモーダルビュー**。

iPhone の大画面化により、片手操作だと画面上部の入力をスムーズに行えないという機会が多くなりました。
`iPhone X` や `iPhone XS` のホームバーのように、画面下部に動線を配置して操作が行えるセミモーダルを使ったUIも普及していくのではないかと思います。

|    セミモーダルビューの例（iOSのマップアプリより）    |
|:------------------:|
|        ![test.gif](https://qiita-image-store.s3.amazonaws.com/0/72194/0c5ffc61-491c-4c45-dc5b-d770bef2b273.gif)        |

しかしながら現在（2018年11月時点）ではセミモーダルビューを手軽に実現できるコンポーネントはUIKitでは提供されておらず、実現するためにはUIScrollViewなどをカスタムして自前で実装する必要があります。

プロダクト開発でセミモーダルビューを使ってみたいなーと思った際に見つけた、手軽にセミモーダルビューを作ることができる [「FloatingPanel」](https://github.com/SCENEE/FloatingPanel) という素敵なライブラリを紹介します。

## FloatingPanel を使ってセミモーダルビューを作る
### 1. 導入

`CocoaPods` と `Carthage` に対応しているので適宜インストールします。

```
// Podfile

pod 'FloatingPanel'
```

```
// Cartfile

github "scenee/FloatingPanel"
```

https://github.com/SCENEE/FloatingPanel#installation

### 2. 基本的な使い方

#### 2.1 セミモーダルビューとなるViewControllerを定義する

セミモーダルビューとなるViewControllerは `UIViewController` を継承していればなんでもOKなのですが、見た目上のわかりやすさのため背景色だけ設定しておきます。

```swift
class SemiModalViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // わかりやすくするため背景色だけ設定
        self.view.backgroundColor = UIColor.orange
    }
}
```

#### 2.2 呼び出し元のViewControllerからFloatingPanelを使ってセミモーダルビューを表示する

```swift
import UIKit
import FloatingPanel

class ViewController: UIViewController {
    
    var floatingPanelController: FloatingPanelController!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        floatingPanelController = FloatingPanelController()

        // セミモーダルビューとなるViewControllerを生成し、contentViewControllerとしてセットする
        let semiModalViewController = SemiModalViewController()
        floatingPanelController.set(contentViewController: semiModalViewController)

        // セミモーダルビューを表示する
        floatingPanelController.addPanel(toParent: self, belowView: nil, animated: false)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        // セミモーダルビューを非表示にする
        floatingPanelController.removePanelFromParent(animated: true)
    }
}
```

ここまでの基本的な使い方で👇のようなセミモーダルビューができます。簡単！

|    セミモーダルビュー（基本バージョン）    |
|:------------------:|
|        ![basic_semi_modal.gif](https://qiita-image-store.s3.amazonaws.com/0/72194/df78bdb2-ada3-5d4b-3070-93eae0368b37.gif)        |


### 3. セミモーダルビューをカスタマイズする

さらに細かくカスタマイズをすることができます。

#### セミモーダルビューを角丸にする

```swift
floatingPanelController = FloatingPanelController()
floatingPanelController.surfaceView.cornerRadius = 24.0
```

#### セミモーダルビューの高さ位置をカスタマイズする

`FloatingPanelController` のDelegateを設定し、 `FloatingPanelLayout` を継承してカスタマイズしたレイアウトを返すようにします。

```swift
// ViewController.Swift

    override func viewDidLoad() {
        super.viewDidLoad()
        
        floatingPanelController = FloatingPanelController()

        // Delegateを設定
        floatingPanelController.delegate = self

        ...
    }

    // FloatingPanelControllerDelegate を実装してカスタマイズしたレイアウトを返す
    extension ViewController: FloatingPanelControllerDelegate {
        func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
            return CustomFloatingPanelLayout()
        }
    }


// CustomFloatingPanelLayout.Swift

class CustomFloatingPanelLayout: FloatingPanelLayout {
    
    // セミモーダルビューの初期位置
    var initialPosition: FloatingPanelPosition {
        return .half
    }
    
    var topInteractionBuffer: CGFloat { return 0.0 }
    var bottomInteractionBuffer: CGFloat { return 0.0 }
    
    // セミモーダルビューの各表示パターンの高さを決定するためのInset
    func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        switch position {
        case .full: return 56.0
        case .half: return 262.0
        case .tip: return 100.0
        }
    }
    
    // セミモーダルビューの背景Viewの透明度
    func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
        return 0.0
    }
}

```

#### セミモーダルビューを各パターン（full/half/tip）の高さに変更する

```swift

floatingPanelController.move(to: .full, animated: true)
```

#### セミモーダルビューの高さ変更後に処理を実行する

レイアウトのカスタマイズと同様に `FloatingPanelController` のDelegateを設定し、 ドラッグ操作完了時のイベントなどを取得して処理を実行することができます。

```swift
// ViewController.Swift

    override func viewDidLoad() {
        super.viewDidLoad()
        
        floatingPanelController = FloatingPanelController()

        // Delegateを設定
        floatingPanelController.delegate = self

        ...
    }

    extension ViewController: FloatingPanelControllerDelegate {
    
        func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {

            // セミモーダルビューの各表示パターンの高さに応じて処理を実行する
            switch targetPosition {
            case .tip:
                print("tip")
            case .half:
                print("half")
            case .full:
                print("full")
            }
        }
    }
```

#### カスタマイズしたセミモーダルビューのサンプル

以下のような機能を追加してみました。

* tipの位置にドラッグされたときにセミモーダルビューを閉じる
* キーボードの表示/非表示に合わせてセミモーダルビューの高さを変更する
* 入力が完了したらセミモーダルをクローズして入力された値を呼び出し元に返す

|    セミモーダルビュー（カスタムバージョン）    |
|:------------------:|
|        ![custom_semi_modal.gif](https://qiita-image-store.s3.amazonaws.com/0/72194/15d66505-fea8-0926-6b3c-c8300cb05edb.gif)        |

## まとめ

[「FloatingPanel」](https://github.com/SCENEE/FloatingPanel) を使うと非常に手軽にセミモーダルビューを追加することができました。

iOSの標準機能として実現されたら、、、と二の足を踏んでいる方も、検証だけでも使ってみる価値があると思います。

また使ってみて気づいたのは、**通常の全画面モーダルでは、モーダルで表示している以外の操作をブロックするが、セミモーダルではユーザの操作はブロックされない** という違いがあるということです。

実際のプロダクトのUIに取り込む際、全画面モーダルで表示している画面を単純にセミモーダルに置き換えるといろいろ破綻してしまう可能性があります。[「FloatingPanel」](https://github.com/SCENEE/FloatingPanel) を使って実際に試してみることをオススメします！