LoginSignup
94
69

More than 3 years have passed since last update.

SwiftでFirebaseを使う時の備忘録

Last updated at Posted at 2019-05-01

こんにちは

ふみっちです。令和元年なうです。
突然ですが、僕は最近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を持つユーザーがサインインしたとき、または
  • 現在のユーザーがサインアウトしたとき

で起こるようです。

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のハマりポイント

  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ライフを楽しんでみてください!
質問・修正案はコメント欄にてお待ちしています。

94
69
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
94
69