MessageKitをカスタマイズして日付のセルを表示する


# やりたいこと
- いい感じのチャットのUIを作りたい
- scroll to bottomの動きや、伸縮するキーボードなど、大変そうなのでライブラリを使いたい
  - [MessageKit](https://github.com/MessageKit/MessageKit)を使う
  - 用意されている以外のUIコンポーネントを自作して使用したい

作りたいのは下のような画面で、アバターの描画、カスタムセルの作り方などにハマったので書いていきます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/8da17ed4-eca9-6c9a-d49c-166c389c5acc.png" width="40%">

画像

# トピック
- 基本的な使い方
- アイコン部分の細かい調整
- 自作セルを使って日付のセルを追加する

# 基本的な使い方
メッセージ部分のカラー設定やマージンの設定については割愛します。
基本的な使い方については下記を参考にさせていただきました。

- [MessageKit - README.md](https://github.com/MessageKit/MessageKit)
- [MessageKitの使いかた（初歩）](https://qiita.com/shoboo/items/bb3dd56a696c46054297)
- [【Swift】JSQMessagesViewController後継のMessageKitでのチャット画面の作成方法](https://www.macky-studio.com/entry/2019/05/14/214413#MessageKit%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)

## アイコン部分の細かい調整

<img src="https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/CellStructure.png" width="80%">

メッセージ部分の構造は画像（READMEより抜粋）のようになっていて、赤枠がviewの構成単位になっています。
各Viewを適宜レイアウト調整用のメソッドを呼んだり、Delegateメソッド経由で調整することでレイアウトを組んでいきます。
アイコン部分は`AvaterView`と呼ばれており、他のview要素と同じ用に上記のアプローチで細かくレイアウトを設定していくことになります。

今回のアイコン部分の実装では以下を要件とします

- 自分が送るときはアイコンなし
- 相手から来たメッセージはアイコンあり（48 x 48、画像の上辺と吹き出しの上辺を合わせる）
- アイコンには角丸+影をつけたい

### 自分が送るときはアイコンなし

```Swift
layout.setMessageOutgoingAvatarSize(.zero)
```

上記のメソッドからアバターサイズを`.zero`で設定します。
自分から送る（出ていく = Outgoing）アバターサイズを設定するメソッド、
という感じの命名になっていて、他のコンポーネントも同じノリです。

ちなみにlayoutプロパティは下記のように、
`messageCollectionView`から取得できる`collectionViewLayout`から呼び出すことができます。
（`messageCollectionView`とは`MessagesViewController`のクラスが保持しているプロパティです）

```Swift
class ChatViewController: MessagesViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        if let layout = self.messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout {
            // ここでlayoutを取得できる
        }
    }
}
```


### 相手から来たメッセージはアイコンあり
```Swift
layout.setMessageIncomingAvatarSize(CGSize(width: 48, height: 48))
layout.setMessageIncomingAvatarPosition(AvatarPosition(vertical: .cellTop))
```

同じノリで、相手から受け取った（送られてくる = Incoming）メッセージも上記のように設定できます。
`AvatarPosition`は水平方向、垂直方向に対してenumが定義されていて、
垂直方向のみによるinitと、水平方向と垂直方向によるinitの2種類提供されています。

### アイコンには影をつけたい
```Swift:ChatViewController.swift
// MARK: - MessagesLayoutDelegate
// バブルの部分的な描画処理や、アクセサリービューやアバタービューの描画のときに呼ばれるDelegate
// ここでaddSubViewすることでいい感じにレイアウトを組む
extension ChatViewController: MessagesDisplayDelegate {    
    func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
        // 初期化
        avatarView.subviews.forEach {
            $0.removeFromSuperview()
        }
        
        // 相手のみアイコンを描画
        if !self.isFromCurrentSender(message: message) {
            // subViewにシャドーをつけるためにfalseにしておく
            avatarView.clipsToBounds = false

            // avatarViewにUserIconViewをaddする
            let userIconView = UserIconView()
            let avaterSize = avatarView.frame.size
            avatarView.addSubview(userIconView)
        }
    }
}
```

AvatarViewの画像などの設定は下記のdelegateを通して設定します。

AvatarViewには`open func set(avatar: Avatar)`のメソッドが公開されており、
このメソッド経由で画像を設定するべきですが、
角丸 + 影を付ける場合にはシャドー付きのviewとcornerRadiusを設定したviewを重ねて表示する必要があるため、
今回はAvaterViewには画像を設定せず、
直接角丸 + 影付きの自作のUserIconViewをaddSubViewすることで実現しました。

また、描画の際に毎回呼ばれるので、セルの再利用で影が再描画され濃くなっていかないように、
初期化して`removeFromSuperview()`をしていく必要があります。

## 自作セルを使って日付のセルを追加する
基本的なチャットに必要なコンポーネントはMessageKitで提供してくれており、
メッセージ用のview要素も、テキスト用の他に[画像や絵文字、音声にも対応したUI](https://github.com/MessageKit/MessageKit/blob/master/Sources/Models/MessageKind.swift)が利用できるみたいです。

ですが、やはりどうしても賄えない部分は存在するので、その場合はカスタムセルを作ってなんとかします。
そしてその方法もしっかりドキュメントに記されていて、この[方法の通り](https://github.com/MessageKit/MessageKit/issues/1458)やればカスタムセルを実装できます。

流れは以下のとおりです
1. `UICollectionViewCell`を継承した、カスタムセルを作る
2. `MessageSizeCalculator`を継承したサイズ計算用のクラスを作る
3. `MessagesCollectionViewFlowLayout`を継承した、レイアウトを決定クラスを作る
4. `viewDidLoad`で`MessagesCollectionViewFlowLayout`を使ったcollectionViewの初期化と、セルのregisterを行う

### 1. `UICollectionViewCell`を継承した、カスタムセルを作る


```Swift:DateCustomCell.swift
final class DateCustomCell: UICollectionViewCell {
    func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView, date: String) {
        // 描画処理
    }
}
```

### 2. `MessageSizeCalculator`を継承したサイズ計算用のクラスを作る

```Swift:DateMessageSizeCalculator.swift
final class DateMessageSizeCalculator: MessageSizeCalculator {
    override func messageContainerSize(for message: MessageType) -> CGSize {
        return CGSize(width: 124, height: 24)
    }
}
```

### 3. `MessagesCollectionViewFlowLayout`を継承した、レイアウトを決定クラスを作る

```Swift:DateMessagesFlowLayout.swift
final class DateMessagesFlowLayout: MessagesCollectionViewFlowLayout {
    lazy var customMessageSizeCalculator = DateMessageSizeCalculator(layout: self)

    override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
        let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
        if case .custom = message.kind {
            return customMessageSizeCalculator // カスタムの時は独自のcalculatorを使用
        }
        return super.cellSizeCalculatorForItem(at: indexPath) // その他の時は親のcalculatorを使用
    }
}
```

この処理の中で`MessageKind`がカスタムのときに 2. で作成したサイズ計算用クラスを使用するように指定する

### 4. `viewDidLoad`で`MessagesCollectionViewFlowLayout`を使ったcollectionViewの初期化と、セルのregisterを行う

```Swift:ChatViewController.swift
final class ChatViewController: MessagesViewController {
    
    // MARK: Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: DateMessagesFlowLayout()) // カスタムセルの時にも処理できるlayoutクラスをセットする
        self.messagesCollectionView.register(DateCustomCell.self) // カスタムセルを登録
    }
}

```


## 最後に
既にご存知だった方もいるかとは思いますが、
スクラッチだとかなり大変なチャットUIの大部分を提供してくれてるMessageKitむちゃくちゃありがたいです。
若干リサーチしつつ実装する必要は出てくるものの、Repository内のExampleもしっかり作られていて参考にできますし、
2021年現在しっかりメンテされているので、チャットをこれから実装しようという方は検討してはいかがでしょうか。