【Xcode/Swift/Firebase】チャット一覧画面の作り方

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

# この記事でやること
- チャット一覧画面作成
- 各チャットタップ時に該当画面へ遷移する機能の実装
- チャット作成ボタンの設置とチャット作成機能の実装

# 環境
- Xcode 15.0.1
- Swift 5

# 前提
- 下記まで完了していること
https://qiita.com/taku-ll/items/3af114563f6307b6f3e9

# 実装方法
## StoryBoardの設定
1. ViewControllerを設置
1. TableViewを設置
1. TableViewCellを設置
1. TableViewを選択し、右側のインスペクターパネルの中にある「Connections Inspector」（接続インスペクタ）を開き、DataSourceおよびDelegateをControllerViewにドロップする
1. TableViewを次章で出てくるtableViewにOutlet接続する

## ChatListViewController
### 全量
```
import UIKit
import FirebaseFirestore

class ChatListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    @IBOutlet weak var tableView: UITableView!
    
    var chats: [Chat] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        
        fetchChats()
        
        navigationItem.title = "チャット一覧"
        let createChatButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(createChat))
        navigationItem.rightBarButtonItem = createChatButton
    }
    
    @IBAction func createChat(_ sender: UIBarButtonItem) {
        performSegue(withIdentifier: "showChatCreation", sender: self)
    }
    
    func fetchChats() {
        // Firestoreからチャットリストを取得
        let db = Firestore.firestore()
        db.collection("chats").getDocuments { snapshot, error in
            if let error = error {
                print("Error getting documents: \(error)")
            } else {
                self.chats = snapshot?.documents.compactMap { document -> Chat? in
                    try? document.data(as: Chat.self)
                } ?? []
                self.tableView.reloadData()
            }
        }
    }
    
    // UITableViewデリゲートおよびデータソースメソッド
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return chats.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ChatCell", for: indexPath)
        let chat = chats[indexPath.row]
        cell.textLabel?.text = chat.name
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let chat = chats[indexPath.row]
        performSegue(withIdentifier: "showChat", sender: chat)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showChat" {
            if let chatViewController = segue.destination as? ChatViewController, let chat = sender as? Chat {
                chatViewController.chat = chat
            }
        } else if segue.identifier == "showChatCreation" {
            if let chatCreationViewController = segue.destination as? ChatCreationViewController {
                chatCreationViewController.delegate = self
            }
        }
    }
}

extension ChatListViewController: ChatCreationDelegate {
    func didCreateChat(chat: Chat) {
        // 新しいチャットを作成し、チャット画面に遷移する
        self.chats.append(chat)
        self.tableView.reloadData()
        self.performSegue(withIdentifier: "showChat", sender: chat)
    }
}
```

### インポート
```
import UIKit
import FirebaseFirestore
```
- import UIKit: UIKitフレームワークをインポートして、UIコンポーネントを使用できるようにします。
- import FirebaseFirestore: Firestoreをインポートして、Firebaseのクラウドデータベース機能を使用できるようにします。

### ChatListViewControllerクラス
```
class ChatListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
```
- ChatListViewController: チャット一覧を表示するためのビューコントローラークラスです。
- UITableViewDelegate: テーブルビューの行選択やスクロールなどのイベントを処理するためのデリゲートプロトコルです。
- UITableViewDataSource: テーブルビューに表示するデータを提供するためのデータソースプロトコルです。

### プロパティ
```
@IBOutlet weak var tableView: UITableView!
var chats: [Chat] = []
```
- @IBOutlet weak var tableView: UITableView!: Interface Builderで接続されたテーブルビューのアウトレットです。
- var chats: [Chat] = []: チャットデータを格納する配列です。

### viewDidLoadメソッド
```
override func viewDidLoad() {
    super.viewDidLoad()
    
    tableView.delegate = self
    tableView.dataSource = self
    
    fetchChats()
    
    navigationItem.title = "チャット一覧"
    let createChatButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(createChat))
    navigationItem.rightBarButtonItem = createChatButton
}
```
- override func viewDidLoad(): ビューがロードされたときに呼び出されるメソッドです。
- tableView.delegate = self: テーブルビューのデリゲートをこのビューコントローラーに設定します。
- tableView.dataSource = self: テーブルビューのデータソースをこのビューコントローラーに設定します。
- fetchChats(): チャットデータをFirestoreから取得するメソッドを呼び出します。
- navigationItem.title = "チャット一覧": ナビゲーションバーのタイトルを「チャット一覧」に設定します。
- navigationItem.rightBarButtonItem = createChatButton: 右上にチャット作成ボタンを追加します。<font color="Red">（※他記事で説明）</font>

