Edited at

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

More than 1 year has passed since last update.

UIControlUIKeyInputを使ったUIPickerViewの出し入れ実装です。システムのキーボードとタッチジェスチャーで、ピッカーの入出力をViewに対し簡単に導入します。


最終的に出来上がるもの


実装


1. UIControlを継承する

PickerKeyboardというクラスを新しく作ります。

class PickerKeyboard: UIControl {}

このクラスは最終的に、


  • タッチジェスチャーに反応してUIPickerViewを出す

  • 選択した列の文字列を表示させる

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


2. UIkeyInputを実装して、キーボードを表示させられるようにする

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

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にすればよいです。

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を継承したクラス(つまりUIView系)は全て、inputViewというプロパティを持ちます。ここへ任意のViewを入れることでそれをシステムキーボードの代わりに表示させられます。

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ではキーボードの上にボタンを配置するために使用していたと思います。同じようにピッカーの選択完了ボタンをおきます。

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(_:)内で直接文字列を書いてしまいます。

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の2つを持ち、これをカスタマイズすることでキーボードの代わりに別の何かを出すことができます。また、タッチジェスチャーに対応させるのであれば、UIViewではなくUIControlを継承するのが楽でしょう。


資料