UIコンポーネントの作り方 #potatotips @トレタ

@hayashi311です。
Potatotips #35@[トレタ](https://www.wantedly.com/projects/63993)でカスタムUIコンポーネントを作る時の基本について話してきました。

この記事は、発表と懇親会での話題をまとめたものです。

# 話したこと、話したかったこと
カスタムUIコンポーネントを作る時のアプローチ、考えていることがあって、これをまとめてみました。

発表では練習不足と詰め込みすぎて論点が曖昧になったので、内容を削っています。

その代わり、考えに至った経緯や、「こういう時はこうしよう」ということが書いてあります。「なぜそうするか」という「理屈」を書きました。

# UIControlからはじめる
![UIKeyInput.006.png](https://qiita-image-store.s3.amazonaws.com/0/1777/ac846e4c-9408-3516-2f0d-557612e9bdb7.png)

レイアウト、レンダリング、イベントハンドリング、UIコンポーネントを作る時に考えることは少なくありません。
小さくて挙動を予想しやすいUIControlを拡張するところからはじめると、うまく実装できることが多いように思います。

たいていの場合、イベントの受け渡しは`addTarget`と`sendAction`で十分です。
標準と同じインターフェースなら、コードを読まなくても使えるコンポーネントになります。

![UIKeyInput.007.png](https://qiita-image-store.s3.amazonaws.com/0/1777/b6ae46ce-3fa4-3fe0-e9d9-d4cddfc8c55c.png)
*標準のAPIに合わせると、使い方に迷わない*

- 最小限のクラス/プロトコルからはじめる
- 標準のAPIに合わせる

# UIResponder, UIViewはすべての基本

UIコンポーネントを実装して`addSubView`するということは、`UIView`の階層と、`UIResponder`チェーンにUIコンポーネントを組み込むということです。

基本的なことですが、すごく大事なことです。

階層やチェーンの中で、どうイベントが発生して、どう処理されるか。レイアウトはどう処理されて、レンダリングはどう実行されるか。

レンダリング結果だけを見て、つじつまを合わせようとするとたいていの場合破綻します。

# `hitTest`

UIViewの階層は、親Viewが子Viewの参照のリストを持つ形で表現されています(`subviews`)。

スクリーンがタップされた時、UIWindowを起点に子Viewの`hitTest`を再帰的に呼び出して､`UIView`の階層の中で どのViewがタップされたのかを特定します。

```swift
// hitTestのイメージ
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if /*pointがboundsの中だったら*/ {
        for subview in subviews {
            if let hitView = subview./*再帰的にhitTestを呼び出す*/ {
                return hitView
            }
         }
        return self
    }
    return nil
}
```

`hitTest`や内部で呼び出される`point(inside:, with:) -> Bool`をoverrideすることで、以下のような実装が可能です

- 子Viewを強制的にhitさせる/させない
- `bounds`より大きな領域をタップに反応させる

# UIResponder、firstResponder

UIResponderチェーンは、子Responderが親Responderの参照を持つ形で表現されています。(`next: UIResponder`)

hitTestでタップされたViewを特定した後、今後は逆向きに`UIResponder`のチェーンを駆け上がって、`UIEvent`に反応するべき`UIResponder`を特定します。

| UIViewの階層 | UIResponderチェーン |
|:-:|:-:|
| rootから末端へ | 末端からrootへ |

タッチイベントは`hitTest`を使って対応するViewを特定できますが、キーボードの入力や端末のシェイクなど対応するViewを特定できないイベントもあります。

`firstResponder`はこのようなグローバルなイベントを最初に受け取るUIResponderを指定する仕組みです。

- `canBecomeFirstResponder`で`true`を返すようにしておく
- 他の`UIResponder`に`firstResponder`が移った時に`resignFirstResponder`が呼ばれるので便利

**`UIView`も`UIViewController`も`UIWindow`もすべて`UIResponder`のサブクラスです。**

## 入力のUI: `UIResponder.inputView`

入力系のUIコンポーネントがfirstResonderになった時、キーボードの代わりにカスタムUIを表示する仕組みがあります。

`UIResponder`が`firstResponder`になった時、`firstResponder`からrootまでUIResponderチェーンを駆け上がって、最初に返される`inputView`&`inputAccessoryView`を表示します。

- `inputView`(キーボードの部分)
- `inputAccessoryView`(キーボード上のツールバーの部分)
- `inputView`と`inputAccessoryView`を返す`UIResponder`は別々で良い
- `UIScrollView`の`keyboardDismissMode`に対応
- `resignFirstResponder`で自動で隠れる

![UIKeyInput.012.png](https://qiita-image-store.s3.amazonaws.com/0/1777/248e458a-9ce9-cfa2-1bc9-9dd05074075a.png)
*UIResponderなのでフォーカス状態の管理をUIKitに任せることができる*

`UIResponder`は`UIView`である必要はないので、Viewの階層に含まれない`UIResponder`のサブクラスを実装してもいいんですよ。
（その場合は、自前で`next: UIResponder`を返します）

## 入力のUI: UIKeyInput

カスタムUIを表示する`inputView`に対して、UIKeyInputは標準のキーボードを使いながら、そのキーボードの入力を受け取るためのシンプルなプロトコルです。

![UIKeyInput.014.png](https://qiita-image-store.s3.amazonaws.com/0/1777/8ecec51c-e711-08b3-f8d1-5df5fc6eef65.png)
*必要だったのはUITextFieldではなく、キーボードの入力だった*

`UIKeyInput`を実装した`UIResponder`が`firstResponder`になると、キーボードが表示されます。
`UITextField`もまた`UIKeyInput`を実装した`UIResponder`ですね。

発表では[4桁のPINCodeを入力するUIコンポーネント](https://github.com/hayashi311/PINCodeField)を例に説明しました。

```swift
@IBDesignable
class PINCodeField: UIControl, UIKeyInput {

    var code: [Decimal] = []

    var hasText: Bool {
        get {
            return !code.isEmpty
        }
    }

    func insertText(_ text: String) {
        guard code.count < 4 else {
            return
        }
        guard let decimal = Decimal(string: text), decimal.isFinite else {
            return
        }
        code.append(decimal)
    }

    func deleteBackward() {
        code = Array(code.dropLast())
    }
}
```

シンプルな実装なので、挙動を予測しやすい。

# まとめ

<blockquote class="twitter-tweet" data-lang="en"><p lang="und" dir="ltr"><a href="https://t.co/tkCGZ4Y6XU">https://t.co/tkCGZ4Y6XU</a> <a href="https://twitter.com/hashtag/potatotips?src=hash">#potatotips</a></p>&mdash; 所 友太 (@tokorom) <a href="https://twitter.com/tokorom/status/803571927168094209">November 29, 2016</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>


- UIKitの各クラス/プロトコルの役割を知る
- 最小限/適切なレイヤーで実装する
- 標準のコンポーネントと同じように振る舞う
- 5分で話す内容ではなかった