【Swift】チャットアプリでよくみるキーボードの動きに追随するTextFieldとButtonの実装

チャットアプリでよくみるキーボードの動きに追随するTextFieldとButtoonを実装してみました。

##開発環境
Xcode 12.3
Swift 5

##デモ
こんなやつです。

![イメージ.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678954/f937111e-493e-e838-4630-55b6f470f71e.gif)

##コード
```swift:InputAccesoryViewController.swift
import UIKit

class InputAccesoryViewController: UIViewController {
    
    let outputTableView = UITableView()
    
    //プロパティにアクセスできるようにグローバル定数として定義
    let inputTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Aa"
        textField.backgroundColor = .systemGray6
        textField.borderStyle = .roundedRect
        return textField
    }()

    override var canBecomeFirstResponder: Bool { return true }

    var inputContainerView: UIView!
    
    //inputAccesoryViewの中身を作って返り値に入れます。
    override var inputAccessoryView: UIView? {

        if inputContainerView == nil {

            //容器となるContainerViewを作成
            inputContainerView = InputContainerView()
            inputContainerView.backgroundColor = .systemYellow
            
            //ContainerViewに定義しておいたTextFieldを追加
            inputContainerView.addSubview(inputTextField)

            //ContainerViewの高さがキーボードを閉じた時と開いた時で自動でリサイズする
            inputContainerView.autoresizingMask = .flexibleHeight
            
            //送信ボタンの追加
            let sendButton = UIButton(type: .system)
            sendButton.setImage(UIImage(systemName: "paperplane"), for: .normal)
            
            //Button Actionを追加
            sendButton.addTarget(self, action: #selector(didTapSend), for: .touchUpInside)
            
            //ContainerViewにSendButtonを追加
            inputContainerView.addSubview(sendButton)
            
            //それぞれの部品に制約を設定します
            sendButton.anchor(
                right: inputContainerView.trailingAnchor,
                paddingRight: 8,
                width: 50,
                height: 50)
            
            inputTextField.anchor(
                top: inputContainerView.topAnchor,
                left: inputContainerView.leadingAnchor,
                bottom: inputContainerView.layoutMarginsGuide.bottomAnchor,
                right: sendButton.leadingAnchor,
                paddingTop: 8,
                paddingLeft: 8,
                paddingBottom: 8,
                paddingRight: 8)
        }
        //作成したContainerViewを返り値に入れる
        return inputContainerView
    }
    
    var messages = [String()]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        inputTextField.delegate = self
        outputTableView.dataSource = self
        
        outputTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        
        //これでTableViewのスクロールに合わせてキーボードが閉じれるようになる
        outputTableView.keyboardDismissMode = .interactive
        view = outputTableView
    }
    
    @objc func didTapSend() {
        
        if let message = inputTextField.text, inputTextField.text != "" {
            messages.append(message)
            inputTextField.text = ""
            outputTableView.reloadData()
        }
    }
}

extension InputAccesoryViewController: UITextFieldDelegate {
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        didTapSend()
        textField.resignFirstResponder()
        return true
    }
}

extension InputAccesoryViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messages.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "cell")
        cell.textLabel?.text = messages[indexPath.row]
        return cell
    }
}
```

###InputContainerView.swift
```swift:InputContainerView.swift
import UIKit

class InputContainerView: UIView {
    
    //これは、inputAccesoryViewが自動レイアウト制約から適切なサイズになるために必要です
    override var intrinsicContentSize: CGSize {
        return CGSize.zero
    }
}
```

###UIView Extension
制約を容易にする為にメソッドを作成しました。

```swift:UIView+.swift
import UIKit

extension UIView {
    
    func anchor(top: NSLayoutYAxisAnchor? = nil,
                left: NSLayoutXAxisAnchor? = nil,
                bottom: NSLayoutYAxisAnchor? = nil,
                right: NSLayoutXAxisAnchor? = nil,
                paddingTop: CGFloat = 0,
                paddingLeft: CGFloat = 0,
                paddingBottom: CGFloat = 0,
                paddingRight: CGFloat = 0,
                width: CGFloat? = nil,
                height: CGFloat? = nil) {
        
        translatesAutoresizingMaskIntoConstraints = false
        
        if let top = top {
            topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
        }
        
        if let left = left {
            leadingAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
        }
        
        if let bottom = bottom {
            bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
        }
        
        if let right = right {
            trailingAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
        }
        
        if let width = width {
            widthAnchor.constraint(equalToConstant: width).isActive = true
        }
        
        if let height = height {
            heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}
```

##苦戦したところ
SafeAreaの部分に作成したContainerViewが被って表示され、意図した結果がなかなか出なかった。

`textFeild.bottomAnchor`を
`inputContainerView.layoutMarginsGuide.bottomAnchor`に指定するのがミソでした。

##参考
こちらの記事を参考にし、解決することが出来ました。
<a href="https://stackoverflow.com/questions/46282987/iphone-x-how-to-handle-view-controller-inputaccessoryview">iPhone X how to handle View Controller inputAccessoryView</a>

<br>
何か間違いやより良い方法がありましたら優しく教えていただけると幸いです🙇‍♂️