[Xcode/Swift] GPT先生と学ぶFirebase(データベース編) No.2

「ユーザーのデータを保存したいけど、サーバーとか書きたくない…」
そんなときのGoodなツール、それがFirebase Firestore

この章では、FirestoreをiOSアプリに組み込んで

  • 実際に読み書きできるようにする
  • 設計&最適化もちゃんと考え

この流れでざっくり学んでいく。

Firebaseの初期セットアップはできている前提で進めます、まだここが完了していない場合は以下を参考に。

[Xcode/Swift] GPT先生と学ぶFirebase(導入、概念編) No.0

Firebase ConsoleでFirestoreを有効化する

  1. Firebase Console にアクセス
  2. プロジェクトを選択(または新規作成)
  3. 左側のメニューから 「Cloud Firestore」 を選ぶ
  4. データベースを作成」をクリック
  5. テストモード(最初はこれでOK、後でセキュリティルールを締める)
  6. リージョンを選ぶ(asia-northeast1(東京))
  7. 完了

実装

まずは動くコードをざっくり作る

プロジェクトにFireStoreが入ってなければ追加 (SPM)

https://github.com/firebase/firebase-ios-sdk

初期化は最低限でOK

import Firebase

@main
struct MyApp: App {
    init() {
        FirebaseApp.configure()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

ContentViewで、データ追加、取得をしてみる。

import SwiftUI
import FirebaseFirestore

struct FSUser: Identifiable, Codable {
    var id: String
    var name: String
    var age: Int
    var isPremium: Bool
}

final class FirestoreViewModel: ObservableObject {
    @Published var users: [FSUser] = []
    private let database = Firestore.firestore()

    init() {
        fetchUsers()
    }

    func addUser(name: String, age: Int, isPremium: Bool) {
        let userDocument = database.collection("users").document() // Firestoreの "users" コレクションに新しいドキュメントを1つ作成する
        let userData: [String: Any] = [
            "name": name,
            "age": age,
            "isPremium": isPremium
        ]

        userDocument.setData(userData) { error in
            if let error = error {
                print("追加失敗: \(error.localizedDescription)")
            } else {
                print("追加成功")
                self.fetchUsers()
            }
        }
    }

    func fetchUsers() {
        database.collection("users").getDocuments { snapshot, error in
            guard let documentSnapshots = snapshot?.documents else {
                print("取得失敗: \(error?.localizedDescription ?? "unknown error")")
                return
            }

            self.users = documentSnapshots.compactMap { documentSnapshot -> FSUser? in
                let data = documentSnapshot.data()
                guard let name = data["name"] as? String,
                      let age = data["age"] as? Int,
                      let isPremium = data["isPremium"] as? Bool else { return nil }

                return FSUser(id: documentSnapshot.documentID, name: name, age: age, isPremium: isPremium)
            }
        }
    }
}

struct HomeView: View {
    @StateObject private var viewModel = FirestoreViewModel()
    @State private var name = ""
    @State private var age = ""
    @State private var isPremium = false

    var body: some View {
        NavigationView {
            VStack {
                Form {
                    TextField("名前", text: $name)
                    TextField("年齢", text: $age)
                        .keyboardType(.numberPad)
                    Toggle("プレミアム", isOn: $isPremium)

                    Button("ユーザー追加") {
                        guard let ageInt = Int(age) else { return }
                        viewModel.addUser(name: name, age: ageInt, isPremium: isPremium)
                        name = ""
                        age = ""
                        isPremium = false
                    }
                }

                List(viewModel.users) { user in
                    VStack(alignment: .leading) {
                        Text(user.name)
                            .font(.headline)
                        Text("年齢: \(user.age), プレミアム: \(user.isPremium ? "はい" : "いいえ")")
                            .font(.subheadline)
                    }
                }
            }
            .navigationTitle("Firestoreユーザー")
        }
    }
}

読み取り最適化 & セキュリティルール

そもそもセキュリティルールとは?

Firebase Firestore = サーバーレスなデータベース、つまり基本的にクライアント(iOSアプリ)から直接データを読み書きする。

つまり、「誰が、どのデータを、どう操作していいか」をルールで守らないと危ない

セキュリティルールって何を制御してるのか:

具体的にはルールで制御できること
未ログインの人が見るの禁止したい✅ できる
他人のユーザーデータを読めなくしたい✅ できる
投稿は自分だけ作れて、みんなが読めるようにしたい✅ できる
特定のフィールドだけ更新許可したい✅ できる

ルールがないと:

テストモード(最初のデフォルト)では

allow read, write: if true;

→ 誰でも・なんでも・どのデータでも操作OK
公開中のアプリでこれだったらとてもマズイ

つまり、セキュリティルール = データの門番

ルールはこの場所に書く:

Firebase Console → Firestore Database → ルール タブ

ここに、JSっぽい構文で条件を書いていく。
例:

match /users/{userId} {
  allow read, write: if request.auth != null && request.auth.uid == userId;
}

Firestoreの基本

Firestoreは「読み取り課金制」

操作課金される?備考
ドキュメント1件取得.getDocument() 1回 = 1読み取り
コレクション取得(10件)✅✅✅…ドキュメント数ぶん課金
書き込み✅(だけど安い)読み取りよりはマシ
クエリ失敗✅(読まれた分)マッチしなくても課金される

読み取り最適化のテクニック:

  • 無駄なネスト構造を避ける(深いサブコレクション地獄は重くなる)
  • まとめて読みたい情報はドキュメント内に冗長に書いてもOK(正規化にこだわらない)
db.collection("posts")
    .whereField("authorId", isEqualTo: userId)
    .order(by: "createdAt", descending: true)
    .limit(to: 20)

無駄読み回避のモデリング:

posts: [
  {
    id: "p1",
    author: { id: "u1", name: "Taro", iconURL: "..." },
    ...
  }
]

// 一見便利だけど、ユーザー情報変更に弱すぎる(全部更新しなきゃいけない)
  • users/{userId}
  • posts/{postId} (中に authorId 持たせる)
  • 表示時は JOIN 的に2回読みに行くか、Cloud Functionsでauthor情報を埋めたサマリを別に作るなど

セキュリティルールの基本構造:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    match /posts/{postId} {
      allow read: if request.auth != null;
      allow write: if request.auth.uid == request.resource.data.authorId;
    }
  }
}

// request.auth.uid → 今ログイン中のユーザーID
// resource.data → 既存データ
// request.resource.data → これから書き込もうとしてるデータ
条件書き方
ログイン済みだけ許可request.auth != null
自分のデータだけアクセスOKrequest.auth.uid == userId
書き込みは一部のフィールドだけ許可request.resource.data.keys().hasOnly(['name', 'age'])
日付の書き換え禁止resource.data.createdAt == request.resource.data.createdAt

チェック & テスト:

  • Firebase Consoleの「ルール」タブ → ルールをテストする機能あり
  • 自分の uid を入力して、ルールが正しく機能してるか確認できる
  • 特に request.resource.data の中身を意識してチェック

まとめ

  • Firestoreは「読み取ったらお金かかる」→ 読み取り数 = コスト
  • クエリは 絞る・並べる・制限する が基本
  • セキュリティルールは 構造設計とセットで考えるべし
  • 1画面1クエリ1ユーザー1パーミッション の原則を守ればOK
  • 「この人(request.auth.uid)が、このデータ(resource.data)を、こんな内容(request.resource.data)で操作しようとしてるけど、OKにする?」
    → YESなら許可、NOならブロック。シンプル。

次回は画像アップロードとセキュリティのベストプラクティス