目標
在此代碼實驗室中,您將在Swift中的iOS上構建一個由Firestore支持的餐廳推薦應用程序。你將學到如何:
- 從iOS應用讀取數據並將數據寫入Firestore
- 實時收聽Firestore數據中的更改
- 使用Firebase身份驗證和安全規則來保護Firestore數據
- 編寫複雜的Firestore查詢
先決條件
在開始此代碼實驗室之前,請確保已安裝:
- Xcode版本8.3(或更高版本)
- CocoaPods 1.2.1(或更高版本)
將Firebase添加到項目
- 轉到Firebase控制台。
- 選擇“創建新項目”,並將您的項目命名為“ Firestore iOS Codelab”。
下載代碼
首先克隆示例項目,然後在項目目錄中運行pod update
:
git clone https://github.com/firebase/friendlyeats-ios cd friendlyeats-ios pod update
在Xcode中打開FriendlyEats.xcworkspace
並運行它(Cmd + R)。該應用程序應正確編譯,並由於缺少GoogleService-Info.plist
文件而立即在啟動時崩潰。我們將在下一步中進行更正。
設置Firebase
按照文檔創建新的Firestore項目。獲得項目後,從Firebase控制台下載項目的GoogleService-Info.plist
文件,然後將其拖到Xcode項目的根目錄。再次運行該項目,以確保該應用程序配置正確並且在啟動時不再崩潰。登錄後,您將看到一個空白屏幕,如下例所示。如果您無法登錄,請確保已在Firebase控制台中的“身份驗證”下啟用了電子郵件/密碼登錄方法。
在本部分中,我們將一些數據寫入Firestore,以便我們可以填充應用程序UI。可以通過Firebase控制台手動完成此操作,但我們將在應用程序本身中進行演示,以演示基本的Firestore編寫。
我們應用程序中的主要模型對像是餐廳。 Firestore數據分為文檔,集合和子集合。我們將每家餐廳作為文檔存儲在稱為restaurants
的頂級集合中。如果您想了解更多關於公司的FireStore數據模型,閱讀有關文件和收藏的文件。
在將數據添加到Firestore之前,我們需要獲取Restaurants集合的引用。將以下內容添加到RestaurantsTableViewController.didTapPopulateButton(_:)
方法的內部for循環中。
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; } } }
稍後我們將詳細討論安全規則,但是如果您急於趕快,請查看安全規則文檔。
運行該應用程序並登錄。然後點擊左上角的“填充”按鈕,這將創建一批餐館文檔,儘管您尚未在該應用程序中看到它。
接下來,導航至Firebase控制台中的“ Firestore數據”選項卡。現在,您應該在餐廳集合中看到新條目:
恭喜,您剛從iOS應用程序將數據寫入Firestore!在下一部分中,您將學習如何從Firestore檢索數據並將其顯示在應用程序中。
在本部分中,您將學習如何從Firestore檢索數據並將其顯示在應用程序中。兩個關鍵步驟是創建查詢和添加快照偵聽器。該偵聽器將收到與查詢匹配的所有現有數據的通知,並實時接收更新。
首先,讓我們構造一個查詢,該查詢將提供默認的未過濾餐廳列表。看一下RestaurantsTableViewController.baseQuery()
的實現:
return Firestore.firestore().collection("restaurants").limit(to: 50)
該查詢最多檢索名為“ restaurants”的頂級集合中的50家餐館。現在我們有了一個查詢,我們需要附加一個快照偵聽器以將數據從Firestore加載到我們的應用程序中。在調用stopObserving()
之後,將以下代碼添加到RestaurantsTableViewController.observeQuery()
方法。
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
),顯示數據僅是分配一些視圖屬性。將以RestaurantsTableViewController.swift
添加到RestaurantTableViewCell.populate(restaurant:)
中。
nameLabel.text = restaurant.name
cityLabel.text = restaurant.city
categoryLabel.text = restaurant.category
starsView.rating = Int(restaurant.averageRating.rounded())
priceLabel.text = priceString(from: restaurant.price)
從表格視圖數據源的tableView(_:cellForRowAtIndexPath:)
方法調用此填充方法,該方法負責將值類型的集合從以前映射到各個表格視圖單元格。
再次運行該應用程序,並驗證我們先前在控制台中看到的餐廳現在在模擬器或設備上可見。如果您成功完成了本節,則您的應用程序現在正在使用Cloud Firestore讀寫數據!
當前,我們的應用程序顯示餐廳列表,但用戶無法根據需要進行過濾。在本部分中,您將使用Firestore的高級查詢來啟用過濾。
這是獲取所有點心餐館的簡單查詢的示例:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
顧名思義, whereField(_:isEqualTo:)
方法將使我們的查詢僅下載其字段符合我們設置的限制的集合成員。在這種情況下,它將僅下載category
為"Dim Sum"
餐館。
在此應用中,用戶可以鏈接多個過濾器以創建特定的查詢,例如“舊金山披薩”或“大眾化訂購的洛杉磯海鮮”。
打開RestaurantsTableViewController.swift
並將以下代碼塊添加到query(withCategory:city:price:sortBy:)
的中間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)
}
上面的代碼段添加了多個whereField
和order
子句,以根據用戶輸入構建單個複合查詢。現在,我們的查詢將僅返回符合用戶要求的餐館。
運行您的項目,並確認您可以按價格,城市和類別進行過濾(確保准確鍵入類別和城市名稱)。在測試時,您可能會在日誌中看到如下所示的錯誤:
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中索引的更多信息,請訪問文檔。
在本部分中,我們將為用戶添加向餐廳提交評論的功能。到目前為止,我們所有的著作都是原子的並且相對簡單。如果其中任何一個錯誤,我們可能會提示用戶重試或自動重試。
為了給餐廳添加評分,我們需要協調多個讀寫操作。首先必須提交評論本身,然後需要更新餐廳的評分數和平均評分。如果其中之一失敗而另一失敗,那麼我們將處於不一致狀態,即數據庫某一部分的數據與另一部分的數據不匹配。
幸運的是,Firestore提供了事務處理功能,使我們能夠在單個原子操作中執行多次讀取和寫入操作,從而確保我們的數據保持一致。
在RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:)
所有let聲明下面添加以下代碼。
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安全規則來保護我們的數據。
首先,讓我們更深入地了解在代碼實驗室開始時編寫的安全規則。打開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
變量是所有規則中都可用的全局變量,並且我們添加的條件可確保在允許用戶執行任何操作之前對請求進行身份驗證。這樣可以防止未經身份驗證的用戶使用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;
}
}
}
第一個match語句匹配屬於restaurants
集合的任何文檔的名為“ ratings
”的子集合。然後,如果評論的用戶ID與該用戶的ID不匹配,則條件為allow write
條件將阻止提交任何評論。第二個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;
}
}
}
在這裡,我們將寫入權限分為創建和更新,因此我們可以更具體地確定應該允許哪些操作。任何用戶都可以將餐廳寫入數據庫,保留在代碼實驗室開始時所做的“填充”按鈕的功能,但是一旦寫入餐廳,其名稱,位置,價格和類別就無法更改。更具體地說,最後一條規則要求所有餐廳更新操作都必須與數據庫中現有字段的名稱,城市,價格和類別保持相同。
要了解有關如何使用安全規則的更多信息,請查看文檔。
在此代碼實驗室中,您學習瞭如何使用Firestore進行基本讀寫,以及如何使用安全規則保護數據訪問的安全。您可以在codelab-complete
分支上找到完整的解決方案。
要了解有關Firestore的更多信息,請訪問以下資源: