SwiftでFirebaseを使う時の備忘録

# こんにちは
ふみっちです。令和元年なうです。
突然ですが、僕は最近iOSアプリを作る時は大体Firebaseを使用しています。
なのでアウトプットも兼ねてFirebaseの備忘録を残したいと思います。

# Firebaseとは
バックエンドを実装するのが簡単になるサービスです。
主に以下のような機能を提供しています。

| 機能 | サービス名 |
| --- | --- |
| データベース | FirebaseFirestore |
| アカウント認証 | FirebaseAuth |  
| 画像ストレージ | FirebaseStorage | 
| 通知メッセージ | FirebaseMessaging |
|HTTPトリガー・Firebaseトリガー|CloudFunctions|


今回は`FirebaseFirestore` `FirebaseAuth` `FirebaseStorage` の3つを紹介します。

# FirebaseFirestore


### コレクションとドキュメント  
Firestoreでは、コレクションというテーブルのようなものが最も大きなデータの構成単位です。データベースはこのコレクションを複数持っています。  
その次に、ドキュメントというものがコレクションの中に複数存在しています。このドキュメントにデータが格納されています。

図にすると下のような感じです。
    
![firebasefirestore.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/bec78ba1-c616-5610-197a-610a66c7f695.png)


### 取得

```usersコレクションからfummicc1というキーのドキュメントを取得するコード.swift
Firestore.firestore().collection("users").document("fummicc1").getDocument { (snap, error) in
}
```

```usersコレクションの中の全てのドキュメントを取得するコード.swift
Firestore.firestore().collection("users").getDocuments { (snaps, error) in
}
```

`snap`または`snaps`にはそのパスに含まれるドキュメントなどがプロパティに含まれていて取得できるようになっています。

以下が、取得したデータをコンソール上にプリントするまでの例です。

```特定のドキュメントを取得した場合.swift
Firestore.firestore().collection("users").document("fummicc1").getDocument { (snap, error) in
    if let error = error {
        fatalError("\(error)")
    }
    guard let data = snap?.data() else { return }
    print(data)
}
```

このようにデータベースに保存されている内容がプリントされます。
<img width="492" alt="スクリーンショット 2019-05-08 14.39.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/fed8e5f3-0a95-7d05-f812-b0832f376e23.png">

```複数のドキュメントを取得した場合.swift
Firestore.firestore().collection("users").getDocuments { (snaps, error) in
    if let error = error {
        fatalError("\(error)")
    }
    guard let snaps = snaps else { return }
    for document in snaps.documents {
        print(document.data())
    }
}
```

結果は、`users`というコレクションの中にあるすべてのドキュメントのデータがプリントされます。
<img width="567" alt="スクリーンショット 2019-05-08 14.43.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/19616b64-bc3f-0bde-2889-8877abace884.png">


### 作成

```usersコレクションに新しくfummicc1というキーのドキュメントを作成するコード.swift
Firestore.firestore().collection("users").document("fummicc1").setData(
    [
        "user_name": "ふみっち",
        "gender": "Otoko"
    ])
```

```usersコレクションに新しくランダムなUIDをキーとしたドキュメントを作成するコード.swift
Firestore.firestore().collection("users").document().setData(
    [
        "identify": "fummicc1",
        "user_name": "ふみっち",
        "gender": "Otoko"
    }
)
```

### 更新

```usersコレクションのfummicc1ドキュメントを更新するコード。更新したい以外のデータは残ります。他にもあるけど、基本はこれを使用していればOK!.swift
// 普通に書き込むコード
Firestore.firestore()
    .collection("users")
    .document("fummicc1")
    .setData(["user_name": "ふみっち"])

// 更新するコード
Firestore.firestore()
    .collection("users")
    .document("fummicc1")
    .setData(["age": 19 + 1], merge: true)　// merge: true がポイント
```

### 削除
```usersコレクションのfummicc1ドキュメントを削除するコード.swift
Firestore.firestore().collection("users").document("fummicc1").delete()
```

### 非同期処理について

上で書き方を紹介しましたが、基本的にFirebaseは**非同期処理**なので、実際にはクロージャーを使う必要が出てきます。Firestoreの例でいうと、`getDocument()`や`getDocuments()`などは引数にクロージャーを使用しています。

そもそも何故クロージャという記述方法をここで取らないといけないのかですが、非同期処理の場合通信している間に時間がかかります。

```Bad.swift
Firestore.firestore().collection("コレクション名").document("ドキュメント名").getDocument { (snap, error) in
}
print("処理が始まったよー！")
var modelList: [Model] = []
for data in snap!.data()! {
   let model = Model(data: data)
   modelList.append(model)
}
self.modelList = modelList
self.tableView.reloadData()
```

```Good.swift
Firestore.firestore().collection("コレクション名").document("ドキュメント名").getDocument {　(snap, error) in
    // ここは通信が終わったら呼ばれる！
    var modelList: [Model] = []
    for data in snap!.data()! {
        let model = Model(data: data)
        modelList.append(model)
    }
    self.modelList = modelList
    self.tableView.reloadData()
}
print("処理が始まったよー！")
```

この二つの例を比べると、処理の順番が違うことがわかると思います。
`Bad.swift`だと通信が始まった直後にデータを取得しようとしています。まだ通信が完了して`snap`に値が入っていないのでうまくいきませんし、そもそもコンパイルエラーになります。
`Good.swift`では、通信が終わった後を明示する`getDocument`のコールバックでデータの取得を行なっているように見えます。この場合は、
ちゃんと`snap`に値が入っているのでうまくいきます。


