Cloud Firestore iOS Codelab

1。概要

目標

このコードラボでは、SwiftのiOSでFirestoreが支援するレストランおすすめアプリを作成します。次の方法を学習します:

  1. iOSアプリからFirestoreへのデータの読み取りと書き込み
  2. Firestoreデータの変更をリアルタイムで聞く
  3. Firebase認証とセキュリティルールを使用してFirestoreデータを保護する
  4. 複雑なFirestoreクエリを作成する

前提条件

このコードラボを開始する前に、次のものがインストールされていることを確認してください。

  • Xcodeバージョン8.3(またはそれ以降)
  • CocoaPods 1.2.1(またはそれ以降)

2.Firebaseコンソールプロジェクトを作成します

プロジェクトにFirebaseを追加する

  1. 行くFirebaseコンソール
  2. 新規プロジェクトを作成]を選択し、プロジェクト「Firestore iOSのコードラボ」という名前を付けます。

3.サンプルプロジェクトを入手する

コードをダウンロードする

クローニングすることから始めますサンプルプロジェクトをして実行しているpod updateプロジェクトディレクトリに:

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

オープンFriendlyEats.xcworkspace Xcodeで、それを実行します(CMD + R)。アプリが正しくコンパイルし、すぐにそれが欠けているため、起動時にクラッシュしGoogleService-Info.plistファイルを。次のステップで修正します。

Firebaseを設定する

フォロードキュメント新しいFirestoreプロジェクトを作成するに。あなたのプロジェクトを持ってたら、プロジェクトのダウンロードGoogleService-Info.plistからファイルをFirebaseコンソールとXcodeプロジェクトのルートにドラッグします。プロジェクトを再度実行して、アプリが正しく構成され、起動時にクラッシュしないことを確認します。ログインすると、次の例のような空白の画面が表示されます。ログインできない場合は、Firebaseコンソールの[認証]で[メール/パスワードのログイン方法]が有効になっていることを確認してください。

10a0671ce8f99704.png

4.Firestoreにデータを書き込む

このセクションでは、アプリのUIにデータを入力できるように、Firestoreにデータを書き込みます。これは、経由して手動で行うことができますFirebaseコンソールが、我々は、基本的なFirestore書き込みを実証するためのアプリ自体にそれをやります。

アプリの主なモデルオブジェクトはレストランです。 Firestoreデータは、ドキュメント、コレクション、およびサブコレクションに分割されます。我々はと呼ばれるトップレベルのコレクション内の文書としての各レストランを格納するrestaurants 。あなたはより多くのFirestoreデータモデルについて学習したい場合は、内のドキュメントやコレクションについて読んドキュメント

Firestoreにデータを追加する前に、レストランコレクションへの参照を取得する必要があります。 forループの内側に、以下を追加しますRestaurantsTableViewController.didTapPopulateButton(_:)方法。

let collection = Firestore.firestore().collection("restaurants")

コレクション参照ができたので、いくつかのデータを書き込むことができます。追加したコードの最後の行の直後に次を追加します。

let collection = Firestore.firestore().collection("restaurants")

// ====== ADD THIS ======
let restaurant = Restaurant(
  name: name,
  category: category,
  city: city,
  price: price,
  ratingCount: 0,
  averageRating: 0
)

collection.addDocument(data: restaurant.dictionary)

上記のコードは、レストランコレクションに新しいドキュメントを追加します。ドキュメントデータは、Restaurant構造体から取得した辞書から取得されます。

Firestoreにドキュメントを書き込む前に、Firestoreのセキュリティルールを開き、データベースのどの部分をどのユーザーが書き込み可能にするかを説明する必要があります。今のところ、認証されたユーザーのみがデータベース全体の読み取りと書き込みを許可します。これは本番アプリには少し寛容すぎますが、アプリの構築プロセスでは、実験中に認証の問題が発生しないように、十分にリラックスしたものが必要です。このコードラボの最後で、セキュリティルールを強化し、意図しない読み取りと書き込みの可能性を制限する方法について説明します。

規則]タブFirebaseコンソールの次のルールを追加して公開]をクリックします。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

私たちは、後に詳細にセキュリティルールを議論するが、あなたは急いでいる場合は、を見てみましょうセキュリティルールのドキュメントを

でアプリや記号を実行します。そして、あなたはまだアプリでこれを見ることはありませんが、レストランのドキュメントのバッチを作成します。これは、左上に「移入」ボタンをタップします。

次に、へナビゲートFirestoreデータタブFirebaseコンソールインチこれで、restaurantsコレクションに新しいエントリが表示されます。

スクリーンショット2017-07-06at 12.45.38 PM.png

おめでとうございます。iOSアプリからFirestoreにデータを書き込んだところです。次のセクションでは、Firestoreからデータを取得してアプリに表示する方法を学習します。

5.Firestoreからのデータを表示します

このセクションでは、Firestoreからデータを取得してアプリに表示する方法を学習します。 2つの重要なステップは、クエリの作成とスナップショットリスナーの追加です。このリスナーは、クエリに一致するすべての既存のデータを通知され、リアルタイムで更新を受信します。

まず、デフォルトのフィルタリングされていないレストランのリストを提供するクエリを作成しましょう。実装を見てくださいRestaurantsTableViewController.baseQuery()

return Firestore.firestore().collection("restaurants").limit(to: 50)

このクエリは、「restaurants」という名前のトップレベルコレクションの最大50のレストランを取得します。クエリができたので、スナップショットリスナーをアタッチして、Firestoreからアプリにデータを読み込む必要があります。次のコードを追加します。 RestaurantsTableViewController.observeQuery()だけを呼び出した後の方法をstopObserving()

listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
  guard let snapshot = snapshot else {
    print("Error fetching snapshot results: \(error!)")
    return
  }
  let models = snapshot.documents.map { (document) -> Restaurant in
    if let model = Restaurant(dictionary: document.data()) {
      return model
    } else {
      // Don't use fatalError here in a real app.
      fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
    }
  }
  self.restaurants = models
  self.documents = snapshot.documents

  if self.documents.count > 0 {
    self.tableView.backgroundView = nil
  } else {
    self.tableView.backgroundView = self.backgroundView
  }

  self.tableView.reloadData()
}

上記のコードは、Firestoreからコレクションをダウンロードし、ローカルの配列に格納します。 addSnapshotListener(_:)コールは、ビューコントローラにサーバー上のデータが変更されるたびに更新されますクエリにスナップショット・リスナーを追加します。更新は自動的に取得され、手動で変更をプッシュする必要はありません。このスナップショットリスナーは、サーバー側の変更の結果としていつでも呼び出すことができるため、アプリが変更を処理できることが重要です。

構造体への私たちの辞書をマッピングした後(参照Restaurant.swift )、データを表示することは、いくつかのビューのプロパティを割り当てるだけの問題です。次の行を追加しRestaurantTableViewCell.populate(restaurant:)RestaurantsTableViewController.swift

nameLabel.text = restaurant.name
cityLabel.text = restaurant.city
categoryLabel.text = restaurant.category
starsView.rating = Int(restaurant.averageRating.rounded())
priceLabel.text = priceString(from: restaurant.price)

このpopulateメソッドは、テーブルビューのデータソースのから呼び出されtableView(_:cellForRowAtIndexPath:)個々のテーブルビューのセルに前からの値型のコレクションをマッピングするの世話をする方法。

アプリを再度実行し、コンソールで以前に表示したレストランがシミュレーターまたはデバイスに表示されることを確認します。このセクションを正常に完了すると、アプリはCloudFirestoreでデータの読み取りと書き込みを行うようになります。

2ca7f8c6052f7f79.png

6.データの並べ替えとフィルタリング

現在、私たちのアプリはレストランのリストを表示していますが、ユーザーがニーズに基づいてフィルタリングする方法はありません。このセクションでは、Firestoreの高度なクエリを使用してフィルタリングを有効にします。

すべての点心レストランを取得するための簡単なクエリの例を次に示します。

let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")

その名前が示すように、 whereField(_:isEqualTo:)メソッドは、クエリのダウンロードフィールド、我々が設定された制限を満たすコレクションのメンバーのみを行います。この場合、それはレストランのみダウンロードしますcategoryある"Dim Sum"

このアプリでは、ユーザーは複数のフィルターをチェーンして、「サンフランシスコのピザ」や「人気順に並べられたロサンゼルスのシーフード」などの特定のクエリを作成できます。

オープンRestaurantsTableViewController.swiftとの真ん中に次のコードブロックを追加しquery(withCategory:city:price:sortBy:)

if let category = category, !category.isEmpty {
  filtered = filtered.whereField("category", isEqualTo: category)
}

if let city = city, !city.isEmpty {
  filtered = filtered.whereField("city", isEqualTo: city)
}

if let price = price {
  filtered = filtered.whereField("price", isEqualTo: price)
}

if let sortBy = sortBy, !sortBy.isEmpty {
  filtered = filtered.order(by: sortBy)
}

上記のスニペットは、複数の追加whereFieldorderユーザの入力に基づいて単一の複合クエリを構築するために句を。これで、クエリはユーザーの要件に一致するレストランのみを返します。

プロジェクトを実行し、価格、都市、カテゴリでフィルタリングできることを確認します(カテゴリと都市名を正確に入力してください)。テスト中に、次のようなエラーがログに表示される場合があります。

Error fetching snapshot results: Error Domain=io.grpc Code=9 
"The query requires an index. You can create it here: https://console.firebase.google.com/project/testapp-5d356/database/firestore/indexes?create_index=..." 
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_index=...}

これは、Firestoreがほとんどの複合クエリにインデックスを必要とするためです。クエリにインデックスを要求することで、Firestoreを大規模に高速に保つことができます。エラーメッセージからリンクを開くと、自動的に記入正しいパラメータとFirebaseコンソールでインデックス作成のUIを開きます。、Firestoreにおけるインデックスの詳細について学ぶためにマニュアルを参照してください

7.トランザクションでのデータの書き込み

このセクションでは、ユーザーがレストランにレビューを送信する機能を追加します。これまでのところ、すべての書き込みはアトミックで比較的単純です。エラーが発生した場合は、ユーザーに再試行するか、自動的に再試行するように求めるメッセージが表示される可能性があります。

レストランに評価を追加するには、複数の読み取りと書き込みを調整する必要があります。最初にレビュー自体を送信する必要があり、次にレストランの評価数と平均評価を更新する必要があります。これらの一方が失敗し、もう一方が失敗しない場合、データベースのある部分のデータが別の部分のデータと一致しないという一貫性のない状態になります。

幸い、Firestoreは、単一のアトミック操作で複数の読み取りと書き込みを実行できるトランザクション機能を提供し、データの一貫性を維持します。

内のすべてのlet宣言の下に次のコードを追加します。 RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:)

let firestore = Firestore.firestore()
firestore.runTransaction({ (transaction, errorPointer) -> Any? in

  // Read data from Firestore inside the transaction, so we don't accidentally
  // update using stale client data. Error if we're unable to read here.
  let restaurantSnapshot: DocumentSnapshot
  do {
    try restaurantSnapshot = transaction.getDocument(reference)
  } catch let error as NSError {
    errorPointer?.pointee = error
    return nil
  }

  // Error if the restaurant data in Firestore has somehow changed or is malformed.
  guard let data = restaurantSnapshot.data(),
        let restaurant = Restaurant(dictionary: data) else {

    let error = NSError(domain: "FireEatsErrorDomain", code: 0, userInfo: [
      NSLocalizedDescriptionKey: "Unable to write to restaurant at Firestore path: \(reference.path)"
    ])
    errorPointer?.pointee = error
    return nil
  }

  // Update the restaurant's rating and rating count and post the new review at the 
  // same time.
  let newAverage = (Float(restaurant.ratingCount) * restaurant.averageRating + Float(review.rating))
      / Float(restaurant.ratingCount + 1)

  transaction.setData(review.dictionary, forDocument: newReviewReference)
  transaction.updateData([
    "numRatings": restaurant.ratingCount + 1,
    "avgRating": newAverage
  ], forDocument: reference)
  return nil
}) { (object, error) in
  if let error = error {
    print(error)
  } else {
    // Pop the review controller on success
    if self.navigationController?.topViewController?.isKind(of: NewReviewViewController.self) ?? false {
      self.navigationController?.popViewController(animated: true)
    }
  }
}

