1. Übersicht
Ziele
In diesem Codelab erstellen Sie eine Restaurantempfehlungs-App für iOS in Swift, die auf Firestore basiert. Nach Abschluss können Sie:
- Daten aus einer iOS-App in Firestore lesen und schreiben
- Änderungen an Firestore-Daten in Echtzeit überwachen
- Firestore-Daten mit Firebase Authentication und Sicherheitsregeln schützen
- Komplexe Firestore-Abfragen schreiben
Vorbereitung
Bevor Sie mit diesem Codelab beginnen, müssen Sie Folgendes installiert haben:
- Xcode-Version 14.0 oder höher
- CocoaPods 1.12.0 oder höher
2. Beispielprojekt abrufen
Code herunterladen
Klonen Sie zuerst das Beispielprojekt und führen Sie pod update
im Projektverzeichnis aus:
git clone https://github.com/firebase/friendlyeats-ios cd friendlyeats-ios pod update
Öffnen Sie FriendlyEats.xcworkspace
in Xcode und führen Sie es aus (Cmd+R). Die App sollte korrekt kompiliert werden und beim Start sofort abstürzen, da eine GoogleService-Info.plist
-Datei fehlt. Das korrigieren wir im nächsten Schritt.
3. Firebase einrichten
Firebase-Projekt erstellen
- Melden Sie sich mit Ihrem Google-Konto in der Firebase Console an.
- Klicken Sie auf die Schaltfläche, um ein neues Projekt zu erstellen, und geben Sie dann einen Projektnamen ein (z. B.
FriendlyEats
).
- Klicken Sie auf Weiter.
- Lesen und akzeptieren Sie bei Aufforderung die Firebase-Nutzungsbedingungen und klicken Sie dann auf Weiter.
- (Optional) Aktivieren Sie die KI-Unterstützung in der Firebase Console (als „Gemini in Firebase“ bezeichnet).
- Für dieses Codelab benötigen Sie kein Google Analytics. Deaktivieren Sie daher die Google Analytics-Option.
- Klicken Sie auf Projekt erstellen, warten Sie, bis Ihr Projekt bereitgestellt wurde, und klicken Sie dann auf Weiter.
App mit Firebase verbinden
Erstellen Sie eine iOS-App in Ihrem neuen Firebase-Projekt.
Laden Sie die Datei GoogleService-Info.plist
Ihres Projekts aus der Firebase Console herunter und ziehen Sie sie in das Stammverzeichnis des Xcode-Projekts. Führen Sie das Projekt noch einmal aus, um sicherzugehen, dass die App richtig konfiguriert wird und beim Start nicht mehr abstürzt. Nach der Anmeldung sollte ein leerer Bildschirm wie im Beispiel unten angezeigt werden. Wenn Sie sich nicht anmelden können, prüfen Sie, ob Sie die Anmeldemethode „E-Mail/Passwort“ in der Firebase Console unter „Authentifizierung“ aktiviert haben.
4. Daten in Firestore schreiben
In diesem Abschnitt schreiben wir einige Daten in Firestore, damit wir die App-UI darstellen können. Das geht manuell über die Firebase Console, doch für dieses Lab tun wir dies direkt in der App, um Grundlagen des Schreibens in Firestore zu demonstrieren.
Das Hauptmodellobjekt in unserer App ist ein Restaurant. Firestore-Daten werden in Dokumente, Sammlungen und Untersammlungen aufgeteilt. Wir speichern jedes Restaurant als Dokument in einer Sammlung auf oberster Ebene namens restaurants
. Weitere Informationen zum Firestore-Datenmodell finden Sie in den Dokumenten und Sammlungen der Dokumentation.
Bevor wir Daten in Firestore hinzufügen können, müssen wir einen Verweis auf die Sammlung „restaurants“ abrufen. Fügen Sie der inneren for-Schleife in der RestaurantsTableViewController.didTapPopulateButton(_:)
-Methode Folgendes hinzu.
let collection = Firestore.firestore().collection("restaurants")
Nachdem wir eine Sammlungsreferenz haben, können wir einige Daten schreiben. Fügen Sie den folgenden Code direkt nach der letzten Codezeile ein, die wir hinzugefügt haben:
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)
Mit dem Code oben wird der Sammlung „restaurants“ ein neues Dokument hinzugefügt. Die Dokumentdaten stammen aus einem Wörterbuch, das wir aus einer Restaurant-Struktur abrufen.
Wir sind fast fertig. Bevor wir Dokumente in Firestore schreiben können, müssen wir die Sicherheitsregeln von Firestore öffnen und beschreiben, welche Teile unserer Datenbank von welchen Nutzern geschrieben werden dürfen. Derzeit erlauben wir nur authentifizierten Nutzern, die gesamte Datenbank zu lesen und in sie zu schreiben. Das ist für eine Produktions-App etwas zu permissiv, aber während der Entwicklung der App möchten wir etwas, das entspannt genug ist, damit wir beim Experimentieren nicht ständig auf Authentifizierungsprobleme stoßen. Am Ende dieses Codelabs werden wir darüber sprechen, wie Sie Ihre Sicherheitsregeln härten und die Möglichkeit unbeabsichtigter Lese- und Schreibvorgänge einschränken können.
Fügen Sie auf dem Tab „Regeln“ der Firebase Console die folgenden Regeln hinzu und klicken Sie dann auf Veröffentlichen.
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; } } }
Wir werden Sicherheitsregeln später noch genauer besprechen. Wenn Sie es eilig haben, können Sie sich aber auch die Dokumentation zu Sicherheitsregeln ansehen.
Starten Sie die App und melden Sie sich an. Tippen Sie dann links oben auf die Schaltfläche Populate (Bevölkern). Dadurch wird ein Batch mit Restaurantdokumenten erstellt, der in der App noch nicht angezeigt wird.
Rufen Sie als Nächstes in der Firebase Console den Tab „Firestore-Daten“ auf. Sie sollten jetzt neue Einträge in der Sammlung „Restaurants“ sehen:
Glückwunsch! Sie haben gerade Daten aus einer iOS-App in Firestore geschrieben. Im nächsten Abschnitt erfahren Sie, wie Sie Daten aus Firestore abrufen und in der App anzeigen.
5. Daten aus Firestore anzeigen
In diesem Abschnitt erfahren Sie, wie Sie Daten aus Firestore abrufen und in der App anzeigen. Die beiden wichtigsten Schritte sind das Erstellen einer Abfrage und das Hinzufügen eines Snapshot-Listeners. Dieser Listener wird über alle vorhandenen Daten informiert, die der Abfrage entsprechen, und erhält Updates in Echtzeit.
Erstellen wir zuerst die Abfrage, mit der die standardmäßige, ungefilterte Liste der Restaurants abgerufen wird. Sehen Sie sich die Implementierung von RestaurantsTableViewController.baseQuery()
an:
return Firestore.firestore().collection("restaurants").limit(to: 50)
Mit dieser Abfrage werden bis zu 50 Restaurants aus der Sammlung auf der obersten Ebene mit dem Namen „restaurants“ abgerufen. Nachdem wir eine Abfrage haben, müssen wir einen Snapshot-Listener anhängen, um Daten aus Firestore in unsere App zu laden. Fügen Sie den folgenden Code in die Methode RestaurantsTableViewController.observeQuery()
direkt nach dem Aufruf von stopObserving()
ein.
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()
}
Mit dem oben stehenden Code wird eine Sammlung aus Firestore heruntergeladen und lokal in einem Array gespeichert. Mit dem addSnapshotListener(_:)
-Aufruf wird der Abfrage ein Snapshot-Listener hinzugefügt, der den Ansichtscontroller jedes Mal aktualisiert, wenn sich die Daten auf dem Server ändern. Wir erhalten automatisch Updates und müssen Änderungen nicht manuell vornehmen. Dieser Snapshot-Listener kann jederzeit aufgrund einer serverseitigen Änderung aufgerufen werden. Daher ist es wichtig, dass Ihre App Änderungen verarbeiten kann.
Nachdem wir unsere Dictionaries Structs zugeordnet haben (siehe Restaurant.swift
), müssen wir nur noch einige Ansichtseigenschaften zuweisen, um die Daten darzustellen. Fügen Sie RestaurantTableViewCell.populate(restaurant:)
in RestaurantsTableViewController.swift
die folgenden Zeilen hinzu.
nameLabel.text = restaurant.name
cityLabel.text = restaurant.city
categoryLabel.text = restaurant.category
starsView.rating = Int(restaurant.averageRating.rounded())
priceLabel.text = priceString(from: restaurant.price)
Diese Methode wird von der tableView(_:cellForRowAtIndexPath:)
-Methode der Datenquelle der Tabellenansicht aufgerufen, die die Zuordnung der Sammlung von Werttypen zu den einzelnen Tabellenansichtszellen übernimmt.
Führen Sie die App noch einmal aus und prüfen Sie, ob die Restaurants, die wir zuvor in der Konsole gesehen haben, jetzt im Simulator oder auf dem Gerät angezeigt werden. Wenn Sie diesen Abschnitt erfolgreich abgeschlossen haben, liest und schreibt Ihre App jetzt Daten mit Cloud Firestore.
6. Daten sortieren und filtern
Derzeit wird in unserer App eine Liste von Restaurants angezeigt, aber Nutzer können nicht nach ihren Bedürfnissen filtern. In diesem Abschnitt verwenden Sie die erweiterten Abfragefunktionen von Firestore, um das Filtern zu ermöglichen.
Hier ist ein Beispiel für eine einfache Abfrage, mit der alle Dim-Sum-Restaurants abgerufen werden:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
Wie der Name schon sagt, werden mit der Methode whereField(_:isEqualTo:)
nur Mitglieder der Sammlung heruntergeladen, deren Felder den von uns festgelegten Einschränkungen entsprechen. In diesem Fall werden nur Restaurants heruntergeladen, bei denen category
gleich "Dim Sum"
ist.
In dieser App kann der Nutzer mehrere Filter verketten, um bestimmte Anfragen zu erstellen, z. B. „Pizza in San Francisco“ oder „Meeresfrüchte in Los Angeles, sortiert nach Beliebtheit“.
Öffnen Sie RestaurantsTableViewController.swift
und fügen Sie den folgenden Codeblock in die Mitte von query(withCategory:city:price:sortBy:)
ein:
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)
}
Im Snippet oben werden mehrere whereField
- und order
-Klauseln hinzugefügt, um eine einzelne zusammengesetzte Abfrage auf Grundlage der Nutzereingabe zu erstellen. Jetzt werden in unserer Anfrage nur noch Restaurants zurückgegeben, die den Anforderungen des Nutzers entsprechen.
Führen Sie Ihr Projekt aus und prüfen Sie, ob Sie nach Preis, Stadt und Kategorie filtern können. Achten Sie darauf, dass Sie die Kategorie- und Stadtnamen genau eingeben. Beim Testen werden möglicherweise Fehler in Ihren Logs angezeigt, die so aussehen:
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/project-id/database/firestore/indexes?create_composite=..." UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...}
Das liegt daran, dass Firestore für die meisten zusammengesetzten Abfragen Indexe benötigt. Durch die Anforderung von Indexen für Abfragen bleibt Firestore auch bei großem Umfang schnell. Wenn Sie den Link aus der Fehlermeldung öffnen, wird die Benutzeroberfläche zum Erstellen von Indexen in der Firebase Console automatisch mit den richtigen Parametern geöffnet. Weitere Informationen zu Firestore-Indexen
7. Daten in eine Transaktion schreiben
In diesem Abschnitt fügen wir die Möglichkeit hinzu, dass Nutzer Rezensionen für Restaurants einreichen können. Bisher waren alle unsere Schreibvorgänge atomar und relativ einfach. Wenn bei einem der Schritte ein Fehler auftritt, bitten wir den Nutzer wahrscheinlich, es noch einmal zu versuchen, oder wir versuchen es automatisch noch einmal.
Um einem Restaurant eine Bewertung hinzuzufügen, müssen wir mehrere Lese- und Schreibvorgänge koordinieren. Zuerst muss die Rezension eingereicht werden. Danach müssen die Anzahl der Bewertungen und die durchschnittliche Bewertung des Restaurants aktualisiert werden. Wenn einer dieser Vorgänge fehlschlägt, der andere jedoch nicht, entsteht ein inkonsistenter Zustand, in dem die Daten in einem Teil unserer Datenbank nicht mit den Daten in einem anderen Teil übereinstimmen.
Glücklicherweise bietet Firestore Transaktionsfunktionen, mit denen wir mehrere Lese- und Schreibvorgänge in einem einzigen atomaren Vorgang ausführen können, sodass unsere Daten konsistent bleiben.
Fügen Sie den folgenden Code unter allen „let“-Deklarationen in RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:)
ein.
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)
}
}
}
Innerhalb des Update-Blocks werden alle Vorgänge, die wir mit dem Transaktionsobjekt ausführen, von Firestore als einzelne atomare Aktualisierung behandelt. Wenn das Update auf dem Server fehlschlägt, wird es von Firestore automatisch einige Male wiederholt. Das bedeutet, dass es sich bei unserem Fehlerzustand höchstwahrscheinlich um einen einzelnen Fehler handelt, der wiederholt auftritt, z. B. wenn das Gerät vollständig offline ist oder der Nutzer nicht berechtigt ist, in den Pfad zu schreiben, in den er schreiben möchte.
8. Sicherheitsregeln
Nutzer unserer App sollten nicht alle Daten in unserer Datenbank lesen und schreiben können. So sollte beispielsweise jeder die Bewertungen eines Restaurants sehen können, aber nur ein authentifizierter Nutzer sollte eine Bewertung abgeben dürfen. Es reicht nicht aus, guten Code auf dem Client zu schreiben. Wir müssen unser Datensicherheitsmodell im Backend angeben, um vollständig sicher zu sein. In diesem Abschnitt erfahren Sie, wie Sie Ihre Daten mit Firebase-Sicherheitsregeln schützen können.
Sehen wir uns zuerst die Sicherheitsregeln genauer an, die wir zu Beginn des Codelabs geschrieben haben. Öffnen Sie die Firebase Console und rufen Sie auf dem Tab „Firestore“ Datenbank > Regeln auf.
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;
}
}
}
Die Variable request
in den Regeln ist eine globale Variable, die in allen Regeln verfügbar ist. Die von uns hinzugefügte Bedingung sorgt dafür, dass die Anfrage authentifiziert wird, bevor Nutzer etwas tun dürfen. So wird verhindert, dass nicht authentifizierte Nutzer die Firestore API verwenden, um unbefugte Änderungen an Ihren Daten vorzunehmen. Das ist ein guter Anfang, aber mit Firestore-Regeln können wir noch viel mehr erreichen.
Wir möchten das Schreiben von Rezensionen so einschränken, dass die Nutzer-ID der Rezension mit der ID des authentifizierten Nutzers übereinstimmen muss. So wird verhindert, dass Nutzer sich gegenseitig ausgeben und betrügerische Rezensionen hinterlassen.
Die erste „match“-Anweisung entspricht der untergeordneten Sammlung namens ratings
eines beliebigen Dokuments, das zur Sammlung restaurants
gehört. Die allow write
-Bedingung verhindert dann, dass eine Rezension eingereicht wird, wenn die Nutzer-ID der Rezension nicht mit der des Nutzers übereinstimmt. Mit der zweiten „match“-Anweisung können alle authentifizierten Nutzer Restaurants in die Datenbank lesen und schreiben.
Das funktioniert sehr gut für unsere Rezensionen, da wir mit Sicherheitsregeln explizit die implizite Garantie angegeben haben, die wir zuvor in unsere App geschrieben haben: Nutzer können nur ihre eigenen Rezensionen schreiben. Wenn wir eine Bearbeitungs- oder Löschfunktion für Rezensionen hinzufügen würden, würde genau derselbe Satz von Regeln auch verhindern, dass Nutzer die Rezensionen anderer Nutzer ändern oder löschen. Firestore-Regeln können aber auch detaillierter verwendet werden, um Schreibvorgänge für einzelne Felder in Dokumenten anstelle der gesamten Dokumente selbst einzuschränken. So können wir Nutzern ermöglichen, nur die Bewertungen, die durchschnittliche Bewertung und die Anzahl der Bewertungen für ein Restaurant zu aktualisieren. Dadurch wird verhindert, dass ein böswilliger Nutzer den Namen oder den Standort eines Restaurants ändert.
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;
}
}
}
Hier haben wir unsere Schreibberechtigung in „Erstellen“ und „Aktualisieren“ aufgeteilt, damit wir genauer angeben können, welche Vorgänge zulässig sein sollen. Jeder Nutzer kann Restaurants in die Datenbank schreiben. Die Funktionalität der Schaltfläche „Populate“ (Bevölkern), die wir zu Beginn des Codelabs erstellt haben, bleibt erhalten. Wenn ein Restaurant geschrieben wurde, können sein Name, sein Standort, sein Preis und seine Kategorie jedoch nicht mehr geändert werden. Genauer gesagt muss bei der letzten Regel bei jedem Updatevorgang für ein Restaurant der Name, die Stadt, der Preis und die Kategorie der bereits vorhandenen Felder in der Datenbank beibehalten werden.
Weitere Informationen dazu, was Sie mit Sicherheitsregeln tun können, finden Sie in der Dokumentation.
9. Fazit
In diesem Codelab haben Sie gelernt, wie Sie grundlegende und erweiterte Lese- und Schreibvorgänge mit Firestore ausführen und den Datenzugriff mit Sicherheitsregeln schützen. Die vollständige Lösung finden Sie im codelab-complete
-Branch.
Weitere Informationen zu Firestore finden Sie in den folgenden Ressourcen: