FirebaseでiOS版簡易SNSを作成する。(タイムライン編 後編)

# はじめに
今回、Firebaseを使ってiOS版簡易SNSを作成する記事になっています。
この記事では タイムライン上に編集可能なユーザー名を表示できる ・ ユーザーがログアウトできる などを行うまでの部分を行なっています。
また、[前回](https://qiita.com/fummicc1-sub/items/b4975f64b9f3f2bd46ca)の続きという扱いで進めていきます。

# タイムラインにユーザー名を表示

### AppUserクラスをデータベースに保存 & 取得

- TimelineViewControllerのviewWillAppearメソッドでAppUserをデータベースに保存。

usersコレクションの中に`me.userID`をキーとして保存します。**この時点ではユーザー名は作成していないので`userID`のみ保存します。**

```TimelineViewController.swift
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    (省略)
    // 追加
    database.collection("users").document(me.userID).setData([
        "userID": me.userID
        ], merge: true)    
}
```

- Postクラスの`userName`にデフォルト値をいれます。

仕様として、`Post.userName`に値が入っていなければ`匿名`と表示するようにします。
このとき、現在の実装だと`as! String`なので、`Post.userName`がnilであるとクラッシュしてしまうので、**Postクラス**を次のように変更します。

```現在の実装.swift
userName = data["userName"] as! String
```

```修正後のコード.swift
userName = data["userName"] as? String ?? "匿名"
```

- 加えて、AppUserをデータベースから取得するコードを同じくviewWillAppearメソッド内に記述します。

```swift
// 追加
database.collection("users").document(me.userID).getDocument { (snapshot, error) in
    if error == nil, let snapshot = snapshot, let data = snapshot.data() {
        self.me = AppUser(data: data)
    }
}
```

##### コラム: Firestoreからデータを取得するコード

- １つのドキュメントを取得する場合

```swift
Firestore.firestore().collection("コレクション名").document("ドキュメント名").getDocument { (snapshot, error) in
    // ここに通信後の処理を書く。 snapshot.data()でドキュメントのデータが取得可能
}
```

- コレクション内の全てのドキュメントを取得する場合

```swift
Firestore.firestore.collection("コレクション名").getDocuments { (snapshots, error) in
    // ここに通信後の処理を書く。
    // snapshot.documentsで全てのドキュメントが取得できる。
    // document.data()でドキュメントのデータが取得可能
    for document in snapshot!.documents {
        let data = document.data()
    }
}
```


### セル１つ１つに対して、ユーザーデータを取得し、ユーザー名を表示する。

```追加するコード.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
    cell.textLabel?.text = postArray[indexPath.row].content

    // 追加。それぞれの記事を投稿したユーザーをPostクラスのsenderIDを元に取得している。
    database.collection("users").document(postArray[indexPath.row].senderID).getDocument { (snapshot, error) in
        if error == nil, let snapshot = snapshot, let data = snapshot.data() {
            let appUser = AppUser(data: data)
            cell.detailTextLabel?.text = appUser.userName // 今回は、ユーザー名をdetailTextLabelに表示。
        }
    }
    return cell
}
```

細かくみていきます。

- 記事を投稿したデータの取得

これは、取得したいユーザーのIDをキーとしているドキュメントを`users`コレクション内から指定してあげる必要があります。

```swift
postArray[indexPath.row].senderID
```
これで**その投稿に適したユーザーID**を取得することは可能です。

なので、以下のように書くことでその投稿に適切な投稿者のデータを取ってこれます。

```swift
database.collection("users").document(postArray[indexPath.row].senderID).getDocument { (snapshot, error) in    
}
```

- 取得したユーザー名をセルに反映

今回は、デフォルトのセルを使用しています。柔軟性は低いので実際の場面では`TimelineTableViewCell`などを作成してUIをカスタマイズしていくことが好ましいですが、現状はデフォルトのセルを使用します。ここで`cell.detailTextLabel`というものを使用します。

##### detailTextLabelの作成の仕方
<img width="1427" alt="スクリーンショット 2019-09-01 22.06.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/15609841-5860-2a5b-7cb2-b8077a5932d2.png">

下のように`Detail`が追加されていればうまくいっています。

<img width="271" alt="スクリーンショット 2019-09-01 22.09.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/42e0b1ac-a9ea-c4dc-d51c-5c82169c157e.png">

##### 用意した`detailTextLabel`にユーザー名を表示する。

```swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
    cell.textLabel?.text = postArray[indexPath.row].content
    database.collection("users").document(postArray[indexPath.row].senderID).getDocument { (snapshot, error) in
        // ここを追加。
        // エラーがnilで、データが入っていることを確認。
        if error == nil, let snapshot = snapshot, let data = snapshot.data() {
            let appUser = AppUser(data: data)
            cell.detailTextLabel?.text = appUser.userName
        }
    }
    return cell
}
```

- 現在の状態

<img width="508" alt="スクリーンショット 2019-09-01 22.14.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/45ff0d32-3777-c91c-5069-20eaf914af27.png">


# ユーザー情報変更画面を作成

今回は、TimelineViewControllerに新たにスワイプジェスチャーを作成して、下から上にスワイプを行うと、ユーザー情報変更画面に遷移するようにしたいと思います。


- Main.storyboardに`UIViewController`を１つ追加し以下の部品を追加します。

|部品名|個数|
|---|---|
|UILabel|1|
|UITextField|1|
|UIButton|3|

以下のように配置してみます。大体自由で大丈夫です。

<img width="337" alt="スクリーンショット 2019-09-02 12.32.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/dadff56f-b0b3-e7f1-5c4d-2c98dc3264ad.png">

- TimelineViewControllerから新しく作成した画面をセグエで繋ぎます。セグエのIdentifierは`Settings`とします。<img width="1114" alt="スクリーンショット 2019-09-02 12.38.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/9d4696c3-4d45-1902-730e-ca96f6624f3a.png">

- `SettingsViewController`を作成・編集

まずは、以下の状態から始めていきます。

```SettingsViewController.swift
import UIKit
import Firebase

class SettingsViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var userNameTextField: UITextField! // 変更するユーザー名を入力するところ
    
    override func viewDidLoad() {
        super.viewDidLoad()
        userNameTextField.delegate = self // delegate指定
    }
    
    // returnキーを押したときの処理
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder() // キーボードを閉じる
        return true
    }
    
    //　前の画面に戻るボタン
    @IBAction func back() {
        dismiss(animated: true, completion: nil)
    }
    
    // 保存ボタンを押したときに呼ばれる。
    @IBAction func save() {
    }
    
    // ログアウトボタンを押したときに呼ばれる。
    @IBAction func logout() {
        
    }
}
```

### 画面遷移を行う

- **1.5秒間画面を長押ししたとき**に`TimelineViewController`から`SettingsViewController`に画面遷移する。

まずは、長押しを検知するために`UILongPressGestureRecognizer`を追加し、`view`(画面全体)にジェスチャーを追加します。

```TimelineViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    // ここから下を追加
    let press = UILongPressGestureRecognizer(target: self, action: #selector(pressScreen))
    press.minimumPressDuration = 1.5
    view.isUserInteractionEnabled = true
    view.addGestureRecognizer(press)
}
```

次に、`pressScreen`メソッドを実装していきます。と言っても内容は簡単で`performSegue`を呼ぶだけです。`Identifier`は先ほど設定した`Settings`にし、`sender`は次の画面でも`me`(ユーザー自身の情報)っは使用したいので、`me`を引数に渡します。

```TimelineViewController.swift
@objc
func pressScreen() {
    performSegue(withIdentifier: "Settings", sender: me)
}
```

その次に、`prepare`メソッドを呼んで、値渡しの処理を書いていきます。

現在、prepareメソッドは以下のようになっていると思います。

```現在のコード.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let destination = segue.destination as! AddViewController
    destination.me = sender as! AppUser
}
```

これを次のように変更します。

```編集後のコード.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {        
    if segue.identifier == "Add" {
        let destination = segue.destination as! AddViewController
        destination.me = sender as! AppUser
    } else if segue.identifier == "Settings" {
        let destination = segue.destination as! SettingsViewController
        destination.me = me
    }
}
```

**segue.identifier**でどのセグエが呼ばれていたかを分けることができます。これは**２つ以上のセグエ**が現在のViewControllerから呼ばれる可能性がある場合に有効です。

- `SettingsViewController`に`me`プロパティを追加します。

```SettingsViewController.swift
class SettingsViewController: UIViewController, UITextFieldDelegate {

    (省略)

    var me: AppUser! // 追加

    (省略)
}
```

### 現在のユーザー名を初期値として`UITextField.text`に設定します。

変更画面で今のユーザー名を間違えないようにユーザー名を初期値としてテキストフィールドに表示します。

```swift
class SettingsViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var userNameTextField: UITextField! // 変更するユーザー名を入力するところ
    var me: AppUser! 
    
    override func viewDidLoad() {
        super.viewDidLoad()
        userNameTextField.delegate = self // デリゲートを指定
        userNameTextField.text = me.userName // 追加: 現在のユーザー名をテキストに表示するコード
    }
}
```

### 変更したユーザー名を保存する

`@IBAction func save() { }`にコードを追加していきます。

```SettingsViewController.swift
//
//  SettingsViewController.swift
//  SNSApp
//
//  Created by Fumiya Tanaka on 2019/09/02.
//  Copyright © 2019 Fumiya Tanaka. All rights reserved.
//

import UIKit
import Firebase

class SettingsViewController: UIViewController, UITextFieldDelegate {

    (省略)

    // 保存ボタンを押したときに呼ばれる。
    @IBAction func save() {
        let newUserName = userNameTextField.text!
        Firestore.firestore().collection("users").document(me.userID).setData([
            "userName": newUserName
        ], merge: true) { error in // ここの merge: true がポイント
            if error == nil {
                self.dismiss(animated: true, completion: nil) // errorがなく、正常に終了していたらタイムラインの画面に戻る
            }
        }
    }

    (省略)
}
```

- コードのミソは、`merge: true`です。

`merge`をtrueにすると、データを**部分的に更新する**ことができます。

`update`だと、更新されないフィールドを消してしまいます。

```
user1:
        "a": 1,
        "b": 2,
        "c": 3,
```

これを `"c": 4`にアップデートする時に、`merge: true`にすると、

```
user1:
        "a": 1,
        "b": 2,
        "c": 4,
```

となって良い感じですが、`updateData`を使用すると

```
user1:
        "c": 4,
```

となって、更新されなかったフィールドは残りません。注意が必要ですが基本的には`merge: true`で問題ないと思います。

### logout機能を追加
- `@IBAction func logout() { }`にコードを追加していきます。

ログアウトする処理は下の一行で可能です。

```swift
try? Auth.auth().signOut()
```

なので、以下のようにコードを書きます。

```SettingsViewController.swift
@IBAction func logout() {
    try? Auth.auth().signOut()
    let accountViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! AccountViewController
    present(accountViewController, animated: true, completion: nil)
}
```

### StoryboardでSettingsViewControllerの関連付けを行う

- クラスを`SettingsViewController`に設定する。

<img width="667" alt="スクリーンショット 2019-09-10 23.00.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/8446ce45-e23e-34c0-5aeb-0f49b0eb7dec.png">

- それぞれの部品に対して関連付けを行う。

<img width="629" alt="スクリーンショット 2019-09-10 23.14.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/48dada1c-b96b-a351-8da6-2b2392baae8b.png">

実行してみましょう。

# 現在のDemo
![demo](https://media.giphy.com/media/Y01OSlmJru2hGqXpLU/giphy.gif)

ここで、気づいた方もいるかもしれませんが、このアプリは現在ログイン機能がないのにログアウト機能だけがある状態です。

また、
`Auth.auth.createUser`メソッドは既に使用されているメールアドレスでの新規登録ができないので、ログイン機能(サインイン機能)を実装していきます。

# サインイン機能
### サインイン画面の作成・Segueの作成

- 以下のGifのように新しく作成したViewControllerに`AccountViewController`からSegueを作成します。

![Demo](https://media.giphy.com/media/VEnvofPzxn0Z2fYO6A/giphy.gif)

- AccountViewController.swiftを編集

AccountViewControllerのprepareメソッドを以下のように編集します。
これによって、Timeline画面に遷移するときのみ、値渡しを行うことができます。

```swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "Timeline" {
        let nextViewController = segue.destination as! TimelineViewController
        let user = sender as! User
        nextViewController.me = AppUser(data: ["userID": user.uid])
    }
}
```

- UI部品を配置します。

<img width="521" alt="スクリーンショット 2019-09-13 12.22.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/4ed09d77-95ec-cedc-535c-f6c60229385a.png">

- TimelineViewControllerにSegueで繋ぎます。SegueのIdentifierを`Timeline`に設定します。

![](https://media.giphy.com/media/hofvIPIEKLw3n4qlI7/giphy.gif)

### SignInViewControllerを作成

- `SignInViewController.swift`

```SignInViewController.swift
import UIKit
import Firebase

class SignInViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var emailTextField: UITextField!
    @IBOutlet var passwordTextField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "Timeline" {
            let user = sender as! User
            let destination = segue.destination as! TimelineViewController
            destination.me = AppUser(data: ["userID": user.uid])
        }
    }
    
    @IBAction func tappedSignInButton() {
        let email = emailTextField.text!
        let password = passwordTextField.text!
        
        Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
            if error == nil, let result = result, result.user.isEmailVerified {
                self.performSegue(withIdentifier: "Timeline", sender: result.user)
            }
        }
    }
}
```

基本的な部分は`AccountViewController.swift`とほとんど同じです。
違いとしては、
`Auth.auth.createUser`で新規アカウント登録なのか、`Auth.auth.signIn`でサインインするのかという違いです。

これで、関連付けを行えばサインインが実装できました。ログアウトができるけど、サインインができないという変なアプリではなくりました！

# 最後に
この記事を通して

- Userの表示名を変更
- ログアウト・サインイン処理

といったことが可能になったと思います。

いいね機能やブロック機能やプロフィール画像などまだSNSとしてやりきれていない部分が多いので、緩く残りを更新して行けたらいいなと感じています。最後まで読んでいただきありがとうございました。


# 関連記事
- [メール認証編](https://qiita.com/fummicc1-sub/items/876b000fb514927a8df7)
- [参考プロジェクト](https://github.com/fummicc1/SNSApp)
- [タイムライン編 前編](https://qiita.com/fummicc1-sub/items/b4975f64b9f3f2bd46ca)