＜Swift＞UITextFieldとUIPickerViewをRxSwiftでバインドさせる方法(閉じるボタン付き、カーソルなし)


```swift:TodolistEditViewController.swift
class TodolistEditViewController: UIViewController {

    @IBOutlet weak var categoryTextField: UITextField!
        
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
    }
    
    private func setupView(){
        // Picker
        let pickerView = UIPickerView()
        categoryTextField.inputView = pickerView
        let strs = ["カテゴリーなし", "カテゴリー1", "カテゴリー2"]
        Observable.just(strs)
            .bind(to: pickerView.rx.itemTitles) { _, str in
                return str
        }.disposed(by: disposeBag)
        pickerView.rx.modelSelected(String.self)
            .map { strs in
                return strs.first
            }
            .bind(to: categoryTextField.rx.text)
            .disposed(by: disposeBag)
        
        // Pickerのヘッダー
        let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 35))
        let spacelItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
        let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: nil)
        toolbar.setItems([spacelItem, doneItem], animated: true)
        categoryTextField.inputAccessoryView = toolbar
        let doneItemObservable = doneItem.rx.tap.asObservable()
        doneItemObservable.subscribe(
            onNext: { [weak self] in
                // pickerを閉じるには、TextFieldの編集モードを終了させる
                self?.categoryTextField.endEditing(true)
            }
        ).disposed(by: disposeBag)
    }
}
```
上記のままでは、テキストフィールドに対してUIPickerView以外からの入力も受け付けてしまう。
そこで、以下の対処を入れる。

```swift:TodolistEditViewController.swift
class TodolistEditViewController: UIViewController {
    private func setupView(){
        ...
        // UIPickerView以外からの入力を拒否するためのdelegate実装
        categoryTextField.delegate = self
        // 上記だけではカーソルは消えないので、カーソルの色をクリアにする
        categoryTextField.tintColor = UIColor.clear
        ...
    }
}

extension TodolistEditViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        return false
    }
}
```

ただし、TextFieldを長押しするとcopy/cut/paseteのメニューが表示される。
これを防ぐには、以下のようにカスタムViewにするしか方法がなさそう。
https://riptutorial.com/ja/ios/example/30704/
https://qiita.com/Simmon/items/f9d60ab51cc6b0b4b3bc

■補足1
初期値をUIPickerViewに設定したい場合は以下を追加する
※UITextFieldには自動的に反映される

```swift:TodolistEditViewController.swift
class TodolistEditViewController: UIViewController {
    private func setupView(){
        ...
        let selectedRow = 1
        pickerView.selectRow(selectedRow, inComponent: 0, animated: false)
        pickerView.delegate?.pickerView!(pickerView,
        didSelectRow: selectedRow,
        inComponent: 0)
        ...
    }
}
```

■補足2
UIPickerViewにキャンセルボタンを設定したい場合は以下のように変更する

```swift:TodolistEditViewController.swift
class TodolistEditViewController: UIViewController {
    private let pickerView = UIPickerView()
    ...
    private func setupView(){
        ...
        // カテゴリーPickerのヘッダー
        let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 35))
        let cancelItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: nil)
        let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
        let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: nil)
        toolbar.setItems([cancelItem,spaceItem, doneItem], animated: true)
        categoryTextField.inputAccessoryView = toolbar
        // ヘッダーのキャンセルボタン
        let cancelItemObservable = cancelItem.rx.tap.asObservable()
        cancelItemObservable.subscribe(
            onNext: { [weak self] in
                // 初期値に戻す
                self?.resetCategoryTextField()
                // pickerを閉じる
                self?.categoryTextField.endEditing(true)
            }
        ).disposed(by: disposeBag)
        // ヘッダーの決定ボタン
        let doneItemObservable = doneItem.rx.tap.asObservable()
        doneItemObservable.subscribe(
            onNext: { [weak self] in
                // pickerを閉じる
                self?.categoryTextField.endEditing(true)
            }
        ).disposed(by: disposeBag)
        ...
    }

    private func resetCategoryTextField(){
        // ドラムロールを初期値に戻す
        // TODO idから動的に取得
        let selectedRow = 1
        pickerView.selectRow(selectedRow, inComponent: 0, animated: false)
        pickerView.delegate?.pickerView!(pickerView,
        didSelectRow: selectedRow,
        inComponent: 0)
    }
}
```

参考にしたサイト
https://culumn.hatenablog.com/entry/2018/06/07/120000
https://tech.studyplus.co.jp/entry/2018/10/15/114548
http://www.366service.com/jp/qa/d46ff39ee4fe4c17803c9c7affb4495f
https://www.egao-inc.co.jp/programming/swift_uipickerview/
https://grandbig.github.io/blog/2018/10/13/popuppickerview/
https://makotton.com/2016/10/13/1544
https://qiita.com/kazuhiro4949/items/d20f08343937cab82390
https://xyk.hatenablog.com/entry/2014/10/02/105147
https://qiita.com/wai21/items/765206fdc37b2c258481
https://ja.stackoverflow.com/questions/28246/
https://stackoverflow.com/questions/50460554/rxswift-set-initial-value-for-uipickerview