It's interesting.

いつまでも中二病。

【Swift】JSQMessagesViewController後継のMessageKitでのチャット画面の作成方法

はじめに

Swiftでチャットアプリを作ろうと思い、どんなライブラリが主流なのかな?と思って調べていると、これまではJSQMessagesViewControllerが一般的だったらしいですが、すでに開発が終了しており、現在は非推奨となっていることが分かりました。
そして、JSQMessagesViewControllerの後継?のMessageKitなるものを見つけました。

使い方を解説してくれているサイトが少なかったのですが、以下のサイトが非常に分かりやすかったです。

www.raywenderlich.com

ちなみに全部英語です(笑)
英語がある程度できれば問題なく読めますが、英語ってだけで挫折しそう!って方もいると思うので、そんな方たちのためにこの記事ではMessageKitの使い方をかみ砕いて日本語で説明していこうと思います!
Firebase Tutorial: Real-time ChatMessageKitのサンプルプロジェクトを合わせて見るとより理解できると思います。

環境

macOS Mojave 10.14.3, XCode 10.2.1, iOS 12.2, Swift 4.2.1, MessageKit 2.0.0

MessageKitのインストール

以下の手順でCocoaPodsを使用してライブラリをインストールします。

$ cd プロジェクト名
$ pod init
$ vim Podfile
$ pod install

Podfileには以下の内容を記述します。

# Pods for FirebaseChat
pod 'MessageKit'
pod 'MessageInputBar' // ver2.0.0から必要

スポンサーリンク

MessageKitの使い方

MessageKitはUICollectionViewControllerの強化版みたいな感じなので、UICollectionViewControllerの使い方が分かっていれば割と簡単です。
MessagesViewControllerクラスを継承するだけでチャット画面を作れます。
後は必要なプロトコルを実装していくって感じです。
メッセージはMessageTypeに準拠したMessageという構造体を自分で用意する必要があるんですが、これはMessageKitのサンプルプロジェクト内のMockMessageを利用すれば問題ありません。

class ChatViewController: MessagesViewController {

    private var messages: [Message] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        messagesCollectionView.messagesDataSource = self
        messagesCollectionView.messageCellDelegate = self
        messagesCollectionView.messagesLayoutDelegate = self
        messagesCollectionView.messagesDisplayDelegate = self
        messagesCollectionView.messageCellDelegate = self
        messageInputBar.delegate = self
    }
}

メッセージの送信やアバターの設定といったカスタマイズは以下の6つのプロトコルを使って行います。

  • MessagesDataSource
  • MessageInputBarDelegate
  • MessagesDisplayDelegate
  • MessagesLayoutDelegate
  • MessageCellDelegate
  • MessageLabelDelegate

それぞれについて簡単に説明していきます。 設定項目は多いですが、テキストメッセージの送受信さえとりあえずできればOKという場合は、 MessagesDataSourceMessageInputBarDelegateのみ実装すれば大丈夫です。

MessagesDataSource

メッセージの送信者や画面に表示するメッセージの数などの設定ができます。
全部で9つのメソッドがありますが、必ず実装しないといけないのはcurrentSendermessageForItem, numberOfSectionsの3つです。
~AttributedTextではメッセージやセルの上下に表示する文字列を設定できます。

extension ChatViewController: MessagesDataSource {

    // idとdisplayNameをプロパティに持つ構造体Senderを返す
    func currentSender() -> Sender {
       return Sender(id: "1", displayName: "Tom")
    }

    func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
        return messages.count
    }

    func messageForItem(at indexPath: IndexPath, 
        in messagesCollectionView: MessagesCollectionView) -> MessageType {

        return messages[indexPath.section]
    }

    func cellTopLabelAttributedText(for message: MessageType, 
        at indexPath: IndexPath) -> NSAttributedString? {

        let name = message.sender.displayName
        return NSAttributedString(
            string: name,
            attributes: [
                .font: UIFont.preferredFont(forTextStyle: .caption1),
                .foregroundColor: UIColor(white: 0.3, alpha: 1)
            ]
        )
    }
}

