UIControlとUIKeyInputでUIPickerViewを手軽に下からシュッと出す

**[UIControl](https://developer.apple.com/reference/uikit/uicontrol)**と**[UIKeyInput](https://developer.apple.com/reference/uikit/uikeyinput)**を使った**[UIPickerView](https://developer.apple.com/reference/uikit/uipickerview)**の出し入れ実装です。システムのキーボードとタッチジェスチャーで、ピッカーの入出力をViewに対し簡単に導入します。

# 最終的に出来上がるもの
<img src="https://qiita-image-store.s3.amazonaws.com/0/29477/c3de50cc-e690-7728-c576-481746bfeb3d.gif" height="600"/>

# 実装

## 1. UIControlを継承する
PickerKeyboardというクラスを新しく作ります。

```swift
class PickerKeyboard: UIControl {}
```

このクラスは最終的に、

+ タッチジェスチャーに反応してUIPickerViewを出す
+ 選択した列の文字列を表示させる

という役割を持ったViewにします。ただしここではUIViewではなく、その子クラスである、タッチジェスチャーに対する便利機能を持ったUIControlを継承します。

## 2. UIkeyInputを実装して、キーボードを表示させられるようにする
UIKeyInputを実装しているViewは、システムキーボードを表示させて入力文字を処理することができます。

```swift
class Pickerkeyboard: UIControl {
    // 1.で作ったクラスへ、入力文字列を保存するためのプロパティを追加
    private var textStore: String = ""
}

// UIKeyInputを適用させる
extension PickerKeyboard: UIKeyInput {
    
    // 以下の3つのメソッドはUIkeyInputで必ず実装しなければならないメソッド
    // 主にキーボード入力が行われたときにそれぞれのメソッドが呼び出される

    // 入力されたテキストが存在するか
    func hasText() -> Bool {
        return !textStore.isEmpty
    }
    
    // テキストが入力されたときに呼ばれる
    func insertText(text: String) {
        textStore += text
        setNeedsDisplay()
    }
    
    // バックスペースが入力されたときに呼ばれる
    func deleteBackward() {
        textStore.removeAtIndex(textStore.characters.endIndex.predecessor())
        setNeedsDisplay()
    }
}
```

これでViewに対してキーボードを呼び出せるようになりました。

## 3. タッチジェスチャーでViewをFirst Responderにする
システムキーボードを出すには、UITextFieldやUITextViewと同じく、UIKeyInputが実装されたViewをFirst Responderにすればよいです。

```swift
class PickerKeyboard: UIControl {
    // ...省略
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        // viewのタッチジェスチャーを取る
        addTarget(self, action: #selector(PickerKeyboard.didTap(_:)), forControlEvents: .TouchDown)
    }

    // タッチされたらFirst Responderになる
    func didTap(sender: PickerKeyboard) {
        becomeFirstResponder()
    }

    // First Responderになるためにはこのメソッドは常にtrueを返す必要がある
    override func canBecomeFirstResponder() -> Bool {
        return true
    }
}
```

これでタッチしたときにシステムキーボードが表示されます。

## 4. UIResponderのinputViewプロパティをオーバーライドして、UIPickerViewを返す
**[UIResponder](https://developer.apple.com/reference/uikit/uiresponder)**を継承したクラス(つまりUIView系)は全て、inputViewというプロパティを持ちます。ここへ任意のViewを入れることでそれをシステムキーボードの代わりに表示させられます。

```swift
class PickerKeyboard: UIControl {
    // ... 省略
 
    // ピッカーに表示させるデータ
    var data: [String] = ["月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"]

    // inputViewをオーバーライドさせてシステムキーボードの代わりにPickerViewを表示
    override var inputView: UIView? {
        let pickerView = UIPickerView()
        pickerView.delegate = self
        let row = data.indexOf(textStore) ?? -1
        pickerView.selectRow(row, inComponent: 0, animated: false)
        return pickerView
    }
}

// UIPickerViewDelegateとDataSourceを実装して、dataの内容をピッカーへ表示させる
extension PickerKeyboard: UIPickerViewDelegate, UIPickerViewDataSource {
    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return data.count
    }
    
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return data[row]
    }
    
    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // ピッカーから選択されたらその値をtextStoreへ入れる
        textStore = data[row]
        setNeedsDisplay()
    }
}
```

これでシステムキーボードの代わりにピッカーが表示され、ピッカーから文字列を選択できるようになりました。

## 5. inputAccessoryViewをオーバーライドしてピッカーを閉じるボタンを配置
UIResponderはinputViewの他にinputAccessoryViewというものを持っています。inputAccessoryViewはUITextFieldやUITextViewではキーボードの上にボタンを配置するために使用していたと思います。同じようにピッカーの選択完了ボタンをおきます。

```swift
class PickerKeyboard: UIControl {
    // ... 省略
    
        override var inputAccessoryView: UIView? {
        // キーボードを閉じるための完了ボタン
        let button = UIButton(type: .System)
        button.setTitle("Done", forState: .Normal)
        button.addTarget(self, action: #selector(PickerKeyboard.didTapDone(_:)), forControlEvents: .TouchDown)
        button.sizeToFit()

        // キーボードの上に置くアクセサリービュー
        let view = UIView(frame: CGRect(x: 0, y: 0, width: self.bounds.width, height: 44))
        view.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
        view.backgroundColor = .groupTableViewBackgroundColor()
        
        // ボタンをアクセサリービュー上に設置
        button.frame.origin.x = 16
        button.center.y = view.center.y
        button.autoresizingMask = [.FlexibleRightMargin, .FlexibleBottomMargin, .FlexibleTopMargin]
        view.addSubview(button)

        return view
    }

    // ボタンを押したらresignしてキーボードを閉じる
    func didTapDone(sender: UIButton) {
        resignFirstResponder()
    }    
}
```

これでピッカーの出し入れが実装できました。

## 6. 選択された文字列を表示
最後に選択された文字列を"PickerKeyboard"上へ表示させます。UILabelをサブビューとして置くやり方もありますが、ここではdrawRect(_:)内で直接文字列を書いてしまいます。

```swift
class PickerKeyboard: UIControl {

    // ...省略    

    // PickerViewで選択されたデータを表示する
    override func drawRect(rect: CGRect) {
        UIColor.blackColor().set()
        UIRectFrame(rect)
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .Center
        let attrs: [String: AnyObject] = [NSFontAttributeName: UIFont.systemFontOfSize(17), NSParagraphStyleAttributeName: paragraphStyle]
        NSString(string: textStore).drawInRect(rect, withAttributes: attrs)
    }
}
```

「最終的に出来上がるもの」で貼ったgifアニメのようなビューを作ることができました。あとはstoryboardなどでUIViewを置いて、クラス名をPickerKeyboardにしましょう。タッチジェスチャーに対してPickerの出し入れができ、選択した文字列がView上に表示されると思います。

# コード全体
コード全体はgistの方に書いておきました。
https://gist.github.com/kazuhiro4949/da5c8d5159e4dd8707f766f0661ee892

# 他のやり方
同じようにUIPickerViewを下からシュッと出すやり方としては、他はこんな感じなのがあるかなぁと思います。

- UIActionSheetのviewをUIPickerViewに差し替える
    - 昔はこのやり方でやってたけど、今はUIActionSheetはdepricatedな上に~~UIAlertControllerでは同様のことはできなさそう？~~ (と思ったけど気になってやってみたらiOS9で普通にできました。規約的にどうだろうというのと、レイアウト調整が難しそうですが...)
- UITextFieldのinputViewとしてUIPickerViewを設定する
    - これでも問題なく実現できる。ただしUITextFieldそのものを使うわけではない場合に若干イケてないのとカスタマイズ性が低い
- UIViewの上において、UIPresentationControllerでカスタムモーダルとして下から表示させる
    - 一番カスタマイズ性は高い気がするし素直な実装だけれど、まぁまぁ面倒くさい。ActionSheetの上に置いていた頃のような見た目にもできる。

# まとめ
UIKeyInputを実装することで、システムキーボードとインタラクションできるカスタムViewを作ることができます。UIResponderはinputViewとinputAccessoryViewの２つを持ち、これをカスタマイズすることでキーボードの代わりに別の何かを出すことができます。また、タッチジェスチャーに対応させるのであれば、UIViewではなくUIControlを継承するのが楽でしょう。

# 資料
+ [Managing the Keyboard](https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html)
+ [UIKeyInput](https://developer.apple.com/reference/uikit/uikeyinput)
+ [UIControl](https://developer.apple.com/reference/uikit/uicontrol)
+ [UIPickerView](https://developer.apple.com/reference/uikit/uipickerview)
+ [UIResponder](https://developer.apple.com/reference/uikit/uiresponder)