inputViewを使ってカスタムキーボードをつくろう！（Swift）

![Xcode-12](https://img.shields.io/badge/Xcode-12-brightgreen) ![Swift-5.3](https://img.shields.io/badge/Swift-5.3-brightgreen) ![iOS-14](https://img.shields.io/badge/iOS-14-brightgreen)

## はじめに
`UITextField` や `UITextView` で入力する際はキーボードの入力タイプが色々用意されていますが実はこのキーボード色々カスタムができます！

たぶん実用的なのは `UIPickerView` とか `UIDatePicker` を表示したりとかです（なんか昔半角カナ用キーボードを作った記憶があるけどあんまよくなかった:innocent:）。

これからはもう `UIKit` を使うことも少なくなっていくのかもしれませんが備忘録として。。。

＊注：「つくろう」って書きましたがとくにカスタムキーボードを薦めるものではありません。

## UITextFieldをカスタマイズ
試しに `UITextField` のキーボードをカスタムしてみます。やり方は簡単で `inputView` に任意の `View` を設定するだけで OK です:v:

```swift
// こんなのとか
let keyboard = CustomKeyboardView(frame: .init(origin: .zero, size: .init(width: 284, height: 284)))
keyboard.delegate = self
textField.inputView = keyboard

// こんなのとか
let picker = UIPickerView()
picker.delegate = self
picker.dataSource = self
textField.inputView = picker
```

あとはデリゲートとかで入力文字を受け取って `UITextField` に表示するだけです。

こんな感じ。

| カスタム View | ピッカー |
| --- | --- |
|![custom](https://user-images.githubusercontent.com/34936885/105650533-5b804c80-5ef7-11eb-9a40-5563f8550adb.png)|![picker](https://user-images.githubusercontent.com/34936885/105650543-633ff100-5ef7-11eb-8ae6-79e9d5563cd8.png)|

カスタム View はこんな感じです。

xib

<img width="300" alt="xib" src="https://user-images.githubusercontent.com/34936885/105650570-72bf3a00-5ef7-11eb-91c2-567185b3d8b0.png">

```swift
protocol CustomKeyboardViewDelegate: AnyObject {
    func customKeyboardView(_ customKeyboardView: CustomKeyboardView, didSelectKey key: String)
}

final class CustomKeyboardView: UIView {

    weak var delegate: CustomKeyboardViewDelegate?
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    private func loadNib() {
        let view = Bundle.main.loadNibNamed("CustomKeyboardView", owner: self, options: nil)?.first as! UIView
        view.frame = bounds
        view.translatesAutoresizingMaskIntoConstraints = true
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(view)
    }

    @IBAction private func selectKey(_ sender: UIButton) {
        delegate?.customKeyboardView(self, didSelectKey: sender.titleLabel!.text!)
    }
}
```

キーボードの高さはわりと自由みたいです:raised_hands:

<img width="300" alt="custom_2" src="https://user-images.githubusercontent.com/34936885/105650559-6aff9580-5ef7-11eb-9d5a-fb719fb29f41.png">

サクッとカスタムキーボードが表示できました！ただカスタムキーボードを入力制限目的で利用するにはいくつか問題があるのでオススメしません:no_good:コピペとか外部キーボード接続とか。。。

コピペに関しては `UITextField` のカスタムクラスをつくって `canPerformAction` ごにょごにょすればいける気がしますが外部キーボードに関しては防げないと思われます（そもそもあんまり `UITextField` で入力制限なんてしない方がいいと思います）。

## UIControlをカスタマイズ
とくに `UITextField` の `inputView` に設定する！でも問題ないのですが `UITextField` を使うとなるとレイアウトの自由度が低いのでせっかくなんでカスタムクラスをつくった方が自由度が高いのでおすすめです:+1:

下記のように `UIControl` を継承したクラスをつくって `canBecomeFirstResponder` と `inputView` を設定してやるとカスタムキーボードが表示できます。`UIView` じゃないのは `addAction` がしたかったからです。フォーカス時がわかりにくいので枠線の色を変更しています。

```swift
protocol PickerInputControlDelegate: AnyObject {
    func pickerInputControl(_ pickerInputControl: PickerInputControl, didSelectValue value: String)
}

final class PickerInputControl: UIControl {

    var items = [String]()
    weak var delegate: PickerInputControlDelegate?

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        commonInit()
    }

    private func commonInit() {
        layer.cornerRadius = 8.0
        layer.borderWidth = 1.0
        layer.borderColor = UIColor.systemGray.cgColor
        addAction(.init(handler: { [weak self] _ in
            self?.isSelected.toggle()
            if self?.isSelected == true {
                self?.becomeFirstResponder()
            } else {
                self?.resignFirstResponder()
            }
        }), for: .touchUpInside)
    }

    override var inputView: UIView? {
        let picker = UIPickerView()
        picker.delegate = self
        picker.dataSource = self
        return picker
    }

    override var canBecomeFirstResponder: Bool {
        return true
    }

    override var isSelected: Bool {
        didSet {
            layer.borderColor = isSelected ? UIColor.systemBlue.cgColor : UIColor.systemGray.cgColor
        }
    }

    @discardableResult
    override func becomeFirstResponder() -> Bool {
        let value = super.becomeFirstResponder()
        isSelected = isFirstResponder
        return value
    }

    @discardableResult
    override func resignFirstResponder() -> Bool {
        let value = super.resignFirstResponder()
        isSelected = isFirstResponder
        return value
    }
}

extension PickerInputControl: UIPickerViewDelegate {

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        delegate?.pickerInputControl(self, didSelectValue: items[row])
    }
}

extension PickerInputControl: UIPickerViewDataSource {

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return items.count
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return items[row]
    }
}
```

こんな感じで `UIControl` なので中に `View` 置き放題です。

<img width="300" alt="control" src="https://user-images.githubusercontent.com/34936885/105650614-98e4da00-5ef7-11eb-8856-641a8305f35f.png">

`UIPickerViewDelegate`, `UIPickerViewDataSource`, `items` は外に出した方が汎用性が高いかも:innocent:

## static cellと組み合わせる
おまけで `UITableViewController` の static cell と組み合わせるとフォーカス時に自動スクロールしてくれるので複数の入力項目がある場合はおすすめです:+1:

こんな感じ。

![static_cell](https://user-images.githubusercontent.com/34936885/105650626-9edabb00-5ef7-11eb-8b86-c28875f5665d.gif)

## おわりに
カスタムキーボードを作る際はタップ領域をちゃんと確保してあげると素敵です。ちょっと古い記事ですが下記の記事はキーボードのタップ領域とか詳しく調べていておもしろいです。

[iPhone の当たり判定を検証した](https://www.sociomedia.co.jp/4152)

わりとお世話になった手法だけどこれからはあんまり使わなくなるのかな `UIKit`。。。

調べてて気になったけどこいつ知らねえ:sweat:

<img width="500" alt="inputviewcontroller" src="https://user-images.githubusercontent.com/34936885/105672148-47ecda00-5f27-11eb-83f4-c82ce3f0147f.png">

## 参考
* [inputView](https://developer.apple.com/documentation/uikit/uiresponder/1621092-inputview)
* [iPhone の当たり判定を検証した](https://www.sociomedia.co.jp/4152)