💽
SwiftUI × Firestoreでデータ保存を実装する
1. はじめに
以前実装したFirebase Authを使った認証機能追加の延長線で、今回はFirestoreを使ったクラウドへのデータ保存を実装したいと思います。
2. 事前準備
以下のリンク先の記事を参考に「プロジェクトの作成」と「アプリの登録」を行います。
3. Firestore Databaseでデータベースを作成する
Firestore Databaseからデータベースを作成する
「データベースID」と「ロケーション」を指定する
セキュリティルールを設定する
4. セキュリティルールを変更する
以下のコードを参考にセキュリティルールを変更する
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 認証されたユーザーのみ読み書き可能(開発用)
// 本番利用の際は細かく設定すること
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
5. Xcodeプロジェクトにライブラリを追加する
以下のサイトを参考に、Xcodeプロジェクトにライブラリを追加します。
[File]>[Add Package Dependencies]から以下のURLを検索する
https://github.com/firebase/firebase-ios-sdk
「Firebase Firestore」を追加する
6. 実装
6.1. インスタンスを初期化する
💡ポイント:
- FirebaseApp.configure() は アプリケーションの起動時に一度だけ 呼び出す必要がある
- SwiftUIの場合は @main 構造体の init() 内で初期化するのが最適
- 初期化前に他のFirebase機能を使用するとクラッシュの原因となる
MyApp.swift
import SwiftUI
import Firebase
import FirebaseFirestore
@main
struct MyApp: App {
init() {
// Firebaseの初期化
FirebaseApp.configure() // ここで一度だけ初期化
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
6.2. データを追加する
💡ポイント:
Firestore.firestore()
でメインのFirestoreインスタンスを取得- コレクション参照 →
db.collection("コレクション名")
addDocument(data:)
メソッドはドキュメントIDを自動生成- データは
[String: Any]
形式のディクショナリで渡す
AddDataView.swift
import SwiftUI
import FirebaseFirestore
struct AddDataView: View {
@State private var name = ""
@State private var age = ""
@State private var message = ""
// Firestoreのインスタンスを取得
let db = Firestore.firestore()
var body: some View {
VStack {
TextField("名前", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("年齢", text: $age)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
Button("保存") {
addUser()
}
Text(message)
}
.padding()
}
func addUser() {
// usersコレクションへの参照
let usersRef = db.collection("users")
// データを追加
usersRef.addDocument(data: [
"name": name,
"age": Int(age) ?? 0, // 文字列から数値への変換に注意
"createdAt": Timestamp(date: Date()) // タイムスタンプの追加
]) { error in
if let error = error {
message = "エラー: \(error.localizedDescription)"
} else {
message = "ユーザーを追加しました"
name = ""
age = ""
}
}
}
}
6.3. データを取得する
単一ドキュメントの取得
💡ポイント:
- キュメント参照 →
db.collection("コレクション名").document("ドキュメントID")
getDocument()
メソッドは非同期で動作する- 取得したドキュメントの存在確認
(document.exists)
が重要- データは
document.data()
で取得でき、型は[String: Any]?
UserDetailView.swift
import SwiftUI
import FirebaseFirestore
struct UserDetailView: View {
let userId: String
@State private var user: [String: Any]?
@State private var message = ""
let db = Firestore.firestore()
var body: some View {
VStack {
if let user = user {
Text("名前: \(user["name"] as? String ?? "")")
Text("年齢: \(String(describing: user["age"] ?? ""))")
} else {
Text("ユーザー情報を読み込み中...")
}
Text(message)
}
.onAppear {
fetchUser()
}
}
func fetchUser() {
// ドキュメントIDを指定して単一のドキュメントを取得
db.collection("users").document(userId).getDocument { document, error in
if let error = error {
message = "エラー: \(error.localizedDescription)"
return
}
if let document = document, document.exists {
// document.exists で存在確認を行う
user = document.data() // データを取得
message = "ユーザー情報を取得しました"
} else {
message = "ユーザーが見つかりません"
}
}
}
}
コレクション内のすべてのドキュメントを取得
💡ポイント:
getDocuments()
メソッドでコレクション全体を取得- 結果は
querySnapshot.documents
の配列として返される- 各ドキュメントの
data()
メソッドでデータにアクセス- 大量のデータがある場合はページネーションを検討する
UserListView.swift
import SwiftUI
import FirebaseFirestore
struct UserListView: View {
@State private var users: [[String: Any]] = []
@State private var message = ""
let db = Firestore.firestore()
var body: some View {
VStack {
List {
ForEach(users, id: \.self) { user in
VStack(alignment: .leading) {
Text("名前: \(user["name"] as? String ?? "")")
Text("年齢: \(String(describing: user["age"] ?? ""))")
}
}
}
Text(message)
}
.onAppear {
fetchAllUsers()
}
}
func fetchAllUsers() {
db.collection("users").getDocuments { querySnapshot, error in
if let error = error {
message = "エラー: \(error.localizedDescription)"
return
}
guard let documents = querySnapshot?.documents else {
message = "ドキュメントがありません"
return
}
// ドキュメントの配列を作成
users = documents.map { $0.data() } // map関数でデータ変換
message = "\(users.count)人のユーザーを取得しました"
}
}
}
リアルタイムリスナーを使用したデータ取得
💡ポイント:
addSnapshotListener
でリアルタイム更新を監視- データベースの変更があるたびに自動的にコールバックが呼ばれる
- リスナーは参照を保持して
onDisappear
などで解除するのが理想的- 複数回のリスナー設定に注意(メモリリークの原因になる)
RealtimeUserListView.swift
import SwiftUI
import FirebaseFirestore
struct RealtimeUserListView: View {
@State private var users: [[String: Any]] = []
@State private var message = ""
let db = Firestore.firestore()
var body: some View {
VStack {
List {
ForEach(users, id: \.self) { user in
VStack(alignment: .leading) {
Text("名前: \(user["name"] as? String ?? "")")
Text("年齢: \(String(describing: user["age"] ?? ""))")
}
}
}
Text(message)
}
.onAppear {
// リアルタイムリスナーを設定
listenForUsers()
}
}
func listenForUsers() {
// リアルタイムリスナーを設定
let listener = db.collection("users").addSnapshotListener { querySnapshot, error in
if let error = error {
message = "エラー: \(error.localizedDescription)"
return
}
guard let snapshot = querySnapshot else {
message = "スナップショットがありません"
return
}
// データベースの変更を自動検知
users = snapshot.documents.map { $0.data() }
message = "\(users.count)人のユーザーを監視中"
}
// リスナーを解除する場合(重要)
// listener.remove()
}
}
クエリを使用したデータ取得
💡ポイント:
whereField()
メソッドでフィルタリング条件を指定- 複数の条件を連結して使用可能
- インデックスが必要な複雑なクエリはFirebaseコンソールからの設定が必要
- クエリの実行結果は通常の
getDocuments()
と同じ形式で返される
FilteredUserListView.swift
import SwiftUI
import FirebaseFirestore
struct FilteredUserListView: View {
@State private var users: [[String: Any]] = []
@State private var message = ""
@State private var minAge = ""
let db = Firestore.firestore()
var body: some View {
VStack {
HStack {
Text("最小年齢:")
TextField("例: 20", text: $minAge)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
.frame(width: 100)
Button("検索") {
fetchFilteredUsers()
}
}
.padding()
List {
ForEach(users, id: \.self) { user in
VStack(alignment: .leading) {
Text("名前: \(user["name"] as? String ?? "")")
Text("年齢: \(String(describing: user["age"] ?? ""))")
}
}
}
Text(message)
}
}
func fetchFilteredUsers() {
guard let ageValue = Int(minAge) else {
message = "有効な年齢を入力してください"
return
}
// 条件付きクエリの実行:年齢でフィルタリングするクエリ
db.collection("users")
.whereField("age", isGreaterThanOrEqualTo: ageValue)
// 複数条件を追加できる
// .whereField("name", isEqualTo: "山田")
// .orderBy("age", descending: true)
.getDocuments { querySnapshot, error in
if let error = error {
message = "エラー: \(error.localizedDescription)"
return
}
guard let documents = querySnapshot?.documents else {
message = "該当するユーザーがいません"
users = []
return
}
users = documents.map { $0.data() }
message = "\(users.count)人のユーザーが該当しました"
}
}
}
6.4. データを更新する
💡ポイント:
updateData()
メソッドは指定したフィールドのみを更新する- 存在しないフィールドは新しく追加される
- ドキュメント全体の置き換えには代わりに
setData()
を使用updateData
はドキュメントが存在しない場合エラーになる- 更新時に
updatedAt
フィールドを追加すると変更履歴の追跡に便利
UpdateUserView.swift
import SwiftUI
import FirebaseFirestore
struct UpdateUserView: View {
let userId: String
@State private var name = ""
@State private var age = ""
@State private var message = ""
let db = Firestore.firestore()
var body: some View {
VStack {
TextField("名前", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("年齢", text: $age)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
Button("更新") {
updateUser()
}
Text(message)
}
.padding()
.onAppear {
// 現在のデータを取得
fetchCurrentData()
}
}
func fetchCurrentData() {
db.collection("users").document(userId).getDocument { document, error in
if let document = document, document.exists, let data = document.data() {
name = data["name"] as? String ?? ""
if let age = data["age"] as? Int {
self.age = String(age)
}
}
}
}
func updateUser() {
// ドキュメントを更新
db.collection("users").document(userId).updateData([
"name": name,
"age": Int(age) ?? 0,
"updatedAt": Timestamp(date: Date()) // 更新日時を記録
]) { error in
if let error = error {
message = "エラー: \(error.localizedDescription)"
} else {
message = "ユーザー情報を更新しました"
}
}
}
}
6.5. データを削除する
💡ポイント:
delete()
メソッドはドキュメント全体を削除する- 一部のフィールドだけを削除するには
updateData()
でFieldValue.delete()
を使用- 削除されたドキュメントは復元できないので確認処理を設ける
- 関連するサブコレクションは自動的に削除されないので注意
DeleteUserView.swift
import SwiftUI
import FirebaseFirestore
struct DeleteUserView: View {
let userId: String
@State private var message = ""
@Environment(\.presentationMode) var presentationMode
let db = Firestore.firestore()
var body: some View {
VStack {
Text("本当にこのユーザーを削除しますか?")
.padding()
Button("削除") {
deleteUser()
}
.foregroundColor(.red)
Text(message)
}
.padding()
}
func deleteUser() {
// ドキュメントを削除(削除操作は取り消せないので注意)
db.collection("users").document(userId).delete() { error in
if let error = error {
message = "エラー: \(error.localizedDescription)"
} else {
message = "ユーザーを削除しました"
// 少し待ってから前の画面に戻る
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
presentationMode.wrappedValue.dismiss()
}
}
}
// 特定のフィールドだけを削除する場合(例)
/*
db.collection("users").document(userId).updateData([
"age": FieldValue.delete()
])
*/
}
}
7. まとめ
今回はFirestoreを使ったデータ保存の実装手順について記載しました。
環境を整えれば実装自体は容易にできるので、ぜひ参考にしていただければと思います。
関連記事
Discussion