iOSの MessageKit によるチャットUIの作成
iOSの「MessageKit」によるチャットUIの作成を試したので、まとめました。
1. MessageKit
「MessageKit」は、iOSでチャットUIを簡単に作成できるパッケージです。
2. MessageType
「MessageType」は、「MessageKit」のメッセージを定義するデータ型です。
次の4つのプロパティを持っています。
2-1. SenderType
「SenderType」は、送信者を定義するデータ型です。
次の2つのプロパティを持っています。
2-2. MessageKind
「MessageKind」は、メッセージ種別を定義するデータ型数です。
次の8種類のメッセージ種別があります。
3. MessagesViewController
「MessagesViewController」は、「MessageKit」のUIを表示するビューコントローラです。
「MessageKit」を利用するには、「MessagesViewController」のサブクラスで、次の4つのプロトコルを実装する必要があります。
class ChatViewController: MessagesViewController {
override func viewDidLoad() {
super.viewDidLoad()
// messagesCollectionView
messagesCollectionView.backgroundColor = UIColor.secondarySystemBackground
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
// messageInputBar
messageInputBar.delegate = self
messageInputBar.sendButton.title = nil
messageInputBar.sendButton.image = UIImage(systemName: "paperplane")
}
}
3-1. MessagesDataSource
「MessagesDataSource」は、「MessagesCollectionView」のデータソースを設定するためのプロトコルです。
実装必須のメソッドは、次の3つです。
public struct Sender: SenderType {
public let senderId: String
public let displayName: String
}
// 例のためにグローバル変数を使っています
let sender = Sender(senderId: "any_unique_id", displayName: "Steven")
let messages: [MessageType] = []
extension ChatViewController: MessagesDataSource {
// 現在の送信者
var currentSender: SenderType {
return Sender(senderId: "any_unique_id", displayName: "Steven")
}
// メッセージ数
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return messages.count
}
// IndexPathに応じたメッセージ
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messages[indexPath.section]
}
}
messageForItem()で、従来のindexPath.rowではなく、indexPath.sectionでメッセージ (MessageType) を取得しています。これは、「MessageKit」ではMessageTypeをMessagesCollectionViewの独自セクションに配置するためです。
「MessagesDataSource」のメソッド一覧は、次のとおりです。
var currentSender: SenderType { get }
現在の送信者 (必須)
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType
IndexPathに応じたメッセージ (必須)
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int
MessagesCollectionViewに表示されるセクション数 (必須)
func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int
MessagesCollectionViewに表示されるセル数
func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
messageTopLabelの属性テキスト
func messageBottomLabelAttributedText(メッセージ用: MessageType, at indexPath: IndexPath) -> NSAttributedString?
messageBottomLabelの属性テキスト
func cellTopLabelAttributedText(メッセージ用: MessageType, at indexPath: IndexPath) -> NSAttributedString?
cellTopLabelの属性テキスト
func cellBottomLabelAttributedText(メッセージ用: MessageType, at indexPath: IndexPath) -> NSAttributedString?
cellBottomLabelの属性テキスト
func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
messageTimestampLabelの属性テキスト
func textCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell?
「text」「attributedText」「emoji」のメッセージセル
func photoCell(for message: MessageType、indexPath: IndexPath、messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell?
「photo」「video」のメッセージセル
func locationCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell?
「location」のメッセージセル
func audioCell(メッセージ用: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell?
「audio」のメッセージセル
func contactCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell?
「contact」のメッセージセル
func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
「custom」のメッセージセル
func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
タイピングインジケータセル
func isFromCurrentSender(message: MessageType) -> Bool
メッセージが現在の送信者のものであるかどうかを判定
デフォルトセル (MessageContentCell) のUI構成は、次のとおりです。
上から順に、次のパーツが配置されてます。
そして両側に、次のパーツが配置されてます。
3-2. MessagesLayoutDelegate
「MessagesLayoutDelegate」は、「MessagesCollectionView」のレイアウトを設定するためのプロトコルです。
実装必須のメソッドはありません。
「MessagesLayoutDelegate」のメソッド一覧は、次のとおりです。
func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
ヘッダービューのサイズ
func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
フッタービューのサイズ
func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize
タイピングインジケータビューのサイズ
func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat
タイピングインジケータビューのトップインセット
func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
messageTopLabelの高さ
func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
messageBottomLabelの高さ
func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
messageTopLabelのの高さ
func messageTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment?
messageTopLabelの配置
func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
messageBottomLabelのの高さ
func messageBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment?
messageBottomLabelの配置
func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize?
avatarViewのサイズ
func textCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「text」のメッセージセルのサイズカリキュレータ
func attributedTextCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「attributedText」のメッセージセルのサイズカリキュレータ
func emojiCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「emoji」のメッセージセルのサイズカリキュレータ
func photoCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「photo」のメッセージセルのサイズカリキュレータ
func videoCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「video」のメッセージセルのサイズカリキュレータ
func locationCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「location」のメッセージセルのサイズカリキュレータ
func audioCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「audio」のメッセージセルのサイズカリキュレータ
func contactCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator?
「contact」のメッセージセルのサイズカリキュレータ
func customCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator
「custom」のメッセージセルのサイズカリキュレータ
3-3. MessagesDisplayDelegate
「MessagesDisplayDelegate」は、「MessagesCollectionView」のビューを設定するためのプロトコルです。
実装必須のメソッドはありません。
「MessagesDisplayDelegate」のメソッド一覧は、次のとおりです。
func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle
メッセージの背景スタイル (MessageStyle)
func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
MessagesCollectionViewの背景色
func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
IndexPathに応じたセクションヘッダ
func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
IndexPathに応じたセクションフッタ
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
AvatarView
func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
AccessoryView
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
セルのテキスト色
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType]
DetectorType
func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any]
DetectorTypeの属性
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions
「location」のSnapshotOption
func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView?
「location」のAnotationView
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)?
「location」のAnimationBlock
func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
「photo」「video」ImageView
func configureAudioCell(_ cell: AudioMessageCell, message: MessageType)
「audio」のAudioMessageCell
func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
プログレスバー色
func audioProgressTextFormat(_ duration: Float, for audioCell: AudioMessageCell, in messageCollectionView: MessagesCollectionView) -> String
オーディオサウンドの持続時間の設定
func configureLinkPreviewImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
LinkPreviewMessageCellのImageView
3-4. InputBarAccessoryViewDelegate
「InputBarAccessoryViewDelegate」は、入力バーを設定するためのプロトコルです。
実装必須のメソッドはありません。
「InputBarAccessoryViewDelegate」のメソッド一覧は、次のとおりです。
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String)
InputBarAccessoryViewの送信ボタン押下時に呼ばれる
func inputBar(_ inputBar: InputBarAccessoryView, didChangeIntrinsicContentTo size: CGSize)
InputBarAccessoryViewのInstrinsicContentSize変更時に呼ばれる
func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String)
InputBarAccessoryViewのInputTextViewのテキスト変更時に呼ばれる
func inputBar(_ inputBar: InputBarAccessoryView, didSwipeTextViewWith gesture: UISwipeGestureRecognizer)
InputBarAccessoryViewのInputTextViewでスワイプジェスチャ認識時に呼ばれる
入力バー (InputBarAccessoryView) のUI構成は、次のとおりです。
4. Xcodeへのパッケージの追加
Xcode14では、「Package Dependencies」からパッケージを追加します。
(1) 「PROJECT」の「Package Dependencies」の「+」を押す。
(2) 右上のテキストボックスに以下のURLを入力し、「MessageKit」を選択し、「Add Package」ボタンを押す。
https://github.com/MessageKit/MessageKit
5. Xcodeへのアセットの追加
今回は、アイコンとして使う2つのアセット画像 (cat と bear)を追加します。
6. コードの編集
ViewControllerを以下のように編集します。
import UIKit
import MessageKit
import InputBarAccessoryView
// 送信者
struct ChatSender: SenderType {
var senderId: String // 送信者ID
var displayName: String // 表示名
var iconName: String // アイコン名
// 自分のSenderType
static var me: ChatSender {
return ChatSender(senderId: "0", displayName: "me", iconName: "cat")
}
// 他人のSenderType
static var other: ChatSender {
return ChatSender(senderId: "1", displayName: "other", iconName: "bear")
}
}
// メッセージ
struct ChatMessage: MessageType {
var sender: SenderType // 送信者
var messageId: String // メッセージID
var kind: MessageKind // メッセージ種別
var sentDate: Date // 送信日時
// メッセージの生成
static func new(sender: SenderType, message: String) -> ChatMessage {
return ChatMessage(
sender: sender,
messageId: UUID().uuidString,
kind: .attributedText(NSAttributedString(
string: message,
attributes: [
.font: UIFont.systemFont(ofSize: 14.0),
.foregroundColor: sender.senderId == "0" ? UIColor.white : UIColor.label
]
)),
sentDate: Date())
}
}
// ViewController
final class ViewController: MessagesViewController {
// メッセージリスト
private var messageList: [ChatMessage] = [] {
// メッセージ設定時に呼ばれる
didSet {
messagesCollectionView.reloadData()
messagesCollectionView.scrollToLastItem(at: .bottom, animated: true)
}
}
// ビューロード時に呼ばれる
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.async {
// メッセージリストの初期化
self.messageList = [
ChatMessage.new(sender: ChatSender.me, message: "こんにちは。"),
ChatMessage.new(sender: ChatSender.other, message: "はい、こんにちは。"),
ChatMessage.new(sender: ChatSender.me, message: "はい、今日は良い天気ですね!"),
ChatMessage.new(sender: ChatSender.other, message: "今日は良い天気ですね!")
]
}
// messagesCollectionView
messagesCollectionView.backgroundColor = UIColor.secondarySystemBackground
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
// messageInputBar
messageInputBar.delegate = self
messageInputBar.sendButton.title = nil
messageInputBar.sendButton.image = UIImage(systemName: "paperplane")
}
}
// MessagesDataSource
extension ViewController: MessagesDataSource {
// 現在の送信者
var currentSender: SenderType {
return ChatSender.me
}
// メッセージ数
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return messageList.count
}
// IndexPathに応じたメッセージ
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messageList[indexPath.section]
}
// messageTopLabelの属性テキスト
func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
return NSAttributedString(
string: messageList[indexPath.section].sender.displayName,
attributes: [.font: UIFont.systemFont(ofSize: 12.0), .foregroundColor: UIColor.systemBlue])
}
// messageBottomLabelの属性テキスト
func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = DateFormatter.dateFormat(
fromTemplate: "HH:mm", options: 0, locale: Locale(identifier: "ja_JP"))
return NSAttributedString(
string: dateFormatter.string(from: messageList[indexPath.section].sentDate),
attributes: [.font: UIFont.systemFont(ofSize: 12.0), .foregroundColor: UIColor.secondaryLabel])
}
}
// MessagesDisplayDelegate
extension ViewController: MessagesDisplayDelegate {
// 背景色
func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
return isFromCurrentSender(message: message) ? UIColor.systemBlue : UIColor.systemBackground
}
// メッセージスタイル
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)
}
// avaterViewの設定
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
let sender = messageList[indexPath.section].sender as! ChatSender
avatarView.image = UIImage(named: sender.iconName)
}
}
// MessagesLayoutDelegate
extension ViewController: MessagesLayoutDelegate {
// messageTopLabelの高さ
func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
return 24
}
// messageBottomLabelの高さ
func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
return 24
}
// headerViewのサイズ
func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize {
return CGSize.zero
}
}
// InputBarAccessoryViewDelegate
extension ViewController: InputBarAccessoryViewDelegate {
// InputBarAccessoryViewの送信ボタン押下時に呼ばれる
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
messageList.append(ChatMessage.new(sender: ChatSender.me, message: text))
messageInputBar.inputTextView.text = String()
}
}
7. 実行
実行すると、以下のようなチャットUIが表示されます。
参考
次回
この記事が気に入ったらサポートをしてみませんか?