Cloud Firestore iOS Codelab

1. Übersicht

Ziele

In diesem Codelab erstellen Sie in Swift eine von Firestore unterstützte Restaurantempfehlungs-App für iOS. Du wirst lernen wie:

  1. Lesen und schreiben Sie Daten aus einer iOS-App in Firestore
  2. Hören Sie Änderungen in Firestore-Daten in Echtzeit
  3. Verwenden Sie die Firebase-Authentifizierung und Sicherheitsregeln, um Firestore-Daten zu schützen
  4. Schreiben Sie komplexe Firestore-Abfragen

Voraussetzungen

Bevor Sie mit diesem Codelab beginnen, stellen Sie sicher, dass Sie Folgendes installiert haben:

  • Xcode-Version 14.0 (oder höher)
  • CocoaPods 1.12.0 (oder höher)

2. Erstellen Sie ein Firebase-Konsolenprojekt

Fügen Sie Firebase zum Projekt hinzu

  1. Gehen Sie zur Firebase-Konsole .
  2. Wählen Sie „Neues Projekt erstellen“ und nennen Sie Ihr Projekt „Firestore iOS Codelab“.

3. Holen Sie sich das Beispielprojekt

Laden Sie den Code herunter

Klonen Sie zunächst 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 (Befehl+R). Die App sollte korrekt kompiliert werden und beim Start sofort abstürzen, da eine GoogleService-Info.plist Datei fehlt. Wir werden das im nächsten Schritt korrigieren.

Richten Sie Firebase ein

Befolgen Sie die Dokumentation , um ein neues Firestore-Projekt zu erstellen. Sobald Sie Ihr Projekt erhalten haben, laden Sie die Datei GoogleService-Info.plist Ihres Projekts von der Firebase-Konsole herunter und ziehen Sie sie in das Stammverzeichnis des Xcode-Projekts. Führen Sie das Projekt erneut aus, um sicherzustellen, dass die App korrekt konfiguriert wird und beim Start nicht mehr abstürzt. Nach der Anmeldung sollte ein leerer Bildschirm wie im folgenden Beispiel angezeigt werden. Wenn Sie sich nicht anmelden können, stellen Sie sicher, dass Sie die Anmeldemethode „E-Mail/Passwort“ in der Firebase-Konsole unter „Authentifizierung“ aktiviert haben.

d5225270159c040b.png

4. Daten in Firestore schreiben

In diesem Abschnitt schreiben wir einige Daten in Firestore, damit wir die Benutzeroberfläche der App füllen können. Dies kann manuell über die Firebase-Konsole erfolgen, wir machen es jedoch in der App selbst, um einen einfachen Firestore-Schreibvorgang zu demonstrieren.

Das Hauptmodellobjekt in unserer App ist ein Restaurant. Firestore-Daten sind in Dokumente, Sammlungen und Untersammlungen unterteilt. Wir speichern jedes Restaurant als Dokument in einer Sammlung auf oberster Ebene namens restaurants . Wenn Sie mehr über das Firestore-Datenmodell erfahren möchten, lesen Sie die Informationen zu Dokumenten und Sammlungen in der Dokumentation .

Bevor wir Daten zu Firestore hinzufügen können, müssen wir einen Verweis auf die Restaurantsammlung erhalten. Fügen Sie der inneren for-Schleife in der Methode RestaurantsTableViewController.didTapPopulateButton(_:) Folgendes hinzu.

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

Da wir nun eine Sammlungsreferenz haben, können wir einige Daten schreiben. Fügen Sie direkt nach der letzten Codezeile, die wir hinzugefügt haben, Folgendes hinzu:

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)

Der obige Code fügt der Restaurantsammlung ein neues Dokument hinzu. Die Dokumentdaten stammen aus einem Wörterbuch, das wir aus einer Restaurantstruktur erhalten.

