【Xcode/Swift/Firebase】チャットアプリにおける友達一覧画面の作り方

# この記事でやること
LINEのようなチャットアプリの友達一覧画面とその機能を実装する。画面は自分・友達一覧を表示し、友達追加ボタン・ログアウトボタンを設置する。機能は自分表示・友達表示・友達追加・ログアウトを実現する。

# 環境
- Xcode 15.0.1
- Swift 5

# 前提
- Firebaseを使用したユーザーの登録・ログイン機能が実装済であること
※下記参照
    - [Firebase Authenticationを使ったユーザー新規登録機能（メールアドレス＆パスワード）](https://qiita.com/taku-ll/items/226faa3e55d0243bb362)
    - [Firebase Authenticationを用いたログイン機能（メールアドレス＆パスワード）](https://qiita.com/taku-ll/items/ce26abfcdbedb105bb3f)
    - [自動ログインの方法](https://qiita.com/taku-ll/items/83b88cc798e625b27a3b)
- ユーザーの管理はFirestoreで行っており、データ構成が以下のようになっていること
```
users
  |-- userID1
        |-- username: "user1"
        |-- email: "user1@example.com"
        |-- profileImageURL: "http://example.com/user1.jpg"
        |-- friends: ["userID2", "userID3"]
```

# Story Board
1. ControllerViewを設置
2. EmbeddedInからNavigationControllerを選択
3. ControllerViewにTableViewとCellを表示
4. NavigationBarの真ん中にLabel("友達一覧")を、右側と左側にそれぞれButtonを設置する
5. TableViewを選択し、右側のインスペクターパネルの中にある「Connections Inspector」（接続インスペクタ）を開き、DataSourceおよびDelegateをControllerViewにドロップする
6. FirendsListViewController作成後にTableViewをアウトレット接続し、各ボタンをアクション接続する

下記画像参照（画像では Tab Bar も実装しているが、この記事では必要ない）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779738/2f8d6035-f354-cd57-faf6-4bcfaff988a5.png)

# FriendsListViewController

## インポートとクラス定義
```
import UIKit
import FirebaseFirestore
import FirebaseAuth

class FriendsListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
```
- UIKit：iOSアプリのUIを作成するためのフレームワーク。
- FirebaseFirestore：Firebase Firestoreデータベースを操作するためのフレームワーク。
- FirebaseAuth：Firebase認証を操作するためのフレームワーク。
- FriendsListViewController：UIViewControllerのサブクラスで、UITableViewDelegateとUITableViewDataSourceプロトコルに準拠。

## プロパティの定義
```
    @IBOutlet weak var tableView: UITableView!

    var friends: [String] = [] // 友達のユーザーIDを格納する配列
    var friendUsernames: [String] = [] // 友達のユーザー名を格納する配列
    var username: String = "" // 自分のユーザー名
```

- tableView：ユーザーのプロフィールと友達リストを表示するためのテーブルビュー。
- friends：友達のユーザーIDを格納する配列。
- friendUsernames：友達のユーザー名を格納する配列。
- username：自分のユーザー名を格納する文字列。

## viewDidLoadメソッド
```
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ナビゲーションバーの設定
        navigationItem.title = "友達一覧"
        let addFriendButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addFriend))
        navigationItem.rightBarButtonItem = addFriendButton
        
        tableView.delegate = self
        tableView.dataSource = self
        
        fetchUserProfile()
        fetchFriendsList()
        
        // ダミーユーザーの作成
//        DummyUserCreator.createDummyUsers()
    }
```
- viewDidLoadメソッド：ビューがロードされたときに呼び出される。
- ナビゲーションバーに「友達一覧」というタイトルを設定し、右上に友達追加ボタンを追加。
- tableViewのデリゲートとデータソースを設定。
- fetchUserProfileメソッドとfetchFriendsListメソッドを呼び出して、ユーザーのプロフィール情報と友達リストを取得。

## 友達追加処理
```
    @IBAction func addFriend(_ sender: UIBarButtonItem) {
        // 友達追加処理
        let alert = UIAlertController(title: "友達追加", message: "友達のユーザー名を入力してください", preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = "ユーザー名"
        }
        alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "追加", style: .default, handler: { _ in
            if let addedUsername = alert.textFields?.first?.text, !addedUsername.isEmpty {
                self.addFriendToDatabase(addedUsername: addedUsername)
            }
        }))
        present(alert, animated: true, completion: nil)
    }
```

- addFriendメソッド：友達追加ボタンがタップされたときに呼び出される。
- アラートを表示し、ユーザー名を入力させる。
- 入力されたユーザー名が空でない場合、addFriendToDatabaseメソッドを呼び出して友達をデータベースに追加。

## ログアウト処理
```
    @IBAction func logoutButtonTapped(_ sender: UIBarButtonItem) {
        do {
            try Auth.auth().signOut()
            // ログイン画面に遷移
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let loginViewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
            if let window = UIApplication.shared.windows.first {
                window.rootViewController = loginViewController
                UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: nil, completion: nil)
            }
        } catch let signOutError as NSError {
            print("Error signing out: %@", signOutError)
        }
    }
```

- logoutButtonTappedメソッド：ログアウトボタンがタップされたときに呼び出される。
- Firebaseからサインアウトし、ログイン画面に遷移。

## プロフィール取得処理
```
    func fetchUserProfile() {
        // ユーザーのプロフィールを取得
        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 {
                self.username = document.data()?["username"] as? String ?? ""
                self.tableView.reloadData()
            }
        }
    }
```

- fetchUserProfileメソッド：現在ログインしているユーザーのプロフィール情報をFirestoreから取得し、usernameプロパティに保存。

## 友達リスト取得処理
```
    func fetchFriendsList() {
        // 友達リストを取得
        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 {
                self.friends = document.data()?["friends"] as? [String] ?? []
                self.fetchFriendUsernames() // 友達のユーザー名を取得
            }
        }
    }
```

- fetchFriendsListメソッド：現在ログインしているユーザーの友達リストをFirestoreから取得し、friendsプロパティに保存。取得後、fetchFriendUsernamesメソッドを呼び出して友達のユーザー名を取得。

## 友達のユーザー名取得処理
```
    func fetchFriendUsernames() {
        // 友達のユーザー名を取得
        let db = Firestore.firestore()
        let dispatchGroup = DispatchGroup()
        friendUsernames = [] // リセット
        
        for friendId in friends {
            dispatchGroup.enter()
            db.collection("users").document(friendId).getDocument { document, error in
                if let document = document, document.exists {
                    let friendUsername = document.data()?["username"] as? String ?? ""
                    self.friendUsernames.append(friendUsername)
                }
                dispatchGroup.leave()
            }
        }
        
        dispatchGroup.notify(queue: .main) {
            self.tableView.reloadData()
        }
    }
```

- fetchFriendUsernamesメソッド：friendsプロパティに格納されている友達のユーザーIDを使って、各ユーザーのユーザー名をFirestoreから取得し、friendUsernamesプロパティに保存。

## 友達追加処理
```
    func addFriendToDatabase(addedUsername: String) {
        // 友達をデータベースに追加
        guard let userId = Auth.auth().currentUser?.uid else { return }
        let db = Firestore.firestore()
        db.collection("users").whereField("username", isEqualTo: addedUsername).getDocuments { snapshot, error in
            if let documents = snapshot?.documents, !documents.isEmpty {
                if let friendId = documents.first?.documentID {
                    db.collection("users").document(userId).updateData([
                        "friends": FieldValue.arrayUnion([friendId])
                    ]) { error in
                        if let error = error {
                            print("友達追加に失敗しました: \(error)")
                        } else {
                            self.friends.append(friendId)
                            self.fetchFriendUsernames()
                        }
                    }
                }
            } else {
                print("ユーザーが見つかりませんでした")
            }
        }
    }
```

- addFriendToDatabaseメソッド：入力されたユーザー名を使ってFirestoreでユーザーを検索し、見つかったユーザーを現在のユーザーの友達リストに追加。

## UITableViewデリゲートおよびデータソースメソッド
```
    func numberOfSections(in tableView: UITableView) -> Int {
        return 2 // プロフィールセクションと友達セクション
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if section == 0 {
            return 1 // プロフィールセクションには自分のプロフィールのみ
        } else {
            return friendUsernames.count // 友達セクションには友達の数
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "FriendCell", for: indexPath)
        if indexPath.section == 0 {
            cell.textLabel?.text = username
        } else {
            cell.textLabel?.text = friendUsernames[indexPath.row]
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if section == 0 {
            return "プロフィール"
        } else {
            return "友達一覧"
        }
    }
}
```

- numberOfSections(in tableView:)メソッド：テーブルビューのセクション数を2に設定。
- tableView(_:numberOfRowsInSection:)メソッド：各セクションの行数を設定。プロフィールセクションは1行、友達セクションは友達の数だけ行を表示。
- tableView(_:cellForRowAt:)メソッド：各行のセルを設定。プロフィールセクションには自分のユーザー名を表示し、友達セクションには友達のユーザー名を表示。
- tableView(_:titleForHeaderInSection:)メソッド：各セクションのヘッダーに表示するタイトルを設定。プロフィールセクションには「プロフィール」、友達セクションには「友達一覧」を表示。

## 全量

```
import UIKit
import FirebaseFirestore
import FirebaseAuth

class FriendsListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!

    var friends: [String] = [] // 友達のユーザーIDを格納する配列
    var friendUsernames: [String] = [] // 友達のユーザー名を格納する配列
    var username: String = "" // 自分のユーザー名

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ナビゲーションバーの設定
        navigationItem.title = "友達一覧"
        let addFriendButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addFriend))
        navigationItem.rightBarButtonItem = addFriendButton
        
        tableView.delegate = self
        tableView.dataSource = self
        
        fetchUserProfile()
        fetchFriendsList()
        
        // ダミーユーザーの作成
//        DummyUserCreator.createDummyUsers()
    }

    @IBAction func addFriend(_ sender: UIBarButtonItem) {
        // 友達追加処理
        let alert = UIAlertController(title: "友達追加", message: "友達のユーザー名を入力してください", preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = "ユーザー名"
        }
        alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "追加", style: .default, handler: { _ in
            if let addedUsername = alert.textFields?.first?.text, !addedUsername.isEmpty {
                self.addFriendToDatabase(addedUsername: addedUsername)
            }
        }))
        present(alert, animated: true, completion: nil)
    }
    
    @IBAction func logoutButtonTapped(_ sender: UIBarButtonItem) {
        do {
            try Auth.auth().signOut()
            // ログイン画面に遷移
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let loginViewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
            if let window = UIApplication.shared.windows.first {
                window.rootViewController = loginViewController
                UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: nil, completion: nil)
            }
        } catch let signOutError as NSError {
            print("Error signing out: %@", signOutError)
        }
    }

    func fetchUserProfile() {
        // ユーザーのプロフィールを取得
        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 {
                self.username = document.data()?["username"] as? String ?? ""
                self.tableView.reloadData()
            }
        }
    }

    func fetchFriendsList() {
        // 友達リストを取得
        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 {
                self.friends = document.data()?["friends"] as? [String] ?? []
                self.fetchFriendUsernames() // 友達のユーザー名を取得
            }
        }
    }
    
    func fetchFriendUsernames() {
        // 友達のユーザー名を取得
        let db = Firestore.firestore()
        let dispatchGroup = DispatchGroup()
        friendUsernames = [] // リセット
        
        for friendId in friends {
            dispatchGroup.enter()
            db.collection("users").document(friendId).getDocument { document, error in
                if let document = document, document.exists {
                    let friendUsername = document.data()?["username"] as? String ?? ""
                    self.friendUsernames.append(friendUsername)
                }
                dispatchGroup.leave()
            }
        }
        
        dispatchGroup.notify(queue: .main) {
            self.tableView.reloadData()
        }
    }

    func addFriendToDatabase(addedUsername: String) {
        // 友達をデータベースに追加
        guard let userId = Auth.auth().currentUser?.uid else { return }
        let db = Firestore.firestore()
        db.collection("users").whereField("username", isEqualTo: addedUsername).getDocuments { snapshot, error in
            if let documents = snapshot?.documents, !documents.isEmpty {
                if let friendId = documents.first?.documentID {
                    db.collection("users").document(userId).updateData([
                        "friends": FieldValue.arrayUnion([friendId])
                    ]) { error in
                        if let error = error {
                            print("友達追加に失敗しました: \(error)")
                        } else {
                            self.friends.append(friendId)
                            self.fetchFriendUsernames()
                        }
                    }
                }
            } else {
                print("ユーザーが見つかりませんでした")
            }
        }
    }
    
    // UITableViewデリゲートおよびデータソースメソッド
    func numberOfSections(in tableView: UITableView) -> Int {
        return 2 // プロフィールセクションと友達セクション
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if section == 0 {
            return 1 // プロフィールセクションには自分のプロフィールのみ
        } else {
            return friendUsernames.count // 友達セクションには友達の数
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "FriendCell", for: indexPath)
        if indexPath.section == 0 {
            cell.textLabel?.text = username
        } else {
            cell.textLabel?.text = friendUsernames[indexPath.row]
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if section == 0 {
            return "プロフィール"
        } else {
            return "友達一覧"
        }
    }
}
```