### createChatアクション<font color="Red">（※他記事で説明）</font>
```
@IBAction func createChat(_ sender: UIBarButtonItem) {
    performSegue(withIdentifier: "showChatCreation", sender: self)
}
```
- @IBAction func createChat(_ sender: UIBarButtonItem): チャット作成ボタンがタップされたときに呼び出されるアクションメソッドです。
- performSegue(withIdentifier: "showChatCreation", sender: self): "showChatCreation"セグエを実行してチャット作成画面に遷移します。

### fetchChatsメソッド
```
func fetchChats() {
    let db = Firestore.firestore()
    db.collection("chats").getDocuments { snapshot, error in
        if let error = error {
            print("Error getting documents: \(error)")
        } else {
            self.chats = snapshot?.documents.compactMap { document -> Chat? in
                try? document.data(as: Chat.self)
            } ?? []
            self.tableView.reloadData()
        }
    }
}
```
- func fetchChats(): Firestoreからチャットデータを取得してchats配列に格納し、テーブルビューを更新するメソッドです。
- let db = Firestore.firestore(): Firestoreのインスタンスを取得します。
- db.collection("chats").getDocuments { ... }: "chats"コレクションからドキュメントを取得します。
- self.chats = snapshot?.documents.compactMap { document -> Chat? in try? document.data(as: Chat.self) } ?? []: 取得したドキュメントをChatオブジェクトに変換してchats配列に格納します。
- self.tableView.reloadData(): テーブルビューをリロードしてデータを表示します。

### UITableViewDataSourceメソッド
```
func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return chats.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ChatCell", for: indexPath)
    let chat = chats[indexPath.row]
    cell.textLabel?.text = chat.name
    return cell
}
```
- func numberOfSections(in tableView: UITableView) -> Int: テーブルビューのセクション数を返します。この場合、セクションは1つです。
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int: 各セクションの行数を返します。この場合、chats配列の数です。
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell: 各行のセルを設定して返します。チャット名をセルに設定します。

### UITableViewDelegateメソッド
```
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let chat = chats[indexPath.row]
    performSegue(withIdentifier: "showChat", sender: chat)
}
```
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath): 行が選択されたときに呼び出されます。選択されたチャットをsenderとして"showChat"セグエを実行します。

### prepareメソッド　<font color="Red">（※他記事で説明）</font>
```
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showChat" {
        if let chatViewController = segue.destination as? ChatViewController, let chat = sender as? Chat {
            chatViewController.chat = chat
        }
    } else if segue.identifier == "showChatCreation" {
        if let chatCreationViewController = segue.destination as? ChatCreationViewController {
            chatCreationViewController.delegate = self
        }
    }
}
```
- override func prepare(for segue: UIStoryboardSegue, sender: Any?): セグエが実行される前に呼び出され、データを次のビューコントローラーに渡します。
- if segue.identifier == "showChat": "showChat"セグエの場合、ChatViewControllerにチャットデータを渡します。
- if segue.identifier == "showChatCreation": "showChatCreation"セグエの場合、ChatCreationViewControllerのデリゲートを設定します。

### ChatCreationDelegateメソッド<font color="Red">（※他記事で説明）</font>
```
extension ChatListViewController: ChatCreationDelegate {
    func didCreateChat(chat: Chat) {
        self.chats.append(chat)
        self.tableView.reloadData()
        self.performSegue(withIdentifier: "showChat", sender: chat)
    }
}
```
- extension ChatListViewController: ChatCreationDelegate: ChatCreationDelegateプロトコルを実装するための拡張です。
- func didCreateChat(chat: Chat): 新しいチャットが作成されたときに呼び出されます。新しいチャットをchats配列に追加し、テーブルビューをリロードして、"showChat"セグエを実行します。