Wir haben es fast geschafft – bevor wir Dokumente in Firestore schreiben können, müssen wir die Sicherheitsregeln von Firestore öffnen und beschreiben, welche Teile unserer Datenbank von welchen Benutzern beschreibbar sein sollen. Vorerst erlauben wir nur authentifizierten Benutzern, in der gesamten Datenbank zu lesen und zu schreiben. Das ist für eine Produktions-App etwas zu freizügig, aber während des App-Erstellungsprozesses wollen wir etwas Lockeres, damit wir beim Experimentieren nicht ständig auf Authentifizierungsprobleme stoßen. Am Ende dieses Codelabs sprechen wir darüber, wie Sie Ihre Sicherheitsregeln verschärfen und die Möglichkeit unbeabsichtigter Lese- und Schreibvorgänge begrenzen können.

Fügen Sie auf der Registerkarte „Regeln“ der Firebase-Konsole die folgenden Regeln hinzu und klicken Sie dann auf „Veröffentlichen“ .

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;
    }
  }
}

Wir werden die Sicherheitsregeln später ausführlich besprechen. Wenn Sie es jedoch eilig haben, schauen Sie sich die Dokumentation zu den Sicherheitsregeln an.

Führen Sie die App aus und melden Sie sich an. Tippen Sie dann oben links auf die Schaltfläche „ Ausfüllen “, wodurch ein Stapel Restaurantdokumente erstellt wird, obwohl Sie dies in der App noch nicht sehen.

Navigieren Sie als Nächstes zur Registerkarte „Firestore-Daten“ in der Firebase-Konsole. Sie sollten nun neue Einträge in der Restaurantsammlung sehen:

Screenshot vom 06.07.2017 um 12:45:38 Uhr.png

Herzlichen 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 benachrichtigt, die der Abfrage entsprechen, und erhält Aktualisierungen in Echtzeit.

Erstellen wir zunächst die Abfrage, die die standardmäßige, ungefilterte Liste von Restaurants bedient. Schauen Sie sich die Implementierung von RestaurantsTableViewController.baseQuery() an:

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

Diese Abfrage ruft bis zu 50 Restaurants der obersten Sammlung mit dem Namen „Restaurants“ ab. Nachdem wir nun 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 zur Methode RestaurantsTableViewController.observeQuery() direkt nach dem Aufruf von stopObserving() hinzu.

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()
}

Der obige Code lädt die Sammlung von Firestore herunter und speichert sie lokal in einem Array. Der Aufruf addSnapshotListener(_:) fügt der Abfrage einen Snapshot-Listener hinzu, der den View Controller jedes Mal aktualisiert, wenn sich die Daten auf dem Server ändern. Wir erhalten Updates automatisch und müssen Änderungen nicht manuell pushen. Denken Sie daran, dass dieser Snapshot-Listener jederzeit als Ergebnis einer serverseitigen Änderung aufgerufen werden kann. Daher ist es wichtig, dass unsere App Änderungen verarbeiten kann.

Nach der Zuordnung unserer Wörterbücher zu Strukturen (siehe Restaurant.swift ) ist die Anzeige der Daten nur noch eine Frage der Zuweisung einiger Ansichtseigenschaften. Fügen Sie die folgenden Zeilen zu RestaurantTableViewCell.populate(restaurant:) in RestaurantsTableViewController.swift 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 Auffüllmethode wird von der Methode tableView(_:cellForRowAtIndexPath:) der Datenquelle der Tabellenansicht aufgerufen, die sich um die Zuordnung der Sammlung von Werttypen von zuvor zu den einzelnen Zellen der Tabellenansicht kümmert.

Führen Sie die App erneut aus und stellen Sie sicher, dass die Restaurants, die wir zuvor in der Konsole gesehen haben, jetzt auf dem Simulator oder Gerät sichtbar sind. Wenn Sie diesen Abschnitt erfolgreich abgeschlossen haben, liest und schreibt Ihre App jetzt Daten mit Cloud Firestore!

391c0259bf05ac25.png

6. Daten sortieren und filtern

Derzeit zeigt unsere App eine Liste von Restaurants an, aber es gibt für den Benutzer keine Möglichkeit, nach seinen Bedürfnissen zu filtern. In diesem Abschnitt verwenden Sie die erweiterte Abfragefunktion von Firestore, um die Filterung zu aktivieren.