更新ブロック内では、トランザクションオブジェクトを使用して行うすべての操作は、Firestoreによって単一のアトミック更新として扱われます。サーバーで更新が失敗した場合、Firestoreは自動的に数回再試行します。これは、エラー状態が繰り返し発生する単一のエラーである可能性が高いことを意味します。たとえば、デバイスが完全にオフラインである場合や、ユーザーが書き込もうとしているパスへの書き込みを許可されていない場合などです。

8.セキュリティルール

アプリのユーザーは、データベース内のすべてのデータを読み書きできるわけではありません。たとえば、誰もがレストランの評価を表示できる必要がありますが、認証されたユーザーのみが評価を投稿できるようにする必要があります。クライアントで適切なコードを記述するだけでは不十分です。完全に安全であるためには、バックエンドでデータセキュリティモデルを指定する必要があります。このセクションでは、Firebaseセキュリティルールを使用してデータを保護する方法を学習します。

まず、コードラボの冒頭で作成したセキュリティルールについて詳しく見ていきましょう。 Firebaseコンソールとナビゲートを開きデータベース> Firestore]タブでルール

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

requestルール内の変数は、上記のすべてのルールで使用可能なグローバル変数であり、条件付き我々は、要求が、ユーザーが何かをする許可する前に認証されることを保証を追加しました。これにより、認証されていないユーザーがFirestoreAPIを使用してデータに不正な変更を加えることを防ぎます。これは良いスタートですが、Firestoreルールを使用してはるかに強力なことを行うことができます。

レビューのユーザーIDが認証されたユーザーのIDと一致する必要があるように、レビューの書き込みを制限しましょう。これにより、ユーザーがお互いになりすまして不正なレビューを残すことがなくなります。セキュリティルールを次のように置き換えます。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{any}/ratings/{rating} {
      // Users can only write ratings with their user ID
      allow read;
      allow write: if request.auth != null 
                   && request.auth.uid == request.resource.data.userId;
    }
  
    match /restaurants/{any} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

最初に一致した文は、サブコレクションという名前の一致したratingsに属する任意の文書のrestaurantsコレクションを。 allow write条件は、レビューのユーザIDは、ユーザのものと一致しない場合は提出されてから任意の見直しを防ぐことができます。 2番目のmatchステートメントを使用すると、認証されたユーザーはレストランの読み取りとデータベースへの書き込みを行うことができます。

これは、セキュリティルールを使用して、以前にアプリに書き込んだ暗黙の保証を明示的に示しているため、レビューには非常に効果的です。つまり、ユーザーは自分のレビューしか書くことができません。レビューの編集または削除機能を追加する場合、これとまったく同じルールセットにより、ユーザーが他のユーザーのレビューを変更または削除することもできなくなります。ただし、Firestoreルールをより詳細に使用して、ドキュメント全体ではなく、ドキュメント内の個々のフィールドへの書き込みを制限することもできます。これを使用して、ユーザーがレストランの評価、平均評価、および評価数のみを更新できるようにし、悪意のあるユーザーがレストランの名前や場所を変更する可能性を排除できます。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{restaurant} {
      match /ratings/{rating} {
        allow read: if request.auth != null;
        allow write: if request.auth != null 
                     && request.auth.uid == request.resource.data.userId;
      }
    
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
                    && request.resource.data.city == resource.data.city
                    && request.resource.data.price == resource.data.price
                    && request.resource.data.category == resource.data.category;
    }
  }
}

ここでは、書き込み権限を作成と更新に分割して、許可する操作をより具体的にできるようにしました。コードラボの開始時に作成した[入力]ボタンの機能を維持したまま、すべてのユーザーがデータベースにレストランを書き込むことができますが、レストランが書き込まれると、その名前、場所、価格、およびカテゴリを変更することはできません。より具体的には、最後のルールでは、データベース内の既存のフィールドと同じ名前、都市、価格、およびカテゴリを維持するために、レストランの更新操作が必要です。

あなたがセキュリティルールで何ができるかについての詳細を学ぶために、見とるドキュメントを

9.結論

このコードラボでは、Firestoreを使用した基本的および高度な読み取りと書き込みの方法、およびセキュリティルールを使用してデータアクセスを保護する方法を学習しました。あなたは上の完全な解決策を見つけることができますcodelab-completeブランチを

Firestoreの詳細については、次のリソースにアクセスしてください。