自分のために備忘録として記載してます。
この記事でやること
- Firestoreを使用したデータ管理
- MessageKitを使用した指定時間にメッセージを表示する画面の設計と機能の実装
環境
- Xcode 15.0.1
- Swift 5
前提
- Firebaseプロジェクトが作成されていること
- iOSアプリがFirebaseプロジェクトに追加されていること
- Fitebase SDK とMessageKit SDK がインストールされていること
- AppDelegate.swift または SceneDelegate.swift でFirebaseが初期化されていること
実装方法
StoryBoardの設定
以下2つの画面を設計する
-
ChatControllerView
メッセージを送信する、かつ指定時間にメッセージを表示する画面 -
MessagesTableViewController
送信したメッセージと各メッセージの送信時間・指定時間を表示する画面
ChatControllerView
- IdentityInspectorのclassを設定する
- EmbedInからTabBarControllerを選択する
MessagesTableViewController
- IdentityInspectorのclassを設定する
- LibraryからTableViewを選択して画面全体に設定する
- LibraryからTableViewCellを選択してTableViewに設定する
- TableViewCellを選択し、AttributesInspectorのIdentifierを"MessageCell"に設定する
- TableViewCellを選択し、AttributesInspectorのStyleを"Subtitle"に設定する
- EmbedInからTabBarControllerを選択する
Model.swift
概要
Firestoreから取得したデータを扱うためのMessage構造体を定義しており、メッセージの内容、スケジュールされた時間、作成時間、表示フラグなどの情報を管理します。また、MessageKitライブラリのMessageTypeプロトコルに準拠することで、チャットアプリで使用するためのプロパティやメソッドを実装しています。
全量
import Foundation
import FirebaseFirestore
import MessageKit
struct Message: Identifiable, Codable, MessageType {
@DocumentID var id: String? // FirestoreのドキュメントID
var messageContent: String
var scheduledTime: Date
var createdTime: Date
var isDisplayed: Bool
// MessageTypeプロトコルに必要なプロパティ
var sender: SenderType {
return Sender(senderId: "anon", displayName: "Anonymous")
}
var messageId: String {
return id ?? UUID().uuidString
}
var sentDate: Date {
return createdTime
}
var kind: MessageKind {
return .text(messageContent)
}
// イニシャライザを定義
init(messageContent: String, scheduledTime: Date, createdTime: Date, isDisplayed: Bool) {
self.messageContent = messageContent
self.scheduledTime = scheduledTime
self.createdTime = createdTime
self.isDisplayed = isDisplayed
}
// Firestoreからデータを取得するためのイニシャライザ
init?(document: DocumentSnapshot) {
guard let data = document.data(),
let messageContent = data["messageContent"] as? String,
let scheduledTime = (data["scheduledTime"] as? Timestamp)?.dateValue(),
let createdTime = (data["createdTime"] as? Timestamp)?.dateValue(),
let isDisplayed = data["isDisplayed"] as? Bool else {
return nil
}
self.id = document.documentID
self.messageContent = messageContent
self.scheduledTime = scheduledTime
self.createdTime = createdTime
self.isDisplayed = isDisplayed
}
// Firestoreにデータを保存するためのディクショナリを生成
func toDictionary() -> [String: Any] {
return [
"messageContent": messageContent,
"scheduledTime": Timestamp(date: scheduledTime),
"createdTime": Timestamp(date: createdTime),
"isDisplayed": isDisplayed
]
}
}
struct Sender: SenderType {
var senderId: String
var displayName: String
}
各行の役割
import Foundation
import FirebaseFirestore
import MessageKit
- import Foundation: 基本的なデータ型やコレクションなどを提供するFoundationフレームワークをインポートします。
- import FirebaseFirestore: Firebase Firestoreを使用するためのモジュールをインポートします。
- import MessageKit: チャットインターフェースを構築するためのMessageKitライブラリをインポートします。
struct Message: Identifiable, Codable, MessageType {
- struct Message: メッセージを表す構造体を定義しています。
- Identifiable: SwiftUIでアイテムを一意に識別するためのプロトコルです。
- Codable: データのエンコードおよびデコード(シリアライズ/デシリアライズ)を可能にするプロトコルです。
- MessageType: MessageKitが提供するプロトコルで、チャットメッセージとして機能するために必要なプロパティを定義します。
@DocumentID var id: String? // FirestoreのドキュメントID
var messageContent: String
var scheduledTime: Date
var createdTime: Date
var isDisplayed: Bool
- @DocumentID var id: String?: FirestoreのドキュメントIDを保持するためのプロパティ。Firestoreが自動的にドキュメントIDを割り当てるため、@DocumentID属性を使用しています。
- var messageContent: String: メッセージの内容を保持します。
- var scheduledTime: Date: メッセージを表示する予定の日時を保持します。
- var createdTime: Date: メッセージが作成された日時を保持します。
- var isDisplayed: Bool: メッセージが表示されたかどうかを示すフラグです。
var sender: SenderType {
return Sender(senderId: "anon", displayName: "Anonymous")
}
var messageId: String {
return id ?? UUID().uuidString
}
var sentDate: Date {
return createdTime
}
var kind: MessageKind {
return .text(messageContent)
}
- var sender: SenderType: MessageTypeプロトコルに必要なプロパティで、メッセージの送信者を定義します。ここでは固定の送信者("anon"というIDと"Anonymous"という名前)を返します。
- var messageId: String: MessageTypeプロトコルに必要なプロパティで、メッセージのIDを返します。FirestoreのドキュメントIDを使用し、ない場合は新しいUUIDを生成します。
- var sentDate: Date: メッセージが送信された日時を返します。ここでは作成日時を返しています。
- var kind: MessageKind: MessageKitが使用するメッセージの種類(テキスト、画像など)を定義します。ここではテキストメッセージとしてmessageContentを返します。
init(messageContent: String, scheduledTime: Date, createdTime: Date, isDisplayed: Bool) {
self.messageContent = messageContent
self.scheduledTime = scheduledTime
self.createdTime = createdTime
self.isDisplayed = isDisplayed
}
- init: 構造体の初期化メソッドを定義しています。Messageオブジェクトを生成する際に、メッセージ内容、スケジュール時間、作成時間、表示フラグを設定します。
init?(document: DocumentSnapshot) {
guard let data = document.data(),
let messageContent = data["messageContent"] as? String,
let scheduledTime = (data["scheduledTime"] as? Timestamp)?.dateValue(),
let createdTime = (data["createdTime"] as? Timestamp)?.dateValue(),
let isDisplayed = data["isDisplayed"] as? Bool else {
return nil
}
self.id = document.documentID
self.messageContent = messageContent
self.scheduledTime = scheduledTime
self.createdTime = createdTime
self.isDisplayed = isDisplayed
}
- init?(document: DocumentSnapshot): Firestoreから取得したデータを使ってMessageオブジェクトを初期化するためのメソッドです。Firestoreのドキュメントを受け取り、必要なデータがすべて揃っている場合にのみオブジェクトを生成します。
func toDictionary() -> [String: Any] {
return [
"messageContent": messageContent,
"scheduledTime": Timestamp(date: scheduledTime),
"createdTime": Timestamp(date: createdTime),
"isDisplayed": isDisplayed
]
}
- func toDictionary() -> [String: Any]: MessageオブジェクトをFirestoreに保存するために、プロパティを辞書形式([String: Any])に変換するメソッドです。Date型のデータはFirestoreのTimestamp型に変換されます。
struct Sender: SenderType {
var senderId: String
var displayName: String
}
- struct Sender: SenderTypeプロトコルに準拠した構造体で、メッセージの送信者を表します。senderIdとdisplayNameをプロパティとして持ちます。
ChatControllerView.swift
概要
- MessageKitの設定: チャットUIの設定とメッセージの表示。
- メッセージの受信と表示: Firestoreからメッセージデータを取得し、リアルタイムでチャット画面に表示。
- メッセージの送信: 送信ボタンを押すと、日時を指定するためのDatePickerを表示し、Firestoreにメッセージを保存。
- メッセージの表示状態の更新: メッセージが指定された時間に達した場合、そのメッセージが表示されたことをFirestoreに更新。
全量
import UIKit
import MessageKit
import InputBarAccessoryView
import Firebase
import FirebaseFirestore
class ChatViewController: MessagesViewController, MessagesDataSource, MessagesLayoutDelegate, MessagesDisplayDelegate, InputBarAccessoryViewDelegate {
// MARK: - Properties
private var messages: [Message] = []
private var messageListener: ListenerRegistration?
// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
configureMessageKit()
listenForMessages()
}
deinit {
messageListener?.remove()
}
// MARK: - MessageKit Configuration
private func configureMessageKit() {
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
messageInputBar.delegate = self
}
var currentSender: SenderType {
return Sender(senderId: "anon", displayName: "Anonymous")
}
// MARK: - MessagesDataSource
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return messages.count
}
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messages[indexPath.section]
}
// MARK: - InputBarAccessoryViewDelegate
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
showDatePicker(messageContent: text)
}
// MARK: - Firestore Handling
private func listenForMessages() {
let db = Firestore.firestore()
messageListener = db.collection("messages").order(by: "createdTime").addSnapshotListener { snapshot, error in
if let error = error {
print("Error fetching messages: \(error)")
return
}
guard let documents = snapshot?.documents else {
print("No documents")
return
}
self.processDocuments(documents)
}
}
private func processDocuments(_ documents: [QueryDocumentSnapshot]) {
var newMessages: [Message] = []
for document in documents {
if var message = Message(document: document) {
if !message.isDisplayed {
updateMessageIfNeeded(&message)
}
if message.isDisplayed {
newMessages.append(message)
}
}
}
self.messages = newMessages
DispatchQueue.main.async {
self.messagesCollectionView.reloadData()
self.messagesCollectionView.scrollToLastItem(animated: true)
}
}
private func updateMessageIfNeeded(_ message: inout Message) {
if Date() >= message.scheduledTime {
message.isDisplayed = true
updateMessageIsDisplayed(message: message)
}
}
private func updateMessageIsDisplayed(message: Message) {
let db = Firestore.firestore()
if let messageId = message.id {
db.collection("messages").document(messageId).updateData(["isDisplayed": true]) { error in
if let error = error {
print("Error updating message: \(error)")
}
}
}
}
private func saveMessageToFirestore(messageContent: String, scheduledTime: Date) {
let db = Firestore.firestore()
let newMessage = Message(
messageContent: messageContent,
scheduledTime: scheduledTime,
createdTime: Date(),
isDisplayed: false
)
db.collection("messages").addDocument(data: newMessage.toDictionary()) { error in
if let error = error {
print("Error saving message: \(error)")
} else {
print("Message successfully saved!")
}
}
}
// MARK: - DatePicker Handling
private func showDatePicker(messageContent: String) {
let datePicker = createDatePicker()
let alertController = createDatePickerAlertController(datePicker: datePicker, messageContent: messageContent)
present(alertController, animated: true, completion: nil)
}
private func createDatePicker() -> UIDatePicker {
let datePicker = UIDatePicker()
datePicker.datePickerMode = .dateAndTime
datePicker.preferredDatePickerStyle = .wheels
return datePicker
}
private func createDatePickerAlertController(datePicker: UIDatePicker, messageContent: String) -> UIAlertController {
let alertController = UIAlertController(title: "時間を指定", message: nil, preferredStyle: .actionSheet)
alertController.view.addSubview(datePicker)
// AutoLayoutを設定
datePicker.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
datePicker.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 30),
datePicker.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),
alertController.view.heightAnchor.constraint(equalToConstant: 350)
])
let setAction = UIAlertAction(title: "設定", style: .default) { _ in
self.saveMessageToFirestore(messageContent: messageContent, scheduledTime: datePicker.date)
}
alertController.addAction(setAction)
alertController.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
return alertController
}
}
各行の役割
import UIKit
import MessageKit
import InputBarAccessoryView
import Firebase
import FirebaseFirestore
- 必要なフレームワークとライブラリをインポートしています。UIKitは基本的なUI要素、MessageKitとInputBarAccessoryViewはチャットインターフェースの構築、FirebaseとFirebaseFirestoreはFirestoreとのやり取りに使用されます。
swift
class ChatViewController: MessagesViewController, MessagesDataSource, MessagesLayoutDelegate, MessagesDisplayDelegate, InputBarAccessoryViewDelegate {
- ChatViewControllerは、MessagesViewControllerを継承し、各プロトコルに準拠することで、メッセージのデータソース、レイアウト、表示、入力バーのデリゲートを担当します。
private var messages: [Message] = []
private var messageListener: ListenerRegistration?
- messagesはチャットで表示するメッセージの配列。
- messageListenerはFirestoreからのリアルタイム更新を監視するリスナーの登録を保持します。
override func viewDidLoad() {
super.viewDidLoad()
configureMessageKit()
listenForMessages()
}
deinit {
messageListener?.remove()
}
- viewDidLoadはビューがロードされたときに呼ばれ、MessageKitの設定とFirestoreからのメッセージリスニングを開始します。
- deinitは、コントローラが解放されるときに呼ばれ、リスナーを解除してメモリリークを防ぎます。
private func configureMessageKit() {
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
messageInputBar.delegate = self
}
- MessageKitの設定を行い、データソース、レイアウト、表示のデリゲート、および入力バーのデリゲートをこのクラスに設定しています。
var currentSender: SenderType {
return Sender(senderId: "anon", displayName: "Anonymous")
}
- 現在のメッセージの送信者を返します。この例では固定値ですが、通常はログインユーザーの情報を返すようにします。
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return messages.count
}
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messages[indexPath.section]
}
- MessagesDataSourceプロトコルに必要なメソッドで、メッセージの数と各メッセージを返します。
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
showDatePicker(messageContent: text)
}
- 送信ボタンが押されたときに呼ばれ、DatePickerを表示してメッセージの送信日時を指定させます。
private func listenForMessages() {
let db = Firestore.firestore()
messageListener = db.collection("messages").order(by: "createdTime").addSnapshotListener { snapshot, error in
if let error = error {
print("Error fetching messages: \(error)")
return
}
guard let documents = snapshot?.documents else {
print("No documents")
return
}
self.processDocuments(documents)
}
}
- Firestoreのmessagesコレクションを監視し、メッセージデータの更新があるたびに処理を行います。
private func processDocuments(_ documents: [QueryDocumentSnapshot]) {
var newMessages: [Message] = []
for document in documents {
if var message = Message(document: document) {
if !message.isDisplayed {
updateMessageIfNeeded(&message)
}
if message.isDisplayed {
newMessages.append(message)
}
}
}
self.messages = newMessages
DispatchQueue.main.async {
self.messagesCollectionView.reloadData()
self.messagesCollectionView.scrollToLastItem(animated: true)
}
}
- Firestoreから取得したドキュメントをメッセージオブジェクトに変換し、表示されるメッセージのリストを更新します。メッセージが表示可能であれば追加します。
private func updateMessageIfNeeded(_ message: inout Message) {
if Date() >= message.scheduledTime {
message.isDisplayed = true
updateMessageIsDisplayed(message: message)
}
}
private func updateMessageIsDisplayed(message: Message) {
let db = Firestore.firestore()
if let messageId = message.id {
db.collection("messages").document(messageId).updateData(["isDisplayed": true]) { error in
if let error = error {
print("Error updating message: \(error)")
}
}
}
}
- メッセージが指定された時間に達している場合、isDisplayedフラグをtrueにしてFirestoreを更新します。
private func saveMessageToFirestore(messageContent: String, scheduledTime: Date) {
let db = Firestore.firestore()
let newMessage = Message(
messageContent: messageContent,
scheduledTime: scheduledTime,
createdTime: Date(),
isDisplayed: false
)
db.collection("messages").addDocument(data: newMessage.toDictionary()) { error in
if let error = error {
print("Error saving message: \(error)")
} else {
print("Message successfully saved!")
}
}
}
- 新しいメッセージをFirestoreに保存するメソッドです。メッセージの内容と指定された時間で新しいメッセージを作成し、Firestoreのmessagesコレクションに追加します。
private func showDatePicker(messageContent: String) {
let datePicker = createDatePicker()
let alertController = createDatePickerAlertController(datePicker: datePicker, messageContent: messageContent)
present(alertController, animated: true, completion: nil)
}
private func createDatePicker() -> UIDatePicker {
let datePicker = UIDatePicker()
datePicker.datePickerMode = .dateAndTime
datePicker.preferredDatePickerStyle = .wheels
return datePicker
}
private func createDatePickerAlertController(datePicker: UIDatePicker, messageContent: String) -> UIAlertController {
let alertController = UIAlertController(title: "時間を指定", message: nil, preferredStyle: .actionSheet)
alertController.view.addSubview(datePicker)
// AutoLayoutを設定
datePicker.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
datePicker.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 30),
datePicker.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),
alertController.view.heightAnchor.constraint(equalToConstant: 350)
])
let setAction = UIAlertAction(title: "設定", style: .default) { _ in
self.saveMessageToFirestore(messageContent: messageContent, scheduledTime: datePicker.date)
}
alertController.addAction(setAction)
alertController.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
return alertController
}
- DatePickerを表示し、メッセージの送信時間をユーザーに指定させるためのメソッド群です。ユーザーが時間を選択して「設定」を押すと、その情報を使ってFirestoreにメッセージが保存されます。
MessagesTableViewController.swift
概要
- Firebase Firestoreからのメッセージ取得: アプリがFirestoreからメッセージデータを取得し、テーブルビューに表示します。
- テーブルビューの設定と表示: メッセージデータをテーブルビューに表示するための設定とデリゲートメソッドを実装しています。
全量
import UIKit
import Firebase
import FirebaseFirestore
class MessagesTableViewController: UIViewController {
// MARK: - Properties
@IBOutlet weak var tableView: UITableView!
private var messages: [Message] = []
// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
fetchMessages()
}
// MARK: - Configuration
private func configureTableView() {
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MessageCell")
}
// MARK: - Firestore Fetching
private func fetchMessages() {
let db = Firestore.firestore()
db.collection("messages").order(by: "createdTime", descending: false).getDocuments { [weak self] snapshot, error in
if let error = error {
print("Error fetching messages: \(error)")
return
}
guard let documents = snapshot?.documents else {
print("No documents found")
return
}
self?.messages = documents.compactMap { Message(document: $0) }
self?.reloadTableView()
}
}
private func reloadTableView() {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
// MARK: - Date Formatting Helper
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
}
// MARK: - UITableViewDataSource & UITableViewDelegate
extension MessagesTableViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath)
// セルが `Subtitle` スタイルを持つようにする
if cell.textLabel == nil || cell.detailTextLabel == nil {
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "MessageCell")
}
let message = messages[indexPath.row]
// Cellのカスタマイズ
cell.textLabel?.text = message.messageContent
cell.detailTextLabel?.numberOfLines = 0 // 行数を制限しない
cell.detailTextLabel?.text = "Created: \(formatDate(message.createdTime))\nScheduled: \(formatDate(message.scheduledTime))"
return cell
}
}
各行の役割
import UIKit
import Firebase
import FirebaseFirestore
- import UIKit: iOSアプリのUIを構築するためのフレームワークです。
- import Firebase: Firebaseの基本的な機能を利用するためのインポートです。
- import FirebaseFirestore: Firebase Firestoreを使用するためのモジュールをインポートします。
class MessagesTableViewController: UIViewController {
- MessagesTableViewControllerは、UIViewControllerを継承し、テーブルビューを使用してメッセージを表示するためのクラスです。
@IBOutlet weak var tableView: UITableView!
private var messages: [Message] = []
- @IBOutlet weak var tableView: UITableView!: Storyboardで接続されたテーブルビューのアウトレットです。
- private var messages: [Message] = []: メッセージを保持するための配列です。この配列にはFirestoreから取得したMessageオブジェクトが格納されます。
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
fetchMessages()
}
- viewDidLoad(): ビューがロードされたときに呼び出されるメソッドで、テーブルビューの設定とメッセージの取得を行います。
private func configureTableView() {
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MessageCell")
}
- configureTableView(): テーブルビューのデータソースとデリゲートを設定し、セルの再利用識別子"MessageCell"でセルを登録します。
private func fetchMessages() {
let db = Firestore.firestore()
db.collection("messages").order(by: "createdTime", descending: false).getDocuments { [weak self] snapshot, error in
if let error = error {
print("Error fetching messages: \(error)")
return
}
guard let documents = snapshot?.documents else {
print("No documents found")
return
}
self?.messages = documents.compactMap { Message(document: $0) }
self?.reloadTableView()
}
}
- fetchMessages(): Firestoreのmessagesコレクションからデータを取得し、createdTimeでソートして取得します。取得したドキュメントをMessageオブジェクトに変換してmessages配列に格納します。その後、テーブルビューをリロードします。
private func reloadTableView() {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
- reloadTableView(): メインスレッドでテーブルビューをリロードします。UIの更新はメインスレッドで行う必要があります。
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
- formatDate(_ date: Date) -> String: DateオブジェクトをStringにフォーマットして返します。メッセージの作成日時や予定日時を表示するために使用されます。
extension MessagesTableViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath)
// セルが `Subtitle` スタイルを持つようにする
if cell.textLabel == nil || cell.detailTextLabel == nil {
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "MessageCell")
}
let message = messages[indexPath.row]
// Cellのカスタマイズ
cell.textLabel?.text = message.messageContent
cell.detailTextLabel?.numberOfLines = 0 // 行数を制限しない
cell.detailTextLabel?.text = "Created: \(formatDate(message.createdTime))\nScheduled: \(formatDate(message.scheduledTime))"
return cell
}
}
- numberOfRowsInSection: テーブルビューに表示する行数を返します。ここではmessages配列の要素数を返します。
- cellForRowAt: 各行に表示するセルを設定します。セルにはメッセージの内容と、作成日時、予定日時を表示します。セルがSubtitleスタイルを持つようにカスタマイズされています。