Hier ist ein Beispiel für eine einfache Abfrage zum Abrufen aller Dim Sum-Restaurants:

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

Wie der Name schon sagt, sorgt die Methode whereField(_:isEqualTo:) dafür, dass unsere Abfrage nur Mitglieder der Sammlung herunterlädt, deren Felder die von uns festgelegten Einschränkungen erfüllen. In diesem Fall werden nur Restaurants heruntergeladen, deren category "Dim Sum" lautet.

In dieser App kann der Benutzer mehrere Filter verketten, um spezifische Abfragen 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)
}

Das obige Snippet fügt mehrere whereField und order -Klauseln hinzu, um eine einzelne zusammengesetzte Abfrage basierend auf Benutzereingaben zu erstellen. Jetzt gibt unsere Abfrage nur Restaurants zurück, die den Anforderungen des Benutzers entsprechen.

Führen Sie Ihr Projekt aus und stellen Sie sicher, dass Sie nach Preis, Stadt und Kategorie filtern können (stellen Sie sicher, dass Sie die Kategorie- und Städtenamen genau eingeben). Während des Tests werden in Ihren Protokollen möglicherweise Fehler angezeigt, die wie folgt 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=...}

Dies liegt daran, dass Firestore für die meisten zusammengesetzten Abfragen Indizes benötigt. Durch die Anforderung von Indizes für Abfragen bleibt Firestore schnell und skalierbar. Wenn Sie den Link in der Fehlermeldung öffnen, wird automatisch die Benutzeroberfläche zur Indexerstellung in der Firebase-Konsole mit den richtigen ausgefüllten Parametern geöffnet. Weitere Informationen zu Indizes in Firestore finden Sie in der Dokumentation .

7. Daten in eine Transaktion schreiben

In diesem Abschnitt fügen wir Benutzern die Möglichkeit hinzu, Bewertungen an Restaurants zu übermitteln. Bisher waren alle unsere Schreibvorgänge atomar und relativ einfach. Wenn bei einem davon ein Fehler aufgetreten wäre, würden wir den Benutzer wahrscheinlich nur auffordern, es erneut zu versuchen, oder es automatisch wiederholen.

Um einem Restaurant eine Bewertung hinzuzufügen, müssen wir mehrere Lese- und Schreibvorgänge koordinieren. Zuerst muss die Bewertung selbst abgegeben werden und dann müssen die Bewertungsanzahl und die Durchschnittsbewertung des Restaurants aktualisiert werden. Wenn eines davon fehlschlägt, das andere jedoch nicht, befinden wir uns in einem inkonsistenten Zustand, in dem die Daten in einem Teil unserer Datenbank nicht mit den Daten in einem anderen Teil übereinstimmen.

Glücklicherweise bietet Firestore Transaktionsfunktionen, die es uns ermöglichen, mehrere Lese- und Schreibvorgänge in einem einzigen atomaren Vorgang durchzuführen und so sicherzustellen, dass unsere Daten konsistent bleiben.

Fügen Sie den folgenden Code unter allen let-Deklarationen in RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:) hinzu.

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 durchführen, von Firestore als einzelnes atomares Update behandelt. Wenn das Update auf dem Server fehlschlägt, wird Firestore es automatisch einige Male wiederholen. Dies bedeutet, dass es sich bei unserem Fehlerzustand höchstwahrscheinlich um einen einzelnen Fehler handelt, der wiederholt auftritt, beispielsweise wenn das Gerät vollständig offline ist oder der Benutzer nicht berechtigt ist, auf den Pfad zu schreiben, auf den er schreiben möchte.

8. Sicherheitsregeln

Benutzer unserer App sollten nicht alle Daten in unserer Datenbank lesen und schreiben können. Beispielsweise sollte jeder die Bewertungen eines Restaurants sehen können, aber nur ein authentifizierter Benutzer sollte berechtigt sein, eine Bewertung abzugeben. Es reicht nicht aus, guten Code auf dem Client zu schreiben, wir müssen unser Datensicherheitsmodell im Backend spezifizieren, um vollkommen sicher zu sein. In diesem Abschnitt erfahren Sie, wie Sie Firebase-Sicherheitsregeln zum Schutz unserer Daten verwenden.

