[Swift]キーボード上にあるツールバーの高さを動的に変更する方法

# はじめに
UITextFieldやUITextViewで表示されるキーボード上のツールバーに、バー上のボタンをタップするともう1段バーが表示される方法になります。
イメージとしてはiOS純正のリマインダーアプリで実装されている、リマインド時間やロケールを選択するツールバーを同じものになります。
`textView.inputAccessoryView = toolbar`でキーボード上にツールバーを設定する基本的な方法はよく見かけますが、
動的にバーの高さを変更する方法が見当たらなかったので参考になれば幸いです。

# 完成イメージ
ツールバー上のベルのボタンをタップすると、もう1段上に通知時間のインターバルを選択するボタンが表示されます。
![RocketSim Recording - iPhone 12 Pro Max - 2020-11-13 16.39.07.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99869/565b4134-df08-3bec-071f-8317c3b7dfc6.gif)

# 実装方法
はじめは`textView.inputAccessoryView = toolbar`で設定したtoolbarの高さを動的に変更しようとしたのですが、なかなかうまくできず。。。
(実装方法わかる方いれば教えてください！)
今回は`textView.inputAccessoryView = toolbar`で設定したtoolbarに対して子viewを`addSubview`、`sendSubviewToBack`で裏側に隠しておいて、
上下に移動することで表示を切り替えています。

今回はUITableViewにCustomTableViewCellを作成し、セル上にUITextViewを配置しています。
キーボード上にツールバーを表示するためにCustomTableViewCell内でUITextViewに対してキーボードが表示されるようにします。

```swift:CustomTableViewCell.swift
let lowerToolbar = LowerToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
lowerToolbar.initToolbarButton(item: item!)
lowerToolbar.delegate = self
textView.inputAccessoryView = lowerToolbar
```
上記で設定しているlowerToolbarの中身がこちらになります

```swift:LowerToolbar.swift
protocol LowerToolbarDelegete: class {
    func onTouchToolbarButton(selectedTimeInterval: Int)
}

class LowerToolbar: UIView {
    @IBOutlet weak var toolbar: UIToolbar!
    var upperToolbar: UIView?
    var isHidenUpperToolbar: Bool = true
    var upperToolbarCenterY: CGFloat = 0
    weak var delegate: LowerToolbarDelegete! = nil
    let oneHourButton = UIButton(type: .system)
    let threeHourButton = UIButton(type: .system)
    let fiveHourButton = UIButton(type: .system)
    var item: ItemModel?
    var selectedTimeInterval: Int = 0 // Last tapped button.
    
    override init(frame: CGRect){
        super.init(frame: frame)
        loadNib()
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib(){
        let view = Bundle.main.loadNibNamed("LowerToolbar", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
        toolbar.clipsToBounds = true
        
        upperToolbar = UIView(frame: CGRect(x: 0, y: -1, width: self.frame.size.width, height: 61))
        upperToolbar?.backgroundColor = UIColor.toolbar
        self.addSubview(upperToolbar!)
        self.sendSubviewToBack(upperToolbar!)
        upperToolbarCenterY = upperToolbar!.center.y
    }
    
    func createToolbarButton(btn: UIButton, title: String, timeInterval: Int) {
        btn.setTitle(title, for: .normal)
        btn.tag = timeInterval
        btn.layer.borderWidth = 1
        btn.layer.borderColor = UIColor.toolbarBorder.cgColor
        btn.layer.cornerRadius = 20
        btn.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
        btn.backgroundColor = UIColor.toolbarButton
        btn.addTarget(self, action: #selector(tapHoursButton), for: .touchUpInside)
        upperToolbar?.addSubview(btn)
        
        if self.item?.timeInterval == timeInterval {
            btn.layer.borderColor = UIColor.systemBlue.cgColor
            btn.backgroundColor = UIColor.systemBlue
            btn.tintColor = UIColor.white
        }
    }
    
    func initToolbarButton(item: ItemModel) {
        self.item = item

        createToolbarButton(btn: oneHourButton, title: "1hour", timeInterval: 60)
        oneHourButton.translatesAutoresizingMaskIntoConstraints = false
        oneHourButton.leadingAnchor.constraint(equalTo: upperToolbar!.leadingAnchor, constant: 20).isActive = true
        oneHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true
        
        createToolbarButton(btn: threeHourButton, title: "3hour", timeInterval: 180)
        threeHourButton.translatesAutoresizingMaskIntoConstraints = false
        threeHourButton.leadingAnchor.constraint(equalTo: oneHourButton.trailingAnchor, constant: 20).isActive = true
        threeHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true
        
        createToolbarButton(btn: fiveHourButton, title: "5hour", timeInterval: 400)
        fiveHourButton.translatesAutoresizingMaskIntoConstraints = false
        fiveHourButton.leadingAnchor.constraint(equalTo: threeHourButton.trailingAnchor, constant: 20).isActive = true
        fiveHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true
    }
    
    func initUpperToolbar() {
        upperToolbar?.center.y = upperToolbarCenterY
        isHidenUpperToolbar = true
    }
    
    func decorateTappedHourButton(btn: UIButton) {
        btn.layer.borderColor = UIColor.systemBlue.cgColor
        btn.backgroundColor = UIColor.systemBlue
        btn.tintColor = UIColor.white
    }
    
    func decorateNormalHourButton(btn: UIButton) {
        btn.layer.borderColor = UIColor.toolbarBorder.cgColor
        btn.backgroundColor = UIColor.toolbarButton
        btn.tintColor = .systemBlue
    }
    
    func decorateHourButton(btn1: UIButton, btn2: UIButton, btn3: UIButton, newTimeInterval: Int) {
        if selectedTimeInterval == newTimeInterval {
            selectedTimeInterval = 0
            decorateNormalHourButton(btn: btn1)
        } else {
            selectedTimeInterval = newTimeInterval
            decorateTappedHourButton(btn: btn1)
        }
        decorateNormalHourButton(btn: btn2)
        decorateNormalHourButton(btn: btn3)
    }
    
    @objc func tapHoursButton(btn: UIButton) {
        switch btn.tag {
        case 60:
            decorateHourButton(btn1: oneHourButton, btn2: threeHourButton, btn3: fiveHourButton, newTimeInterval: btn.tag)
        case 180:
            decorateHourButton(btn1: threeHourButton, btn2: oneHourButton, btn3: fiveHourButton, newTimeInterval: btn.tag)
        case 400:
            decorateHourButton(btn1: fiveHourButton, btn2: oneHourButton, btn3: threeHourButton, newTimeInterval: btn.tag)
        default:
            print("no item")
        }
        delegate?.onTouchToolbarButton(selectedTimeInterval: btn.tag)
    }
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if isHidenUpperToolbar {
            let rect = self.bounds
            return rect.contains(point)
        } else {
            var rect = self.bounds
            if rect.contains(point) {
                return rect.contains(point)
            }
            
            rect.origin.y -= 60
            return rect.contains(point)
        }
    }
    
    @IBAction func tapBellButton(_ sender: Any) {
        if isHidenUpperToolbar {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y -= 60
                self.isHidenUpperToolbar = false
                self.layoutIfNeeded()
            })
        } else {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y += 60
                self.isHidenUpperToolbar = true
                self.layoutIfNeeded()
            })
        }
    }
}
```
# 解説

