【Xcode/Firebase/Swift】チャット作成機能の実装

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

# この記事でやること
- チャット作成ボタンの設置とチャット作成機能の実装

# 環境
- Xcode 15.0.1
- Swift 5

# 前提
- 下記まで完了していること
[チャット一覧画面の作り方](https://qiita.com/taku-ll/items/c801fc09ff071c73c0b6)

# 実装方法

## StoryBoardの設定
### ChatCreationView
1. ControllerView（”ChatCreationViewController”）を設置
1. Button（”作成”）を設置
    1. 次章のcreateChatにAction接続する
1. チャット名入力用のTextFieldを設置
    1. 次章のchatNameTextFieldにOutlet接続する
1. チャット参加者選択のための友達一覧表示用のTableViewを設置
    1. 次章のtableViewにOutlet接続する
1. TableViewにCellを設置

### Segue接続
1. チャット一覧画面とChatCreationViewControllerをsegueで接続する

## ChatListViewControllerへの追加
チャット一覧画面のクラスファイルに「createChatButton」「createChatアクション」「ChatCreationDelegateメソッド」を追加する

### createChatButton
```
let createChatButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(createChat))
navigationItem.rightBarButtonItem = createChatButton
```
- viewDidLoad()内に追加する
- navigationItem.rightBarButtonItem = createChatButton: 右上にチャット作成ボタンを追加します

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

### ChatCreationDelegateメソッド
```
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"セグエを実行します

## ChatCreationViewController

### 全量
```
import UIKit
import FirebaseFirestore
import FirebaseAuth

protocol ChatCreationDelegate: AnyObject {
    func didCreateChat(chat: Chat)
}

class ChatCreationViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var chatNameTextField: UITextField!
    
    var users: [User] = []
    var selectedUsers: [User] = []
    weak var delegate: ChatCreationDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        
        fetchFriends()
        
        navigationItem.title = "チャット作成"
        let createButton = UIBarButtonItem(title: "作成", style: .done, target: self, action: #selector(createChat))
        navigationItem.rightBarButtonItem = createButton
    }
    
    func fetchFriends() {
        guard let userId = Auth.auth().currentUser?.uid else { return }
        let db = Firestore.firestore()
        
        // 友達リストを取得
        db.collection("users").document(userId).getDocument { document, error in
            if let document = document, document.exists {
                let friends = document.data()?["friends"] as? [String] ?? []
                self.fetchFriendDetails(friendIds: friends)
            }
        }
    }
    
    func fetchFriendDetails(friendIds: [String]) {
        let db = Firestore.firestore()
        let dispatchGroup = DispatchGroup()
        users = [] // リセット
        
        for friendId in friendIds {
            dispatchGroup.enter()
            db.collection("users").document(friendId).getDocument { document, error in
                if let document = document, document.exists {
                    let friendUsername = document.data()?["username"] as? String ?? ""
                    let user = User(id: friendId, name: friendUsername)
                    self.users.append(user)
                }
                dispatchGroup.leave()
            }
        }
        
        dispatchGroup.notify(queue: .main) {
            self.tableView.reloadData()
        }
    }
    
    @IBAction func createChat(_ sender: UIButton) {
        guard let chatName = chatNameTextField.text, !chatName.isEmpty else {
            showAlert(title: "エラー", message: "チャット名を入力してください")
            return
        }
        guard !selectedUsers.isEmpty else {
            showAlert(title: "エラー", message: "少なくとも1人の友達を選択してください")
            return
        }
        
        let db = Firestore.firestore()
        let chat = Chat(id: UUID().uuidString, name: chatName, participants: selectedUsers.map { $0.id })
        
        db.collection("chats").document(chat.id).setData(chat.toDictionary()) { error in
            if let error = error {
                print("Error creating chat: \(error)")
            } else {
                self.delegate?.didCreateChat(chat: chat)
                self.navigationController?.popViewController(animated: true)
            }
        }
    }
    
    func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }
    
    // UITableViewデリゲートおよびデータソースメソッド
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
        let user = users[indexPath.row]
        cell.textLabel?.text = user.name
        cell.accessoryType = selectedUsers.contains(where: { $0.id == user.id }) ? .checkmark : .none
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let user = users[indexPath.row]
        if let index = selectedUsers.firstIndex(where: { $0.id == user.id }) {
            selectedUsers.remove(at: index)
        } else {
            selectedUsers.append(user)
        }
        tableView.reloadRows(at: [indexPath], with: .automatic)
    }
    
    // 追加: セクション名を表示するメソッド
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "友達一覧"
    }
}
```

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

### ChatCreationDelegateプロトコル
```
protocol ChatCreationDelegate: AnyObject {
    func didCreateChat(chat: Chat)
}
```
- ChatCreationDelegate: チャット作成が完了したときに呼び出されるデリゲートメソッドを定義するプロトコルです。

### ChatCreationViewControllerクラス
```
class ChatCreationViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
```
- ChatCreationViewController: チャットを作成するためのビューコントローラーです。UITableViewDelegateとUITableViewDataSourceのプロトコルを実装しています。

### アウトレットとプロパティ
```
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var chatNameTextField: UITextField!

var users: [User] = []
var selectedUsers: [User] = []
weak var delegate: ChatCreationDelegate?
```
- @IBOutlet weak var tableView: UITableView!: Interface Builderで接続されたテーブルビューのアウトレットです。
- @IBOutlet weak var chatNameTextField: UITextField!: Interface Builderで接続されたテキストフィールドのアウトレットです。
- var users: [User] = []: 友達リストのユーザーを格納する配列です。
- var selectedUsers: [User] = []: 選択されたユーザーを格納する配列です。
- weak var delegate: ChatCreationDelegate?: チャット作成を通知するためのデリゲートです。

### viewDidLoadメソッド
```
override func viewDidLoad() {
    super.viewDidLoad()

    tableView.delegate = self
    tableView.dataSource = self

    fetchFriends()

    navigationItem.title = "チャット作成"
    let createButton = UIBarButtonItem(title: "作成", style: .done, target: self, action: #selector(createChat))
    navigationItem.rightBarButtonItem = createButton
}
```
- viewDidLoad: ビューがロードされたときに呼び出されます。テーブルビューのデリゲートとデータソースを設定し、友達リストを取得します。ナビゲーションバーのタイトルとチャット作成ボタンを設定します。

### fetchFriendsメソッド
```
func fetchFriends() {
    guard let userId = Auth.auth().currentUser?.uid else { return }
    let db = Firestore.firestore()

    // 友達リストを取得
    db.collection("users").document(userId).getDocument { document, error in
        if let document = document, document.exists {
            let friends = document.data()?["friends"] as? [String] ?? []
            self.fetchFriendDetails(friendIds: friends)
        }
    }
}
```
- fetchFriends: 現在のユーザーの友達リストをFirestoreから取得します。

### fetchFriendDetailsメソッド
```
func fetchFriendDetails(friendIds: [String]) {
    let db = Firestore.firestore()
    let dispatchGroup = DispatchGroup()
    users = [] // リセット

    for friendId in friendIds {
        dispatchGroup.enter()
        db.collection("users").document(friendId).getDocument { document, error in
            if let document = document, document.exists {
                let friendUsername = document.data()?["username"] as? String ?? ""
                let user = User(id: friendId, name: friendUsername)
                self.users.append(user)
            }
            dispatchGroup.leave()
        }
    }

    dispatchGroup.notify(queue: .main) {
        self.tableView.reloadData()
    }
}
```
- fetchFriendDetails: 友達リストの各ユーザーの詳細をFirestoreから取得し、users配列に格納します。すべてのデータを取得した後、テーブルビューをリロードします。

### createChatアクションメソッド
```
@IBAction func createChat(_ sender: UIButton) {
    guard let chatName = chatNameTextField.text, !chatName.isEmpty else {
        showAlert(title: "エラー", message: "チャット名を入力してください")
        return
    }
    guard !selectedUsers.isEmpty else {
        showAlert(title: "エラー", message: "少なくとも1人の友達を選択してください")
        return
    }

    let db = Firestore.firestore()
    let chat = Chat(id: UUID().uuidString, name: chatName, participants: selectedUsers.map { $0.id })

    db.collection("chats").document(chat.id).setData(chat.toDictionary()) { error in
        if let error = error {
            print("Error creating chat: \(error)")
        } else {
            self.delegate?.didCreateChat(chat: chat)
            self.navigationController?.popViewController(animated: true)
        }
    }
}
```
- createChat: チャット作成ボタンがタップされたときに呼び出されます。チャット名が入力されていること、友達が選択されていることを確認し、Firestoreに新しいチャットを作成します。作成が成功すると、デリゲートを介して親ビューコントローラーに通知し、現在のビューコントローラーを閉じます。

### showAlertメソッド
```
func showAlert(title: String, message: String) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    present(alert, animated: true, completion: nil)
}
```
- showAlert: エラーメッセージを表示するためのアラートを表示します。

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

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

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
    let user = users[indexPath.row]
    cell.textLabel?.text = user.name
    cell.accessoryType = selectedUsers.contains(where: { $0.id == user.id }) ? .checkmark : .none
    return cell
}
```
- numberOfSections: テーブルビューのセクション数を返します。この場合、セクションは1つです。
- numberOfRowsInSection: 各セクションの行数を返します。この場合、users配列の数です。
- cellForRowAt: 各行のセルを設定して返します。ユーザー名をセルに設定し、選択されたユーザーにはチェックマークを表示します。

### UITableViewDelegateメソッド
```
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let user = users[indexPath.row]
    if let index = selectedUsers.firstIndex(where: { $0.id == user.id }) {
        selectedUsers.remove(at: index)
    } else {
        selectedUsers.append(user)
    }
    tableView.reloadRows(at: [indexPath], with: .automatic)
}
```
- didSelectRowAt: 行が選択されたときに呼び出されます。選択されたユーザーをselectedUsers配列に追加または削除し、行を更新します。

### セクション名を表示するメソッド
```
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return "友達一覧"
}
```
- titleForHeaderInSection: セクションのタイトルを返します。この場合、セクションのタイトルは「友達一覧」です。