Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

UIPickerViewをUIControlを使用してキーボードの様に表示する

こんにちは。 入社して一ヶ月が経過したiOSエンジニアの弘田です。
今回はUIPickerViewをキーボードの様に表示する方法を解説します。

なぜそんなことをするの?

昔のiPhoneでしたら画面の中心などにUIPickerViewを表示しても画面サイズが小さかったので片手で操作できていましたが、最近は大画面化が進み片手での操作が難しくなってきました。
操作性を損なわずにUIPickerViewを使用してもらう為にも今回の方法が役にたつと思います。

Human Interface GuidelinesでもPickerついて触れているページがありこの様な記載があります。

Avoid switching screens to show a picker. A picker works well when displayed in context, below or in close proximity to the field being edited.

翻訳
ピッカーを表示するように画面を切り替えることは避けてください。ピッカーは、編集中のフィールドの下、または近くにコンテキストで表示されたときにうまく機能します。

今回目指すもの

※シミューレーターなのでキーボードを閉じるアニメーションが少しおかしいです

実装

1. UIControlを継承したclassを作る

class pickerKeyboard: UIControl {

}

2.イニシャライザを作成し、自身がタップされた時にinputViewを出す処理を作る

class pickerKeyboard: UIControl {
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        addTarget(self, action: #selector(tappedPickerKeyboard(_:)), for: .touchDown)
    }
    
    @objc private func tappedPickerKeyboard(_ sender: PickerKeyboard) {
        self.becomeFirstResponder()
    }
}

3.canBecomeFirstResponderをtrueで返して自身をFirstResponderにする

canBecomeFirstResponderのデフォルトはfalseになっていて、
trueを返さないと後述のinputViewで指定したViewが表示されません。

class pickerKeyboard: UIControl {
    
    //~~~省略~~~
    
    override var canBecomeFirstResponder: Bool {
        return true
    }
    
}

4.FirstResponderになった上でinputViewをoverrideする

ここでは表示したいViewを返します。
今回はUIPickerViewをaddSubviewしたUIViewを返します。
UIViewを返す理由はSafeAreaに対応するためです。
inputViewについて(Apple公式)

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputView: UIView? {
        let pickerView: UIPickerView = UIPickerView()
        pickerView.delegate = self
        pickerView.dataSource = self
        pickerView.backgroundColor = UIColor.white
        pickerView.autoresizingMask = [.flexibleHeight]
        
        // SafeArea対応をする為にUIViewを挟む
        let view = UIView()
        view.backgroundColor = .white
        view.autoresizingMask = [.flexibleHeight]
        view.addSubview(pickerView)
        
        pickerView.translatesAutoresizingMaskIntoConstraints = false
        pickerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        pickerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        pickerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
        
        return view
    }
}

5.inputAccessoryViewをoverrideしてUIPickerViewを閉じるボタンを作る

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputAccessoryView: UIView? {
        
        let view = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
        view.frame = CGRect(x: 0, y: 0, width: frame.width, height: 44)

        let closeButton = UIButton(type: .custom)
        closeButton.setTitle("閉じる", for: .normal)
        closeButton.sizeToFit()
        closeButton.addTarget(self, action: #selector(tappedCloseButton(_:)), for: .touchUpInside)
        closeButton.setTitleColor(UIColor(red: 0, green: 122/255, blue: 1, alpha: 1.0), for: .normal)

        view.contentView.addSubview(closeButton)
        
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        closeButton.widthAnchor.constraint(equalToConstant: closeButton.frame.size.width).isActive = true
        closeButton.heightAnchor.constraint(equalToConstant: closeButton.frame.size.height).isActive = true
        closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
        closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true

        return view
    }

    @objc private func tappedCloseButton(_ sender: UIButton) {
        resignFirstResponder()
    }
}

6.通常のUIPickerViewと同様にUIPickerViewDelegateとUIPickerViewDataSourceを継承してデータを表示

class pickerKeyboard: UIControl {
    let array:[String] = ["A","B","C","D","E"]
        
    //~~~省略~~~
}

extension PickerKeyboard: UIPickerViewDelegate, UIPickerViewDataSource {
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return array.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return array[row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // delegateなどでViewControllerに選択された情報を渡す
    }
}

7.StoryboradやXibでUIViewのCustomClassとして設定する

まとめ

手順4のUIPickerViewを作った時にpickerView.backgroundColor = UIColor.whiteとしているのでわかり難いですが、 別の色に変更するとSafeArea対応できていることが確認できます。

記事で紹介したコードはGithubで公開しています。
https://github.com/srknra/PickerKeyboard

今回はUIPickerViewをキーボードの様に表示してUXを低下させない様な工夫でしたが、
Studyplusのアプリでは他にもユーザーの事を考えて様々な工夫をしてるので今後もブログで紹介していこうと思います。