Cloud Firestore iOS 代碼實驗室

目標

在此 Codelab 中,您將在 iOS 上使用 Swift 構建 Firestore 支持的餐廳推薦應用。你將學到如何:

  1. 從 iOS 應用讀取數據並將數據寫入 Firestore
  2. 實時監聽 Firestore 數據的變化
  3. 使用 Firebase 身份驗證和安全規則來保護 Firestore 數據
  4. 編寫複雜的 Firestore 查詢

先決條件

在開始此代碼實驗室之前,請確保您已安裝:

  • Xcode 8.3(或更高版本)
  • CocoaPods 1.2.1(或更高版本)

將 Firebase 添加到項目

  1. 轉至火力地堡控制台
  2. 選擇創建新的項目和項目命名為“iOS版的FireStore程式碼實驗室”。

下載代碼

通過克隆開始樣品項目和運行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從文件火力地堡控制台,並拖動到Xcode項目的根。再次運行項目以確保應用程序配置正確並且在啟動時不再崩潰。登錄後,您應該會看到如下例所示的空白屏幕。如果您無法登錄,請確保您已在 Firebase 控制台中的身份驗證下啟用電子郵件/密碼登錄方法。

10a0671ce8f99704.png

在本節中,我們將向 Firestore 寫入一些數據,以便我們可以填充應用 UI。這可以通過手動調節來完成火力地堡控制台,但我們會在應用程序本身來證明一個基本的公司的FireStore寫做。

我們應用程序中的主要模型對像是一家餐廳。 Firestore 數據分為文檔、集合和子集合。我們將存儲每個餐廳中稱為頂級集合文件restaurants 。如果您想了解更多關於公司的FireStore數據模型,閱讀有關文件和收藏的文件

在向 Firestore 添加數據之前,我們需要獲取對餐廳集合的引用。下面添加到內中環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 的安全規則並描述我們數據庫的哪些部分應該由哪些用戶寫入。現在,我們將只允許經過身份驗證的用戶讀取和寫入整個數據庫。這對於生產應用程序來說有點過於寬鬆,但在應用程序構建過程中,我們希望有一些足夠放鬆的東西,這樣我們就不會在試驗時不斷遇到身份驗證問題。在本 Codelab 的最後,我們將討論如何強化安全規則並限制意外讀取和寫入的可能性。

規則選項卡的火力地堡控制台添加下面的規則,然後單擊發布

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數據標籤在火力地堡控制台。您現在應該會在餐廳集合中看到新條目:

屏幕截圖 2017-07-06 at 12.45.38 PM.png

恭喜,您剛剛從 iOS 應用程序向 Firestore 寫入數據!在下一部分中,您將學習如何從 Firestore 檢索數據並將其顯示在應用程序中。

在本節中,您將學習如何從 Firestore 檢索數據並將其顯示在應用程序中。兩個關鍵步驟是創建查詢和添加快照偵聽器。此偵聽器將收到與查詢匹配的所有現有數據的通知,並實時接收更新。

首先,讓我們構建一個查詢,該查詢將為默認的、未過濾的餐館列表提供服務。看看執行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:)方法,它負責從映射值類型的集合的前向個體表視圖細胞。

再次運行應用程序並驗證我們之前在控制台中看到的餐廳現在在模擬器或設備上是否可見。如果您成功完成了本部分,您的應用現在正在使用 Cloud Firestore 讀取和寫入數據!

2ca7f8c6052f7f79.png

目前,我們的應用程序顯示餐廳列表,但用戶無法根據自己的需要進行過濾。在本節中,您將使用 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 快速擴展。從錯誤信息打開鏈接就會自動打開與填入正確的參數的火力地堡控制台索引創建UI。要了解更多關於公司的FireStore索引,訪問文件

在本節中,我們將添加用戶向餐廳提交評論的功能。到目前為止,我們所有的寫入都是原子的並且相對簡單。如果其中任何一個出錯,我們可能只會提示用戶重試或自動重試。

為了給餐廳添加評級,我們需要協調多次讀取和寫入。首先必須提交評論本身,然後需要更新餐廳的評分計數和平均評分。如果其中一個失敗,而另一個沒有失敗,我們就會處於不一致的狀態,即數據庫一部分中的數據與另一部分中的數據不匹配。

幸運的是,Firestore 提供了事務功能,讓我們可以在單個原子操作中執行多次讀取和寫入,從而確保我們的數據保持一致。

添加以下代碼下面的所有讓利聲明中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 會自動重試幾次。這意味著我們的錯誤條件很可能是重複發生的單個錯誤,例如,如果設備完全脫機或用戶無權寫入他們嘗試寫入的路徑。

我們應用程序的用戶不應該能夠讀取和寫入我們數據庫中的每條數據。例如,每個人都應該能夠看到餐廳的評分,但只有經過身份驗證的用戶才能發布評分。在客戶端寫好代碼是不夠的,我們需要在後端指定我們的數據安全模型才能完全安全。在本節中,我們將學習如何使用 Firebase 安全規則來保護我們的數據。

首先,讓我們深入了解一下我們在 codelab 開始時編寫的安全規則。打開火力地堡控制台並導航到數據庫>規則在公司的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在上面的規則的變量是在所有規則中可用的全局變量和條件,我們加入保證了請求允許用戶做任何事情之前要驗證。這可以防止未經身份驗證的用戶使用 Firestore API 對您的數據進行未經授權的更改。這是一個好的開始,但我們可以使用 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不匹配用戶的提交。第二個 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;
    }
  }
}

在這裡,我們將我們的寫入權限拆分為創建和更新,以便我們可以更具體地確定應該允許哪些操作。任何用戶都可以將餐廳寫入數據庫,保留我們在代碼實驗室開始時創建的 Populate 按鈕的功能,但是一旦寫入餐廳,其名稱、位置、價格和類別就無法更改。更具體地說,最後一條規則要求任何餐廳更新操作保持數據庫中現有字段的名稱、城市、價格和類別相同。

要了解更多有關您可以用安全規則做什麼,看看該文檔

在此 Codelab 中,您學習瞭如何使用 Firestore 進行基本和高級讀寫,以及如何使用安全規則保護數據訪問。你可以找到的完整的解決方案codelab-complete分支

要了解有關 Firestore 的更多信息,請訪問以下資源: