【Xcode/Swift/Firebase/MessageKit】指定した時間にメッセージを表示するアプリの実装方法

自分のために備忘録として記載してます。

# この記事でやること
- Firestoreを使用したデータ管理
- MessageKitを使用した指定時間にメッセージを表示する画面の設計と機能の実装

# 環境
- Xcode 15.0.1
- Swift 5

# 前提
- Firebaseプロジェクトが作成されていること
- iOSアプリがFirebaseプロジェクトに追加されていること
- Fitebase SDK とMessageKit SDK がインストールされていること
- AppDelegate.swift または SceneDelegate.swift でFirebaseが初期化されていること

# 実装方法
## StoryBoardの設定
以下2つの画面を設計する
1. **ChatControllerView**
メッセージを送信する、かつ指定時間にメッセージを表示する画面
1. **MessagesTableViewController**
送信したメッセージと各メッセージの送信時間・指定時間を表示する画面

### ChatControllerView
- IdentityInspectorのclassを設定する
- EmbedInからTabBarControllerを選択する


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779738/4556c855-1fab-00c2-19c5-3c15b5198d59.png)

### MessagesTableViewController
- IdentityInspectorのclassを設定する
- LibraryからTableViewを選択して画面全体に設定する
- LibraryからTableViewCellを選択してTableViewに設定する
- TableViewCellを選択し、AttributesInspectorのIdentifierを"MessageCell"に設定する
- TableViewCellを選択し、AttributesInspectorのStyleを"Subtitle"に設定する
- EmbedInからTabBarControllerを選択する


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779738/5e0d3eb7-d259-e255-e0f9-e2ab33a47ae4.png)

## 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
### 概要
1. MessageKitの設定: チャットUIの設定とメッセージの表示。
1. メッセージの受信と表示: Firestoreからメッセージデータを取得し、リアルタイムでチャット画面に表示。
1. メッセージの送信: 送信ボタンを押すと、日時を指定するためのDatePickerを表示し、Firestoreにメッセージを保存。
1. メッセージの表示状態の更新: メッセージが指定された時間に達した場合、そのメッセージが表示されたことを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スタイルを持つようにカスタマイズされています。