#始めに
本ドキュメントは次の4点を目標とします。
- セキュリティを考慮したFirestore設定を行う
- アプリからFirestoreへ書き込む
- Firestoreからアプリへ読み込む
- Firestoreを監視し、アプリで表示する情報をリアルタイム更新する
執筆時の開発環境は次の通りです。
- Xcode 12.4
- Swift 5
Xcodeプロジェクトとそれに対応するFirebaseプロジェクトが作成済みであり、iOS側のFirebase初期導入手順は実装済みである事が前提です。
また次のライブラリが必要です。
- FirebaseFirestore
- FirebaseFirestoreSwift
#実装
次の仕様を満たすサンプルを実装します。
- Firebaseにサインインし、Firestoreに日記データの入出力を行う
##Firebaseの設定
###Firestoreデータベースの作成
Firestore画面を表示し、データベースの作成をクリックします。
データベースの作成画面では「テストモードで開始する」を選択し、次へボタンをクリックします。
重要:
公式ドキュメントにも記載されていますが、アプリからFirestoreにアクセスする場合、必ず「テストモード」で開始してください。
本番環境モードで開始した場合、その後セキュリティルールを全解放にしてもアプリからアクセスできませんでした。
好きなロケーションを選択し、有効にするをクリックするとFirestoreデータベースが作成されます。
###Firestoreルールの設定
作成するデータベースは次のイメージです。
日記コレクションに対し次のルールを設定します。
- 未サインインユーザはデータベースにアクセスできない
- 他人の日記データにはアクセスできない
Firestore画面のルールタブを選択すると、初期のルールが記述されています。
次のルールに書き換え、公開をクリックします。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /diary {
match /{document} {
allow read, update, delete: if request.auth != null && resource.data.uid == request.auth.uid;
allow create: if request.auth != null;
}
}
}
}
##iOSアプリの実装
次の流れを実装します。
- Firebaseにサインインする
- 日記をFirestoreに保存する
- Firestoreを監視し、リアルタイムに日記一覧を更新する
###サインイン機能
次の記事で紹介しています。
https://qiita.com/ICTFractal/items/52d9ffdc85f50ccba657
###日記データ構造体
FirebaseFirestoreSwiftフレームワークを使用すると、自動的にFirestoreから得られるデータを構造体にマップできます。
また構造体から直接Firestoreに書き込むことも可能になり、非常に便利です。
マップする際定型的な処理が必要になる為、まず定型処理をラップしたプロトコルを実装します。
import Firebase
import FirebaseFirestoreSwift
protocol FirestoreDataCodable: Codable {
var id: String? { get set }
static func targetCollectionRef() -> CollectionReference
}
// MARK: - Static
extension FirestoreDataCodable {
typealias ComplesionClosure = (Error?)->Void
static func from(document: QueryDocumentSnapshot) -> Self? {
do {
return try document.data(as: Self.self)
} catch {
debugPrint(error.localizedDescription)
}
return nil
}
static func from(collection: QuerySnapshot) -> [Self] {
return collection.documents.compactMap { Self.from(document: $0) }
}
}
// MARK: - Internal
extension FirestoreDataCodable {
func set(complesion: ComplesionClosure?) {
do {
let collectionRef = Self.targetCollectionRef()
let documentRef = id == nil ? collectionRef.document() : collectionRef.document(id!)
try documentRef.setData(from: self) { error in
if let error = error {
debugPrint(error.localizedDescription)
}
complesion?(error)
}
} catch {
debugPrint(error.localizedDescription)
complesion?(error)
}
}
}
次にFirestoreDataCodableプロトコルを採用した日記データ構造体を実装します。
import Firebase
import FirebaseFirestoreSwift
struct DiaryModel: FirestoreDataCodable {
@DocumentID var id: String?
let uid: String
let body: String
let title: String
static func targetCollectionRef() -> CollectionReference {
return Firestore.firestore().collection("diary")
}
}
targetCollectionRef()
は入出力を行うFirestore内の場所を返します。
@DocumentID
を指定すると、データ取得時にドキュメントIDが格納されます。
###画面設計
StoryboardでViewController上にUITableViewを配置し、次の設定を行います。
- 「tableView」という名前でUITableViewのプロパティを作成
- dataSourceをViewControllerに設定
- delegateをViewControllerに設定
###日記保存処理
tableViewのフッターに日記保存ボタンを配置し、タップ時の保存処理を実装します。
またアプリを実行する為、tableView周りの最低限の処理を実装します。
class ViewController: UIViewController {
private var diary = [DiaryModel]()
}
private extension ViewController {
func addDiary() {
guard let uid = AccountManager.shared.uid else { return }
DiaryModel(uid: uid, body: "本文", title: Date().debugDescription).set { (error) in
if let error = error {
debugPrint(error.localizedDescription)
} else {
debugPrint("保存しました")
}
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return diary.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
}
let data = diary[indexPath.row]
cell?.textLabel?.text = data.title
cell?.detailTextLabel?.text = data.body
return cell!
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let button = UIButton(type: .roundedRect)
button.backgroundColor = .blue
button.setTitleColor(.white, for: .normal)
button.setTitle("保存", for: .normal)
button.addAction(.init(handler: { (_) in
self.addDiary()
}), for: .touchUpInside)
return button
}
}
addDiary()
で日記データをFirestoreに保存しています。
アプリを実行し、保存ボタンをタップします。
「保存しました」とログに出力されれば正常に処理が終了しています。
FirebaseコンソールからFirestoreデータを表示すると、日記データが保存されていることが確認できます。
###Firestoreの監視とtableViewのリアルタイム更新
Firestoreから取得した日記データをtableViewに表示します。
またFirestoreを監視し、tableViewを常に最新の状態に更新します。
import Firebase
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
listenDiary()
}
}
private extension ViewController {
func listenDiary() {
guard let uid = AccountManager.shared.uid else { return }
DiaryModel.targetCollectionRef().whereField("uid", isEqualTo: uid).addSnapshotListener { [weak self] (snapshot, error) in
if let error = error {
debugPrint(error.localizedDescription)
}
if let collection = snapshot {
self?.diary = DiaryModel.from(collection: collection)
self?.tableView.reloadSections(.init(integer: 0), with: .automatic)
}
}
}
}
viewDidLoad()
でlistenDiary()
を実行し、Firestoreの監視を開始しています。
DiaryModel.from(collection: collection)
で受信データを構造体にマップしています。
本サンプルのようにコレクションを指定して複数のドキュメントを得る場合、snapshot
の型はQuerySnapshot
となります。
ドキュメントIDを指定して単一のドキュメントを得る場合のsnapshot
の型はQueryDocumentSnapshot
です。
単一ドキュメントを構造体にマップしたい場合はDiaryModel.from(document:)
を使います。
アプリを実行し、先ほど保存した日記データが表示されることが確認できます。
また、新たに保存する度に一覧が更新されることが確認できます。
正しく監視できていない場合、listenDiary()
コール時点でFirebaseにサインインされているかを確認してください。