「ユーザーのデータを保存したいけど、サーバーとか書きたくない…」
そんなときのGoodなツール、それがFirebase Firestore
この章では、FirestoreをiOSアプリに組み込んで
- 実際に読み書きできるようにする
- 設計&最適化もちゃんと考え
この流れでざっくり学んでいく。
Firebaseの初期セットアップはできている前提で進めます、まだここが完了していない場合は以下を参考に。

Contents 非表示
Firebase ConsoleでFirestoreを有効化する
- Firebase Console にアクセス
- プロジェクトを選択(または新規作成)
- 左側のメニューから 「Cloud Firestore」 を選ぶ
- データベースを作成」をクリック
- テストモード(最初はこれでOK、後でセキュリティルールを締める)
- リージョンを選ぶ(
asia-northeast1
(東京)) - 完了
実装
まずは動くコードをざっくり作る
プロジェクトに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 |
自分のデータだけアクセスOK | request.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ならブロック。シンプル。
次回は画像アップロードとセキュリティのベストプラクティス編