Schauen wir uns zunächst die Sicherheitsregeln genauer an, die wir zu Beginn des Codelabs geschrieben haben. Öffnen Sie die Firebase-Konsole und navigieren Sie auf der Registerkarte Firestore zu Datenbank > Regeln .

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;
    }
  }
}

Die request in den obigen Regeln ist eine globale Variable, die in allen Regeln verfügbar ist, und die von uns hinzugefügte Bedingung stellt sicher, dass die Anforderung authentifiziert wird, bevor Benutzern erlaubt wird, etwas zu tun. Dadurch wird verhindert, dass nicht authentifizierte Benutzer die Firestore-API verwenden, um unbefugte Änderungen an Ihren Daten vorzunehmen. Das ist ein guter Anfang, aber wir können Firestore-Regeln verwenden, um viel leistungsfähigere Dinge zu tun.

Beschränken wir das Schreiben von Rezensionen so, dass die Benutzer-ID der Rezension mit der ID des authentifizierten Benutzers übereinstimmen muss. Dadurch wird sichergestellt, dass Benutzer sich nicht untereinander ausgeben und betrügerische Bewertungen hinterlassen können. Ersetzen Sie Ihre Sicherheitsregeln durch Folgendes:

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 erste Übereinstimmungsanweisung gleicht die Untersammlung mit dem Namen ratings aller Dokumente ab, die zur Sammlung restaurants gehören. Die Bedingung allow write “ verhindert dann, dass eine Bewertung eingereicht wird, wenn die Benutzer-ID der Bewertung nicht mit der des Benutzers übereinstimmt. Die zweite Match-Anweisung ermöglicht es jedem authentifizierten Benutzer, Restaurants in der Datenbank zu lesen und zu schreiben.

Das funktioniert bei unseren Bewertungen sehr gut, da wir Sicherheitsregeln verwendet haben, um die implizite Garantie, die wir zuvor in unsere App geschrieben haben, explizit anzugeben – dass Benutzer nur ihre eigenen Bewertungen schreiben können. Wenn wir eine Bearbeitungs- oder Löschfunktion für Bewertungen hinzufügen würden, würden genau dieselben Regeln auch verhindern, dass Benutzer die Bewertungen anderer Benutzer ebenfalls ändern oder löschen. Firestore-Regeln können jedoch auch detaillierter verwendet werden, um Schreibvorgänge auf einzelne Felder innerhalb von Dokumenten statt auf die gesamten Dokumente selbst zu beschränken. Damit können wir Benutzern ermöglichen, nur die Bewertungen, die durchschnittliche Bewertung und die Anzahl der Bewertungen für ein Restaurant zu aktualisieren, wodurch die Möglichkeit ausgeschlossen wird, dass ein böswilliger Benutzer den Namen oder 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 festlegen können, welche Vorgänge zulässig sein sollen. Jeder Benutzer kann Restaurants in die Datenbank schreiben und dabei die Funktionalität der Schaltfläche „Auffüllen“ beibehalten, die wir zu Beginn des Codelabs erstellt haben. Sobald ein Restaurant jedoch geschrieben ist, können Name, Standort, Preis und Kategorie nicht mehr geändert werden. Genauer gesagt erfordert die letzte Regel, dass bei jedem Restaurantaktualisierungsvorgang der gleiche Name, die gleiche Stadt, derselbe Preis und die gleiche Kategorie der bereits vorhandenen Felder in der Datenbank beibehalten werden.

Um mehr darüber zu erfahren, was Sie mit Sicherheitsregeln tun können, werfen Sie einen Blick auf die Dokumentation .

9. Fazit

In diesem Codelab haben Sie gelernt, wie Sie mit Firestore grundlegende und erweiterte Lese- und Schreibvorgänge durchführen und wie Sie den Datenzugriff mit Sicherheitsregeln sichern. Die vollständige Lösung finden Sie im codelab-complete Zweig .

Um mehr über Firestore zu erfahren, besuchen Sie die folgenden Ressourcen: