Edited at

SwiftでFirebaseを使う時の備忘録


こんにちは

ふみっちです。令和元年なうです。

突然ですが、僕は最近iOSアプリを作る時は大体Firebaseを使用しています。

なのでアウトプットも兼ねてFirebaseの備忘録を残したいと思います。


Firebaseとは

バックエンドを実装するのが簡単になるサービスです。

主に以下のような機能を提供しています。

機能
サービス名

データベース
FirebaseFirestore

アカウント認証
FirebaseAuth

画像ストレージ
FirebaseStorage

通知メッセージ
FirebaseMessaging

HTTPトリガー・Firebaseトリガー
CloudFunctions

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


FirebaseFirestore


コレクションとドキュメント

Firestoreでは、コレクションというテーブルのようなものが最も大きなデータの構成単位です。データベースはこのコレクションを複数持っています。


その次に、ドキュメントというものがコレクションの中に複数存在しています。このドキュメントにデータが格納されています。

図にすると下のような感じです。

firebasefirestore.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)
}

このようにデータベースに保存されている内容がプリントされます。

スクリーンショット 2019-05-08 14.39.02.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というコレクションの中にあるすべてのドキュメントのデータがプリントされます。

スクリーンショット 2019-05-08 14.43.44.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メソッドを使用してAuthの認証状態に変更が走ったときのリスナーを設定することができます。

Authの公式サイトに書いてあるように、


  • ブロックがリスナーとして登録されたとき

  • 現在と異なるUIDを持つユーザーがサインインしたとき、または

  • 現在のユーザーがサインアウトしたとき

で起こるようです。


var isRegisterd: Bool = false

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

// 初回は、リッスン開始時に自動で呼ばれるので無視。
if !isRegisterd {
isRegisterd = true
return
}

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

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

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


FirebaseAuthのハマりポイント


  1. Authの認証情報はKeychainで管理される。

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


  2. コンソールで操作した内容が反映されない。

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



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)
})
}
}


書き込み

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

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


まとめ

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

質問・修正案はコメント欄にてお待ちしています。