自分のために備忘録として記載してます。
この記事でやること
- MessageKitを使用したチャット画面の設計・チャット機能の実装
- inputBarAccessoryViewのカスタマイズ
- 送信ボタンと送信予定ボタンの設置
環境
- Xcode 15.0.1
- Swift 5
前提
- MessageKitがインストールされていること
- 下記まで完了していること
実装方法
Model
チャットアプリケーションにおけるデータモデルを定義します。それぞれの構造体 (struct) は、アプリ内で使用されるデータの構造と動作を表しています。
import Foundation
import FirebaseFirestoreSwift
import MessageKit
struct Chat: Codable {
var id: String
var name: String
var participants: [String]
func toDictionary() -> [String: Any] {
return [
"id": id,
"name": name,
"participants": participants
]
}
}
struct User: Codable {
var id: String
var name: String
}
struct Message: Codable, MessageType {
var id: String
var senderId: String
var senderName: String
var text: String
var timestamp: Date
var sender: SenderType {
return Sender(senderId: senderId, displayName: senderName)
}
var messageId: String {
return id
}
var sentDate: Date {
return timestamp
}
var kind: MessageKind {
return .text(text)
}
func toDictionary() -> [String: Any] {
return [
"id": id,
"senderId": senderId,
"senderName": senderName,
"text": text,
"timestamp": timestamp
]
}
}
struct Sender: SenderType {
var senderId: String
var displayName: String
}
ChatViewController
全量
import UIKit
import MessageKit
import InputBarAccessoryView
import FirebaseFirestore
import FirebaseAuth
class ChatViewController: MessagesViewController {
// MARK: - Properties
var chat: Chat?
var messages: [Message] = []
var messageListener: ListenerRegistration?
let db = Firestore.firestore()
var currentUsername: String = "Unknown"
// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupMessageCollectionView()
setupInputBar()
addKeyboardObservers()
fetchCurrentUser()
fetchMessages()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
messageListener?.remove()
tabBarController?.tabBar.isHidden = false
removeKeyboardObservers()
}
// MARK: - Setup Methods
func setupNavigationBar() {
navigationItem.title = chat?.name
}
func setupMessageCollectionView() {
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
messagesCollectionView.addGestureRecognizer(tapGesture)
}
func setupInputBar() {
messageInputBar.delegate = self
messageInputBar.inputTextView.placeholder = "メッセージを入力"
messageInputBar.separatorLine.isHidden = true
let sendButton = createSendButton()
let sendLaterButton = createSendLaterButton()
let bottomStackView = InputStackView(arrangedSubviews: [sendLaterButton, sendButton])
bottomStackView.axis = .horizontal
bottomStackView.alignment = .center
bottomStackView.distribution = .fillProportionally
bottomStackView.spacing = 8
messageInputBar.bottomStackView.addArrangedSubview(bottomStackView)
messageInputBar.setStackViewItems([], forStack: .right, animated: false)
messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false)
}
func createSendButton() -> InputBarButtonItem {
let sendButton = InputBarButtonItem()
sendButton.setTitle("送信", for: .normal)
sendButton.setSize(CGSize(width: 60, height: 40), animated: false)
sendButton.onTouchUpInside { [weak self] _ in
if let text = self?.messageInputBar.inputTextView.text, !text.isEmpty {
self?.sendMessage(text: text)
self?.messageInputBar.inputTextView.text = ""
}
}
return sendButton
}
func createSendLaterButton() -> InputBarButtonItem {
let sendLaterButton = InputBarButtonItem()
sendLaterButton.setTitle("送信予定", for: .normal)
sendLaterButton.setSize(CGSize(width: 80, height: 40), animated: false)
sendLaterButton.onTouchUpInside { [weak self] _ in
self?.showDatePicker()
}
return sendLaterButton
}
func addKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
func removeKeyboardObservers() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
// MARK: - Fetch Methods
func fetchCurrentUser() {
guard let userId = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(userId).getDocument { (document, error) in
if let document = document, document.exists {
self.currentUsername = document.data()?["username"] as? String ?? "Unknown"
} else {
print("User does not exist")
}
}
}
func fetchMessages() {
guard let chatId = chat?.id else { return }
messageListener = db.collection("chats").document(chatId).collection("messages")
.order(by: "timestamp", descending: false)
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let self = self else { return }
if let error = error {
print("Error getting messages: \(error)")
return
}
let now = Date()
self.messages = querySnapshot?.documents.compactMap { document in
if let message = try? document.data(as: Message.self) {
return message.timestamp <= now ? message : nil
}
return nil
} ?? []
self.messagesCollectionView.reloadData()
self.scrollToBottom(animated: false)
}
}
// MARK: - Helper Methods
func sendMessage(text: String, scheduledDate: Date? = nil) {
guard let chatId = chat?.id, let userId = Auth.auth().currentUser?.uid else { return }
let timestamp = scheduledDate ?? Date()
let message = Message(id: UUID().uuidString, senderId: userId, senderName: currentUsername, text: text, timestamp: timestamp)
db.collection("chats").document(chatId).collection("messages").document(message.id).setData(message.toDictionary()) { error in
if let error = error {
print("Error sending message: \(error)")
}
}
if timestamp <= Date() {
messages.append(message)
messagesCollectionView.reloadData()
scrollToBottom(animated: true)
}
}
func scrollToBottom(animated: Bool) {
guard !messages.isEmpty else { return }
let lastMessageIndex = IndexPath(item: 0, section: messages.count - 1)
messagesCollectionView.scrollToItem(at: lastMessageIndex, at: .bottom, animated: animated)
}
@objc func keyboardWillShow(notification: NSNotification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardHeight = keyboardFrame.cgRectValue.height
UIView.animate(withDuration: 0.3) {
self.view.frame.origin.y = -keyboardHeight
}
}
}
@objc func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.3) {
self.view.frame.origin.y = 0
}
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
// MARK: - DatePicker
func showDatePicker() {
let alert = UIAlertController(title: "送信予定時間を選択", message: nil, preferredStyle: .actionSheet)
let datePicker = UIDatePicker()
datePicker.datePickerMode = .dateAndTime
datePicker.preferredDatePickerStyle = .wheels
let height: NSLayoutConstraint = NSLayoutConstraint(item: alert.view!, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 330)
alert.view.addConstraint(height)
alert.view.addSubview(datePicker)
datePicker.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
datePicker.centerXAnchor.constraint(equalTo: alert.view.centerXAnchor),
datePicker.bottomAnchor.constraint(equalTo: alert.view.bottomAnchor, constant: -100),
datePicker.widthAnchor.constraint(equalTo: alert.view.widthAnchor),
datePicker.heightAnchor.constraint(equalToConstant: 200)
])
alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: "設定", style: .default, handler: { [weak self] _ in
let scheduledDate = datePicker.date
if let text = self?.messageInputBar.inputTextView.text, !text.isEmpty {
self?.sendMessage(text: text, scheduledDate: scheduledDate)
self?.messageInputBar.inputTextView.text = ""
}
}))
present(alert, animated: true, completion: nil)
}
}
// MARK: - Date Extension
extension Date {
func timeAgoDisplay() -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.second, .minute, .hour, .day, .weekOfMonth]
formatter.maximumUnitCount = 1
formatter.unitsStyle = .abbreviated
return formatter.string(from: self, to: Date()) ?? ""
}
}
// MARK: - MessageKit Protocols
extension ChatViewController: MessagesDataSource, MessagesDisplayDelegate, MessagesLayoutDelegate {
var currentSender: MessageKit.SenderType {
guard let userId = Auth.auth().currentUser?.uid else {
return Sender(senderId: "unknown", displayName: "Unknown")
}
return Sender(senderId: userId, displayName: currentUsername)
}
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? {
return NSAttributedString(
string: message.sentDate.timeAgoDisplay(),
attributes: [.font: UIFont.preferredFont(forTextStyle: .caption1)]
)
}
func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
return NSAttributedString(
string: message.sender.displayName,
attributes: [.font: UIFont.preferredFont(forTextStyle: .caption1)]
)
}
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
return isFromCurrentSender(message: message) ? .white : .darkText
}
func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key : Any] {
switch detector {
case .hashtag, .mention: return [.foregroundColor: UIColor.blue]
default: return MessageLabel.defaultAttributes
}
}
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
}
func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
return .bubbleTail(tail, .curved)
}
func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
return 0
}
func cellBottomLabelHeight(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 20
}
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
let sender = message.sender
let initials = sender.displayName.components(separatedBy: " ").compactMap({ $0.first }).map({ String($0) }).joined()
avatarView.set(avatar: Avatar(initials: initials))
}
}
// MARK: - InputBarAccessoryViewDelegate
extension ChatViewController: InputBarAccessoryViewDelegate {
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
sendMessage(text: text)
inputBar.inputTextView.text = ""
}
}
インポートセクション
import UIKit
import MessageKit
import InputBarAccessoryView
import FirebaseFirestore
import FirebaseAuth
- UIKit: iOSアプリのUIコンポーネントを使用するためのフレームワーク。
- MessageKit: チャットUIを構築するためのライブラリ。
- InputBarAccessoryView: メッセージ入力バーのカスタマイズを容易にするライブラリ。
- FirebaseFirestore: FirebaseのNoSQLクラウドデータベースであるFirestoreを使用するためのライブラリ。
- FirebaseAuth: Firebase Authenticationを使用してユーザー認証を行うためのライブラリ。
プロパティの定義
class ChatViewController: MessagesViewController {
var chat: Chat?
var messages: [Message] = []
var messageListener: ListenerRegistration?
let db = Firestore.firestore()
var currentUsername: String = "Unknown"
- chat: 現在のチャットの情報を格納するプロパティ。
- messages: 取得したメッセージを格納する配列。
- messageListener: Firestoreのリアルタイムリスナー。
- db: Firestoreデータベースのインスタンス。
- currentUsername: 現在のユーザー名を格納するプロパティ。
ライフサイクルメソッド
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupMessageCollectionView()
setupInputBar()
addKeyboardObservers()
fetchCurrentUser()
fetchMessages()
}
- viewDidLoad: ビューがロードされたときに呼ばれるメソッド。ここでは、UIのセットアップやFirestoreからのデータ取得、キーボードの表示に関するオブザーバーの追加などを行っています。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
messageListener?.remove()
tabBarController?.tabBar.isHidden = false
removeKeyboardObservers()
}
- viewWillAppear: 画面が表示される直前に呼ばれるメソッド。タブバーを非表示にしています。(※タブバーを使用していない場合は必要なし)
- viewWillDisappear: 画面が非表示になる直前に呼ばれるメソッド。Firestoreリスナーの解除や、キーボードのオブザーバーを削除しています。
UIセットアップメソッド
func setupNavigationBar() {
navigationItem.title = chat?.name
}
func setupMessageCollectionView() {
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
messagesCollectionView.addGestureRecognizer(tapGesture)
}
- setupNavigationBar: ナビゲーションバーのタイトルを設定します。
- setupMessageCollectionView: メッセージコレクションビューのデータソース、レイアウトデリゲート、ディスプレイデリゲートを設定し、タップジェスチャーを追加してキーボードを閉じる機能を実装しています。
func setupInputBar() {
messageInputBar.delegate = self
messageInputBar.inputTextView.placeholder = "メッセージを入力"
messageInputBar.separatorLine.isHidden = true
let sendButton = createSendButton()
let sendLaterButton = createSendLaterButton()
let bottomStackView = InputStackView(arrangedSubviews: [sendLaterButton, sendButton])
bottomStackView.axis = .horizontal
bottomStackView.alignment = .center
bottomStackView.distribution = .fillProportionally
bottomStackView.spacing = 8
messageInputBar.bottomStackView.addArrangedSubview(bottomStackView)
messageInputBar.setStackViewItems([], forStack: .right, animated: false)
messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false)
}
- setupInputBar: メッセージ入力バーのセットアップを行います。送信ボタンと送信予定ボタンを作成し、それを入力バーの下部に配置します。
メッセージ送信ボタンの作成メソッド
func createSendButton() -> InputBarButtonItem {
let sendButton = InputBarButtonItem()
sendButton.setTitle("送信", for: .normal)
sendButton.setSize(CGSize(width: 60, height: 40), animated: false)
sendButton.onTouchUpInside { [weak self] _ in
if let text = self?.messageInputBar.inputTextView.text, !text.isEmpty {
self?.sendMessage(text: text)
self?.messageInputBar.inputTextView.text = ""
}
}
return sendButton
}
func createSendLaterButton() -> InputBarButtonItem {
let sendLaterButton = InputBarButtonItem()
sendLaterButton.setTitle("送信予定", for: .normal)
sendLaterButton.setSize(CGSize(width: 80, height: 40), animated: false)
sendLaterButton.onTouchUpInside { [weak self] _ in
self?.showDatePicker()
}
return sendLaterButton
}
- createSendButton: 送信ボタンを作成し、タップ時にメッセージを送信する処理を設定します。
- createSendLaterButton: 送信予定ボタンを作成し、タップ時に日付ピッカーを表示する処理を設定します。
キーボード表示/非表示の処理
func addKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
func removeKeyboardObservers() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
- addKeyboardObservers: キーボードの表示と非表示を監視するオブザーバーを追加します。
- removeKeyboardObservers: キーボードのオブザーバーを解除します。
Firestoreからのデータ取得メソッド
func fetchCurrentUser() {
guard let userId = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(userId).getDocument { (document, error) in
if let document = document, document.exists {
self.currentUsername = document.data()?["username"] as? String ?? "Unknown"
} else {
print("User does not exist")
}
}
}
func fetchMessages() {
guard let chatId = chat?.id else { return }
messageListener = db.collection("chats").document(chatId).collection("messages")
.order(by: "timestamp", descending: false)
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let self = self else { return }
if let error = error {
print("Error getting messages: \(error)")
return
}
let now = Date()
self.messages = querySnapshot?.documents.compactMap { document in
if let message = try? document.data(as: Message.self) {
return message.timestamp <= now ? message : nil
}
return nil
} ?? []
self.messagesCollectionView.reloadData()
self.scrollToBottom(animated: false)
}
}
- fetchCurrentUser: Firestoreから現在のユーザー情報を取得します。
- fetchMessages: Firestoreから現在のチャットに関連するメッセージを取得し、表示するための処理です。リスナーを使ってリアルタイムでデータを取得し、現在時刻より未来のメッセージは除外します。
メッセージ送信メソッド
func sendMessage(text: String, scheduledDate: Date? = nil) {
guard let chatId = chat?.id, let userId = Auth.auth().currentUser?.uid else { return }
let timestamp = scheduledDate ?? Date()
let message = Message(id: UUID().uuidString, senderId: userId, senderName: currentUsername, text: text, timestamp: timestamp)
db.collection("chats").document(chatId).collection("messages").document(message.id).setData(message.toDictionary()) { error in
if let error = error {
print("Error sending message: \(error)")
}
}
if timestamp <= Date() {
messages.append(message)
messagesCollectionView.reloadData()
scrollToBottom(animated: true)
}
}
- sendMessage: メッセージをFirestoreに保存します。もし送信予定時刻が現在時刻より未来でなければ、メッセージを表示します。
メッセージ表示を調整するメソッド
func scrollToBottom(animated: Bool) {
guard !messages.isEmpty else { return }
let lastMessageIndex = IndexPath(item: 0, section: messages.count - 1)
messagesCollectionView.scrollToItem(at: lastMessageIndex, at: .bottom, animated: animated)
}
- scrollToBottom: 新しいメッセージが表示されたときにメッセージリストの最後までスクロールします。
キーボード表示時の処理
@objc func keyboardWillShow(notification: NSNotification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardHeight = keyboardFrame.cgRectValue.height
UIView.animate(withDuration: 0.3) {
self.view.frame.origin.y = -keyboardHeight
}
}
}
@objc func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.3) {
self.view.frame.origin.y = 0
}
}
- keyboardWillShow: キーボードが表示された際に、ビュー全体を上に移動させて隠れないようにします。
- keyboardWillHide: キーボードが隠れるときに、ビュー全体を元の位置に戻します。
キーボードを閉じる処理
@objc func dismissKeyboard() {
view.endEditing(true)
}
- dismissKeyboard: ビューをタップしたときにキーボードを閉じます。
DatePickerの表示メソッド
func showDatePicker() {
let alert = UIAlertController(title: "送信予定時間を選択", message: nil, preferredStyle: .actionSheet)
let datePicker = UIDatePicker()
datePicker.datePickerMode = .dateAndTime
datePicker.preferredDatePickerStyle = .wheels
let height: NSLayoutConstraint = NSLayoutConstraint(item: alert.view!, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 330)
alert.view.addConstraint(height)
alert.view.addSubview(datePicker)
datePicker.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
datePicker.centerXAnchor.constraint(equalTo: alert.view.centerXAnchor),
datePicker.bottomAnchor.constraint(equalTo: alert.view.bottomAnchor, constant: -100),
datePicker.widthAnchor.constraint(equalTo: alert.view.widthAnchor),
datePicker.heightAnchor.constraint(equalToConstant: 200)
])
alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: "設定", style: .default, handler: { [weak self] _ in
let scheduledDate = datePicker.date
if let text = self?.messageInputBar.inputTextView.text, !text.isEmpty {
self?.sendMessage(text: text, scheduledDate: scheduledDate)
self?.messageInputBar.inputTextView.text = ""
}
}))
present(alert, animated: true, completion: nil)
}
- showDatePicker: メッセージの送信予定時間を選択するためのDatePickerを表示します。選択された時間を使用して、メッセージを将来送信するようにスケジュールします。
Date拡張メソッド
extension Date {
func timeAgoDisplay() -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.second, .minute, .hour, .day, .weekOfMonth]
formatter.maximumUnitCount = 1
formatter.unitsStyle = .abbreviated
return formatter.string(from: self, to: Date()) ?? ""
}
}
- timeAgoDisplay: 日時を「何分前」「何時間前」のような形式で表示するためのメソッドです。
MessageKitのプロトコル実装
extension ChatViewController: MessagesDataSource, MessagesDisplayDelegate, MessagesLayoutDelegate {
var currentSender: MessageKit.SenderType {
guard let userId = Auth.auth().currentUser?.uid else {
return Sender(senderId: "unknown", displayName: "Unknown")
}
return Sender(senderId: userId, displayName: currentUsername)
}
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return messages.count
}
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messages[indexPath.section]
}
func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
return NSAttributedString(
string: message.sender.displayName,
attributes: [.font: UIFont.preferredFont(forTextStyle: .caption1)]
)
}
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
return isFromCurrentSender(message: message) ? .white : .darkText
}
func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key : Any] {
switch detector {
case .hashtag, .mention: return [.foregroundColor: UIColor.blue]
default: return MessageLabel.defaultAttributes
}
}
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
}
func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
return .bubbleTail(tail, .curved)
}
func cellBottomLabelHeight(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 20
}
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
let sender = message.sender
let initials = sender.displayName.components(separatedBy: " ").compactMap({ $0.first }).map({ String($0) }).joined()
avatarView.set(avatar: Avatar(initials: initials))
}
}
- currentSender: 現在のユーザーを取得します。
- numberOfSections: メッセージのセクション数(ここではメッセージの数)を返します。
- messageForItem: 各インデックスに対応するメッセージを返します。
- messageTopLabelAttributedText: メッセージの送信者名を設定します。
- textColor: メッセージのテキスト色を設定します。
- detectorAttributes: メッセージ内のハッシュタグやメンションのスタイルを設定します。
- enabledDetectors: メッセージ内で検出する項目を設定します。
- messageStyle: メッセージのスタイルを設定します。
- cellBottomLabelHeight: メッセージの下部ラベルの高さを設定します。
- messageBottomLabelHeight: メッセージの下部ラベルの高さを設定します。
- configureAvatarView: メッセージの送信者のアバターを設定します。
InputBarAccessoryViewDelegateの実装
extension ChatViewController: InputBarAccessoryViewDelegate {
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
sendMessage(text: text)
inputBar.inputTextView.text = ""
}
}
- inputBar(_:didPressSendButtonWith:): 送信ボタンが押されたときに呼ばれるメソッドで、メッセージを送信し、入力フィールドをクリアします。
改修が必要な点
この記事では時間指定してチャットを送信できる機能を実装しているが、トリガーがfetchMessages()もしくはfirestoreのデータ変更時のみである。
そのため、指定した時間より前から画面を開いたまま待っていて、かつ他ユーザーもチャット送信しなかった場合、指定した時間になってもチャットは表示されない。
なので、完璧にこの機能を実装しようとした場合、別のトリガーを設定する必要がある。
例えば、FirestoreのFunctionsを利用して、サーバー側で送信予定時刻にメッセージが表示されるようにすることも考えられる。