```swift:LowerToolbar.swift
    func loadNib(){
        let view = Bundle.main.loadNibNamed("LowerToolbar", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
        toolbar.clipsToBounds = true
        
        upperToolbar = UIView(frame: CGRect(x: 0, y: -1, width: self.frame.size.width, height: 61))
        upperToolbar?.backgroundColor = UIColor.toolbar
        self.addSubview(upperToolbar!)
        self.sendSubviewToBack(upperToolbar!)
        upperToolbarCenterY = upperToolbar!.center.y
    }
```
LowerToolbarを初期化する際に、UpperToolbarを作成し、`addSubview`して`sendSubviewToBack`で背面に移動することで非表示にします。

```swift:LowerToolbar.swift
    @IBAction func tapBellButton(_ sender: Any) {
        if isHidenUpperToolbar {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y -= 60
                self.isHidenUpperToolbar = false
                self.layoutIfNeeded()
            })
        } else {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y += 60
                self.isHidenUpperToolbar = true
                self.layoutIfNeeded()
            })
        }
    }
```
ベルボタンをタップした際に、upperToolbarを上下に60移動しています。

```swift:LowerToolbar.swift
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if isHidenUpperToolbar {
            // upperToolbarが非表示で
            // lowerToolbarがタップされた場合
            let rect = self.bounds
            return rect.contains(point)
        } else {
            // upperToolbarが表示されているが
            // upperToolbarがタップされた場合
            var rect = self.bounds
            if rect.contains(point) {
                return rect.contains(point)
            }
            
            // lowerToolbarがタップされた場合
            rect.origin.y -= 60
            return rect.contains(point)
        }
    }
```
ポイントなのがこの部分で、upperToolbarを60上に移動した場合、親viewのlowerToolbarのframe領域からはみ出してしまうため、
upperToolbarの上に乗っているボタンのイベントを受け取れなくなってしまいます。
そこで、`override func point(inside point: CGPoint, with event: UIEvent?) -> Bool `をオーバーライドして
upperToolbarがタップされた場合にイベントが受け取れるようにしています。

# その他気づき
今回はlowerToolbarの中でupperToolbarを生成してコードで作成しました。
これは、upperToolbarをstoryboardで個別に作成したものを使用したところ、
delegateを使用してタップされたイベントをupperToolbar->lowerToolbar->CustomTableViewCellへ移譲することができなかったためです。
(lowerToolbarでupperToolbarのdelegate設定できなかったです。)

# まとめ
ツールバーの高さを動的に変更できれば一番シンプルな実装にはなるかと思いますが、
高さの変更が思ったようにできず、違う方法でトライしてみました。
アップルの純正リマインダーアプリではどうやって実装してるんですかね。気になります。
何か参考になれば嬉しいです。