MessageViewControllerの画面をタップしてキーボードを閉じる方法(MessageKit)

# はじめに
どうも@kaneko77です。
今回は業務でMessagekitを使っていたときに画面タップした際にキーボードを閉じる方法がネットに全く情報がなかった為共有したいと思います。
3日かかってこの解に行きついた為、お役に立てましたら是非いいねください😄
テキストフィールドなど適当な画面タップしたらキーボード閉じるの当たり前だと思うのでぜひ実装してみてください。

# キーボードを消すにはどうするのか？
以下がキーボードを閉じるコードになります。
`resignFirstResponder`については[こちら](https://developer.apple.com/documentation/uikit/uiresponder/1621097-resignfirstresponder)を参照

```swift
self.messageInputBar.inputTextView.resignFirstResponder()
```

# 各々のタップイベントを取得していく
次にタップイベントを取得する必要があります。
MessegaKitではタップイベントが専用のコールバックメソッドで用意されています。(セルのみ)

## タップイベントの一覧
こちらを対象に各々のタップイベントを説明します。
ヒエラルキーで絞って表示してます。
一応[リファレンス](https://messagekit.github.io/Protocols/MessageCellDelegate.html)も貼っておきます。（リファレンスなんか古い...）
github[参照](https://github.com/MessageKit/MessageKit/blob/master/Sources/Protocols/MessageCellDelegate.swift)してください
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/496e7b5b-a1d3-ccb5-1db3-6237bc0bfdc3.png" width="500px">
下記のメソッドは`MessageCellDelegate`を継承する必要があります。

|  関数名  |  タップ位置  | 画像 |
| ---- | ---- | ---- |
|  didTapBackground  | バックグラウンド  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/f85f05ea-b582-2b3e-0e62-61a3035a3d27.png" width="500px">  |
|  didTapMessage  |  メッセージ  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/37a23818-3ac4-fab0-c9b0-08c8ac466c06.png" width="500px">  |
|  didTapAvatar  |  アバター  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/78fb33df-e8fc-fd34-df40-e35d63cda2fc.png" width="500px">  |
|  didTapMessageBottomLabel  |  メッセージの下部  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/f442cf2c-ad09-fb42-7817-406e8a81607e.png" width="500px">  |
|  didTapMessageTopLabel  |  メッセージの上部  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/d9c9ef6a-2539-d8f1-f458-41b07cf7efd1.png" width="500px">  |

これでセルに関してのタッチイベントは網羅しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/2e05ff90-5d87-3858-1cfa-17ebcc7a43dc.png" width="500px">

## バックグラウンドのタップ
セルのバックグラウンドとは別のバックグラウンドになります。
実は`UICollectionView`をチャットは継承していまして、
コレクションのバックグラウンドも叩けるようになる必要があります。
キーボードを表示するときに隙間ができたりセルの数が３個未満だと多分バックグランドが剥き出して表示されるはず..
とういうことでバックグラウンドタップのタップジェスチャーを入れます。

###　結論
説明が長いので先に結論を述べます。
普通だったらViewControllerのViewにジェスチャーつけてはい終わりですがそうはいかない理由があります(※後で説明)
`MessagesCollectionView`をカスタムで定義してタップジェスチャーつけてキーボードを閉じる機能を実現します。
それでは説明いきます。

### 説明
先にヒエラルキー
`UIColletionView`が`ChatMessagesCollectionView`という名前で登録されています。
ヒエラルキー上で見るとわかりやすいかな。。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/9f8e6b24-52c2-1abc-9fb4-c9bfe9a6ce78.png" width="800px">

まず順番に説明させてください。
MessagesViewControllerを継承すると`messagesCollectionView`が使えるようになりそいつにセルを貼り付けてChatでのゴニョゴニョが可能になります。

```swift
class ChatViewController: MessagesViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        messagesCollectionView
    }
}

```

`messagesCollectionView`を見ると、、、はい先ほどヒエラルキーで見た`ChatMessagesCollectionView`が出てきました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397376/0846b4b9-96ec-f221-7c3c-640204f5f0ee.png" width="800px">

それで`messagesCollectionView`はすでにジェスチャー機能がデフォルトで付けられているため最初にお話ししたカスタムで改めてジェスチャー機能がついているクラスを作り
`messagesCollectionView`に代入してあげる必要があります。

### コード
以下がカスタムクラスです。
こちらに`Gesture`つきました。
[こちら](https://github.com/MessageKit/MessageKit/issues/1491)参考にしました。

```swift
protocol MessagesCollectionViewDelegate: AnyObject {
    func didTap()
}

class ChatMessagesCollectionView: MessagesCollectionView {
    weak var collectionDelegate: MessagesCollectionViewDelegate?
    
    override func handleTapGesture(_ gesture: UIGestureRecognizer) {
        super.handleTapGesture(gesture)
        collectionDelegate?.didTap()
    }
}
```

先ほどのクラスを...
代入します。

```swift
override func loadView() {
    super.loadView()
    let collectionView = ChatMessagesCollectionView()
    collectionView.collectionDelegate = self
    messagesCollectionView = collectionView
}
```

これでバックグラウンドタップが実現できるようになりました。

# 全コード貼っちゃおう✨
[こちら](https://qiita.com/kaneko77/items/e28b538e154ad452ec2e)に肉付けして書いてます。

```swift
import UIKit
import MessageKit
import InputBarAccessoryView

class ChatViewController: MessagesViewController {
    
    var messageList: [MockMessage] = [] {
        didSet {
            // messagesCollectionViewをリロード
            self.messagesCollectionView.reloadData()
            // 一番下までスクロールする
            self.messagesCollectionView.scrollToLastItem()
        }
    }
    
    lazy var formatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.locale = Locale(identifier: "ja_JP")
        return formatter
    }()
    
    override func loadView() {
        super.loadView()
        let collectionView = ChatMessagesCollectionView()
        collectionView.collectionDelegate = self
        messagesCollectionView = collectionView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // モックデータを取得
        DispatchQueue.main.async { self.messageList = MockMessage.getMessages() }
        messagesCollectionView.messagesDataSource = self
        messagesCollectionView.messagesLayoutDelegate = self
        messagesCollectionView.messagesDisplayDelegate = self
        messagesCollectionView.messageCellDelegate = self
        messageInputBar.delegate = self
        
        setupInput()
        setupButton()
        // 背景の色を指定
        messagesCollectionView.backgroundColor = .white
        
        // メッセージ入力時に一番下までスクロール
        scrollsToLastItemOnKeyboardBeginsEditing = true
        maintainPositionOnKeyboardFrameChanged = true
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    private func setupInput(){
        // プレースホルダーの指定
        messageInputBar.inputTextView.placeholder = "入力"
        // 入力欄のカーソルの色を指定
        messageInputBar.inputTextView.tintColor = .red
        // 入力欄の色を指定
        messageInputBar.inputTextView.backgroundColor = .gray
    }
    
    private func setupButton(){
        // ボタンの変更
        messageInputBar.sendButton.title = "送信"
        // 送信ボタンの色を指定
        messageInputBar.sendButton.tintColor = .lightGray
    }
}

// MARK: - MessagesDataSource
extension ChatViewController: MessagesDataSource {
    func currentSender() -> SenderType {
        return userType.me.data
    }
    
    func otherSender() -> SenderType {
        return userType.you.data
    }
    
    func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
        return messageList.count
    }
    
    func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
        return messageList[indexPath.section]
    }
    
    // メッセージの上に文字を表示
    func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        if indexPath.section % 3 == 0 {
            return NSAttributedString(
                string: MessageKitDateFormatter.shared.string(from: message.sentDate),
                attributes: [
                    .font: UIFont.boldSystemFont(ofSize: 10),
                    .foregroundColor: UIColor.darkGray
                ]
            )
        }
        return nil
    }
    
    // メッセージの上に文字を表示（名前）
    func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        let name = message.sender.displayName
        return NSAttributedString(string: name, attributes: [.font: UIFont.preferredFont(forTextStyle: .caption1)])
    }
    
    // メッセージの下に文字を表示（日付）
    func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        let dateString = formatter.string(from: message.sentDate)
        return NSAttributedString(string: dateString, attributes: [.font: UIFont.preferredFont(forTextStyle: .caption2)])
    }
}

// MARK: - MessagesDisplayDelegate
extension ChatViewController: MessagesDisplayDelegate {
    
    // メッセージの色を変更（デフォルトは自分：白、相手：黒）
    func textColor(
        for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView
    ) -> UIColor {
        isFromCurrentSender(message: message) ? .white : .darkText
    }
    
    // メッセージの背景色を変更している（デフォルトは自分：緑、相手：グレー）
    func backgroundColor(
        for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView
    ) -> UIColor {
        isFromCurrentSender(message: message) ? .darkGray : .cyan
    }
    
    // メッセージの枠にしっぽを付ける
    func messageStyle(
        for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView
    ) -> MessageStyle {
        let corner: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
        return .bubbleTail(corner, .curved)
    }
    
    // アイコンをセット
    func configureAvatarView(
        _ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView
    ) {
        avatarView.set( avatar: Avatar(initials: message.sender.senderId == "001" ? "😊" : "🥳") )
    }
}


// 各ラベルの高さを設定（デフォルト0なので必須）
// MARK: - MessagesLayoutDelegate
extension ChatViewController: MessagesLayoutDelegate {
    
    func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        indexPath.section % 3 == 0 ? 10 : 0
    }
    
    func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        16
    }
    
    func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        16
    }
}

// MARK: - MessageCellDelegate
extension ChatViewController: MessageCellDelegate {
    
    //MARK: - Cellのバックグラウンドをタップした時の処理
    func didTapBackground(in cell: MessageCollectionViewCell) {
        print("バックグラウンドタップ")
        closeKeyboard()
    }

    //MARK: - メッセージをタップした時の処理
    func didTapMessage(in cell: MessageCollectionViewCell) {
        print("メッセージタップ")
        closeKeyboard()
    }

    //MARK: - アバターをタップした時の処理
    func didTapAvatar(in cell: MessageCollectionViewCell) {
        print("アバタータップ")
        closeKeyboard()
    }

    //MARK: - メッセージ上部をタップした時の処理
    func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {
        print("メッセージ上部タップ")
        closeKeyboard()
    }

    //MARK: - メッセージ下部をタップした時の処理
    func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {
        print("メッセージ下部タップ")
        closeKeyboard()
    }
}

// MARK: - InputBarAccessoryViewDelegate
extension ChatViewController: InputBarAccessoryViewDelegate {
    // 送信ボタンをタップした時の挙動
    func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
        let attributedText = NSAttributedString(
            string: text, attributes: [.font: UIFont.systemFont(ofSize: 15), .foregroundColor: UIColor.white])
        let message = MockMessage(attributedText: attributedText, sender: currentSender(), messageId: UUID().uuidString, date: Date())
        self.messageList.append(message)
        
        self.messageInputBar.inputTextView.text = String()
        self.messageInputBar.invalidatePlugins()
        self.messagesCollectionView.scrollToLastItem()
    }
    
}

extension ChatViewController {
    func closeKeyboard(){
        self.messageInputBar.inputTextView.resignFirstResponder()
        self.messagesCollectionView.scrollToLastItem()
    }
}

extension ChatViewController: MessagesCollectionViewDelegate {
    func didTap() {
        closeKeyboard()
    }
}

protocol MessagesCollectionViewDelegate: AnyObject {
    func didTap()
}

class ChatMessagesCollectionView: MessagesCollectionView {
    weak var collectionDelegate: MessagesCollectionViewDelegate?
    
    override func handleTapGesture(_ gesture: UIGestureRecognizer) {
        super.handleTapGesture(gesture)
        collectionDelegate?.didTap()
    }
}
```

# 終わりに
今回はめちゃくちゃ詰まりまくった世の中にあって当たり前の機能、画面適当タップでキーボードをMessageKitでやる方法でした。
デフォルトのジェスチャー機能を消してほしいと切実に実装中思ってました。
ずっとIssue見てて思ったのがライブラリ開発って大変なんだなと思いました。
ここまで見ていただきありがとうございます。
これからは皆さんググれば私の記事に行き着くと思うので、私みたいに時間を食う人がかなり減るはず!!!
皆さんのお役に立てますと幸いです。