こんにちは
ふみっちです。令和元年なうです。
突然ですが、僕は最近iOSアプリを作る時は大体Firebaseを使用しています。
なのでアウトプットも兼ねてFirebaseの備忘録を残したいと思います。
Firebaseとは
バックエンドを実装するのが簡単になるサービスです。
主に以下のような機能を提供しています。
機能 | サービス名 |
---|---|
データベース | FirebaseFirestore |
アカウント認証 | FirebaseAuth |
画像ストレージ | FirebaseStorage |
通知メッセージ | FirebaseMessaging |
HTTPトリガー・Firebaseトリガー | CloudFunctions |
今回はFirebaseFirestore
FirebaseAuth
FirebaseStorage
の3つを紹介します。
FirebaseFirestore
コレクションとドキュメント
Firestoreでは、コレクションというテーブルのようなものが最も大きなデータの構成単位です。データベースはこのコレクションを複数持っています。
その次に、ドキュメントというものがコレクションの中に複数存在しています。このドキュメントにデータが格納されています。
図にすると下のような感じです。
取得
Firestore.firestore().collection("users").document("fummicc1").getDocument { (snap, error) in
}
Firestore.firestore().collection("users").getDocuments { (snaps, error) in
}
snap
またはsnaps
にはそのパスに含まれるドキュメントなどがプロパティに含まれていて取得できるようになっています。
以下が、取得したデータをコンソール上にプリントするまでの例です。
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)
}
このようにデータベースに保存されている内容がプリントされます。
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
というコレクションの中にあるすべてのドキュメントのデータがプリントされます。
作成
Firestore.firestore().collection("users").document("fummicc1").setData(
[
"user_name": "ふみっち",
"gender": "Otoko"
])
Firestore.firestore().collection("users").document().setData(
[
"identify": "fummicc1",
"user_name": "ふみっち",
"gender": "Otoko"
}
)
更新
// 普通に書き込むコード
Firestore.firestore()
.collection("users")
.document("fummicc1")
.setData(["user_name": "ふみっち"])
// 更新するコード
Firestore.firestore()
.collection("users")
.document("fummicc1")
.setData(["age": 19 + 1], merge: true) // merge: true がポイント
削除
Firestore.firestore().collection("users").document("fummicc1").delete()
非同期処理について
上で書き方を紹介しましたが、基本的にFirebaseは非同期処理なので、実際にはクロージャーを使う必要が出てきます。Firestoreの例でいうと、getDocument()
やgetDocuments()
などは引数にクロージャーを使用しています。
そもそも何故クロージャという記述方法をここで取らないといけないのかですが、非同期処理の場合通信している間に時間がかかります。
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()
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を使用する方法もありますが、基本はユーザーにメールアドレスを登録してもらうのが簡単で良いです。
var email: String = "sample@gmail.com"
var password: String = "123456"
Auth.auth().createUser(withEmail: email, password: password, completion: nil)
Auth.auth().signIn(withEmail: email, password: password, completion: nil)
try? Auth.auth().signOut()
ユーザーの認証状態をリッスンする
基本的にAuth.auth().currentUser == nil
でユーザーが現在存在するのかはチェックが可能です。
ですが、アプリを起動してすぐの段階では初期化処理が終わっていなくnilを返してしまうケースがあったり、ログアウト処理やログイン処理に成功したときのハンドリングを様々な場所で行うと複雑になってしまうことがあります。
なので、addStateDidChangeListenerメソッドを使用してAuth
の認証状態に変更が走ったときのリスナーを設定することができます。
Authの公式サイトに書いてあるように、
- ブロックがリスナーとして登録されたとき
- 現在と異なるUIDを持つユーザーがサインインしたとき、または
- 現在のユーザーがサインアウトしたとき
で起こるようです。
Auth.auth().addStateDidChangeListener { (auth, user) in
guard let user = user else {
// サインアウトしたときの処理を書く
return
}
// ユーザーが正常にログインしたときの処理を書く
}
また、以下のように書き込み時に書き込みのハンドリングをするのではなくリスナーを使用して読み込み時に書き込み時のハンドリングをする手法もあると思います。
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のハマりポイント
-
Authの認証情報はKeychainで管理される。
一般のiOSアプリではアプリをアンインストールして、再インストールした時には、再ログインを求められます。しかし、Firebaseではバックアップとして認証情報をKeychainを使用してデバイスに永続化しています。なので、アプリを再インストールしてもログインしたままになっています。デフォルトがこれなので、再インストールを考慮した場合は、UserDefaultsを使用して再インストール初回起動時にログアウトという処理をした方がいいようです。 -
コンソールで操作した内容が反映されない。
例えば、Firebaseのコンソール上でユーザーの削除などを行った場合、FirebaseAuthは1時間の猶予を与えるようで、つまり直ぐにはコンソール上の結果がユーザーの元に反映されません。なので、どうしたらいいかと言うと、Auth.auth().currentUser?.reload {(result, error) in }
を呼ぶと、最新状態を取得してくれます。なので、これをAppDelegate.swift
のdidFinishLaunchOptions
などで読んでおくと対処可能です。
FirebaseStorage
FirebaseStorageは画像を保存するための機能で、ユーザーのプロフィール画像やSNSなどで投稿した写真などを保存しておくなどの用途があります。
僕個人としては、FirestoreにFirebaseStorageのアイコンパスを保存しておいてそれを元に取得するっていうパターンをよく取っています。
Storage.storage().reference().child("icon.png")
注意として、書き込み・読み込みともに拡張子付きで保存してあげてください!結構忘れやすいです。
取得
下の例は、読み込みを行った時の例です。
コールバックも呼んでいるので複雑になっていますが、使えるようになると便利なので最初は何度も書いて実践してみると良きな気がします。
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)
})
}
}
書き込み
Storage.storage().reference().child("\(iconPath).jpeg").putData(imageData)
これでいけます。もし完了時に何かしたい場合はputData(uploadData:, metadata:, completion:)
を使用すれば良いです。
まとめ
Firebaseは近年注目されているサービスの一つで定期的に新しい機能・修正が入ってきます。これから新しく作り始めるアプリはバックエンドをFirebaseで管理して速く・質の良いアプリを作ることができると個人的には考えています。ぜひ、Firebaseライフを楽しんでみてください!
質問・修正案はコメント欄にてお待ちしています。