# FirebaseAuth
FirebaseAuthはユーザーを管理する機能です。
FacebookやGoogleなどが提供しているSDKを使用する方法もありますが、基本はユーザーにメールアドレスを登録してもらうのが簡単で良いです。

```アカウント作成処理.swift
var email: String = "sample@gmail.com"
var password: String = "123456"
Auth.auth().createUser(withEmail: email, password: password, completion: nil)
```

```ログイン処理.swift
Auth.auth().signIn(withEmail: email, password: password, completion: nil)
```

```ログアウト処理.swift
try? Auth.auth().signOut()
```

## ユーザーの認証状態をリッスンする
基本的に`Auth.auth().currentUser == nil`でユーザーが現在存在するのかはチェックが可能です。
ですが、アプリを起動してすぐの段階では初期化処理が終わっていなくnilを返してしまうケースがあったり、ログアウト処理やログイン処理に成功したときのハンドリングを様々な場所で行うと複雑になってしまうことがあります。
なので、[addStateDidChangeListener](https://firebase.google.com/docs/auth/ios/start#listen_for_authentication_state)メソッドを使用して`Auth`の認証状態に変更が走ったときのリスナーを設定することができます。

[Authの公式サイト](https://firebase.google.com/docs/reference/swift/firebaseauth/api/reference/Classes/Auth#addstatedidchangelistener_:)に書いてあるように、

- ブロックがリスナーとして登録されたとき
- 現在と異なるUIDを持つユーザーがサインインしたとき、または
- 現在のユーザーがサインアウトしたとき

で起こるようです。

```swift
Auth.auth().addStateDidChangeListener { (auth, user) in

    guard let user = user else {
        // サインアウトしたときの処理を書く
        return
    }

    // ユーザーが正常にログインしたときの処理を書く
}
```

また、以下のように書き込み時に書き込みのハンドリングをするのではなくリスナーを使用して読み込み時に書き込み時のハンドリングをする手法もあると思います。

```swift
Auth.auth().addStateDidChangeListener { (auth, user) in

    guard let firUser = user else {
        // サインアウトしたときの処理を書く
        return
    }

    // ユーザーが正常にログインしたときの処理を書く （Firestoreにユーザーデータを保存）
    let ref = Firestore.firestore().collection("users").document(firUser.uid)
    let user = AppUser(uid: firUser.uid, createdAt: firUser.metadata.creationDate, updatedAt: firUser.metadata.lastSignInDate)
    ref.setData(user.dicValue, merge: true)
}
```

## FirebaseAuthのハマりポイント
1. **Authの認証情報はKeychainで管理される。**
一般のiOSアプリではアプリをアンインストールして、再インストールした時には、再ログインを求められます。しかし、Firebaseではバックアップとして認証情報をKeychainを使用してデバイスに永続化しています。なので、アプリを再インストールしてもログインしたままになっています。デフォルトがこれなので、再インストールを考慮した場合は、**UserDefaults**を使用して再インストール初回起動時にログアウトという処理をした方がいいようです。

2. **コンソールで操作した内容が反映されない。**
例えば、[Firebase](https://console.firebase.google.com)のコンソール上でユーザーの削除などを行った場合、FirebaseAuthは1時間の猶予を与えるようで、つまり直ぐにはコンソール上の結果がユーザーの元に反映されません。なので、どうしたらいいかと言うと、`Auth.auth().currentUser?.reload {(result, error) in }`を呼ぶと、最新状態を取得してくれます。なので、これを`AppDelegate.swift`の`didFinishLaunchOptions`などで読んでおくと対処可能です。

# FirebaseStorage
FirebaseStorageは画像を保存するための機能で、ユーザーのプロフィール画像やSNSなどで投稿した写真などを保存しておくなどの用途があります。
僕個人としては、**FirestoreにFirebaseStorageのアイコンパスを保存しておいてそれを元に取得する**っていうパターンをよく取っています。

```参照の作成.swift
Storage.storage().reference().child("icon.png")
```

注意として、書き込み・読み込みともに**拡張子付き**で保存してあげてください！結構忘れやすいです。

### 取得

下の例は、読み込みを行った時の例です。
コールバックも呼んでいるので複雑になっていますが、使えるようになると便利なので最初は何度も書いて実践してみると良きな気がします。

 ```特定のユーザーのアイコン画像を取得する処理.swift
func loadIconImage(authenticationID: String, completion: @escaping (UIImage) -> ()) {
    Firestore.firestore().collection("users").document(authenticationID).getDocument { (snap, error) in
        guard let data = snap?.data() else { return }
        let iconPath = data["icon_path"] as? String ?? ""
        Storage.storage().reference().child("icons").child("\(iconPath).jpeg").downloadURL(completion: { (url, error) in
            guard
                let url = url,
                let imageData = try? Data(contentsOf: url),
                let image = UIImage(data: imageData) else { return }                
            completion(image)
        })
    }
}
```

### 書き込み

```swift
Storage.storage().reference().child("\(iconPath).jpeg").putData(imageData)
```

これでいけます。もし完了時に何かしたい場合は`putData(uploadData:, metadata:, completion:)`を使用すれば良いです。

# まとめ
Firebaseは近年注目されているサービスの一つで定期的に新しい機能・修正が入ってきます。これから新しく作り始めるアプリはバックエンドをFirebaseで管理して速く・質の良いアプリを作ることができると個人的には考えています。ぜひ、Firebaseライフを楽しんでみてください！
質問・修正案はコメント欄にてお待ちしています。