MessageInputBarDelegate

メッセージ入力バーに関する処理を実装するものです。
ほぼ確実に実装することになるのは、送信ボタンを押した時の処理を表すmessageInputBarメソッドだと思います。
以下の例は、送信ボタンを押すと、入力された文字列からMessageオブジェクトを作成して、メッセージリストに追加し、メッセージ入力バーを空にして、画面の下までスクロールするというものです。
insertMessage(message)もしくは、それに値するメソッドは自分で作る必要があります。
そのメソッドの中でメッセージデータをローカルに保存するなり、データベースに保存するなりします。 firebaseを利用する場合の処理と、チャット画面全体をリロードして、送信したメッセージを表示する処理もここで行います。

extension ChatViewController: MessageInputBarDelegate {
    
    func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {      
        let message = Message(text: text, sender: currentSender(), messageId: UUID().uuidString, date: Date())
        insertMessage(message)
        inputBar.inputTextView.text = String()
        messagesCollectionView.scrollToBottom(animated: true)
    }
    
}

MessagesDisplayDelegate

メッセージの表示形式やデザインを決めるものです。
テキストメッセージのみに反映される設定や全てのメッセージに反映される設定等いろいろあるので、MessageKitのサンプルプロジェクトを参考にするといいと思います。
以下の例では、相手のメッセージが自分のメッセージかに応じてメッセージの背景色を変えて、メッセージスタイルを吹き出しにしています。
isFromCurrentSender()がそのメッセージが自分が送信したのもかどうかを判定するメソッドです。
.primary.incomingMessageは自作の色なので、ここは各自好きな色にしてください。

extension ChatViewController: MessagesDisplayDelegate {
  
    func backgroundColor(for message: MessageType, at indexPath: IndexPath, 
        in messagesCollectionView: MessagesCollectionView) -> UIColor {

        return isFromCurrentSender(message: message) ? .primary : .incomingMessage
    }

    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)
    }
}

MessagesLayoutDelegate

このプロトコルはメッセージ周辺のラベルのレイアウトを決めるものです。
メッセージ周辺のラベルというのは、メッセージ付近に表示される時間や既読、日付といったものです。
それらの位置や大きさはここで設定できます。
メッセージの下に余白を作るといったこともここで行います。

extension ChatViewController: MessagesLayoutDelegate {
    
    func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, 
        in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return 18
    }
    
    func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, 
        in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return 20
    }
    
    func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, 
        in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return 16
    }
}

MessageCellDelegate

チャット画面で様々なものをタップした時の処理を実装できます。
以下は、アバター(自分のプロフ画像が表示される所)とメッセージがタップされた時の処理の例です。
アバターとメッセージをタップするとコンソールにそれぞれ文字列が出力されます。

extension ChatViewController: MessageCellDelegate {
    
    func didTapAvatar(in cell: MessageCollectionViewCell) {
        print("Avatar tapped")
    }
    
    func didTapMessage(in cell: MessageCollectionViewCell) {
        print("Message tapped")
    }
    
}

MessageLabelDelegate

メッセージ中の電話番号や住所、URLなどをタップした時の処理を実装できます。
URLをタップしたらブラウザを開く、住所をタップしたらマップを起動するといった処理はここで実装します。
以下は例なので、コンソールに文字列を出力するだけです。

extension ChatViewController: MessageLabelDelegate {
    
    func didSelectAddress(_ addressComponents: [String: String]) {
        print("Address Selected: \(addressComponents)")
    }
    
    func didSelectPhoneNumber(_ phoneNumber: String) {
        print("Phone Number Selected: \(phoneNumber)")
    }
    
    func didSelectURL(_ url: URL) {
        print("URL Selected: \(url)")
    }
    
}

おわりに

さらに詳しく知りたい方はMessageKit Referenceをご覧ください。
今回の内容だけだとチャットアプリにはならないので、次はFirebaseとも組み合わせた実装例についても記事を書きたいと思います!