Cloud Firestore iOS-Codelab

1. Übersicht

Ziele

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

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

Voraussetzungen

Bevor Sie dieses Codelab starten, vergewissern Sie sich, dass Sie Folgendes installiert haben:

  • Xcode-Version 13.0 (oder höher)
  • CocoaPods 1.11.0 (oder höher)

2. Erstellen Sie ein Firebase-Konsolenprojekt

Fügen Sie dem Projekt Firebase 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

Beginnen Sie mit dem Klonen des Beispielprojekts und dem Ausführen pod update im Projektverzeichnis:

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 ihr eine GoogleService-Info.plist Datei fehlt. Das korrigieren wir im nächsten Schritt.

Firebase einrichten

Folgen Sie der Dokumentation , um ein neues Firestore-Projekt zu erstellen. Nachdem Sie Ihr Projekt erstellt 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 ist und beim Start nicht mehr abstürzt. Nach der Anmeldung sollten Sie einen leeren Bildschirm wie im folgenden Beispiel sehen. Wenn Sie sich nicht anmelden können, vergewissern Sie sich, dass Sie die E-Mail/Passwort-Anmeldemethode 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 führen dies jedoch in der App selbst durch, um einen einfachen Firestore-Schreibvorgang 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 übergeordneten Sammlung namens restaurants . Wenn Sie mehr über das Firestore-Datenmodell erfahren möchten, lesen Sie mehr über Dokumente und Sammlungen in der Dokumentation .

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

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

Jetzt, da wir eine Sammlungsreferenz haben, können wir einige Daten schreiben. Fügen Sie Folgendes direkt nach der letzten Codezeile hinzu, 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)

Der obige Code fügt der Restaurantsammlung ein neues Dokument hinzu. Die Dokumentdaten stammen aus einem Wörterbuch, das wir von 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 Lese- und Schreibzugriff auf die gesamte Datenbank. Dies ist für eine Produktions-App etwas zu freizügig, aber während des App-Erstellungsprozesses möchten wir etwas entspanntes, 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 einschränken 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 im Detail besprechen, aber wenn Sie es eilig haben, werfen Sie einen Blick in die Dokumentation zu den Sicherheitsregeln .

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 von Restaurantdokumenten erstellt wird, obwohl Sie dies noch nicht in der App sehen werden.

Navigieren Sie als Nächstes zur Registerkarte Firestore-Daten in der Firebase-Konsole. Sie sollten jetzt 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 von einer iOS-App in Firestore geschrieben! Im nächsten Abschnitt erfahren Sie, wie Sie Daten aus Firestore abrufen und in der App anzeigen.

5. Zeigen Sie Daten aus Firestore an

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.

Lassen Sie uns zunächst die Abfrage erstellen, die die standardmäßige, ungefilterte Liste von Restaurants liefert. Sehen Sie sich die Implementierung von RestaurantsTableViewController.baseQuery() an:

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

Diese Abfrage ruft bis zu 50 Restaurants der Sammlung der obersten Ebene mit dem Namen "Restaurants" ab. Da 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 der Methode RestaurantsTableViewController.observeQuery() unmittelbar nach dem Aufruf von stopObserving() den folgenden Code 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.

Nachdem wir unsere Wörterbücher auf Strukturen gemappt haben (siehe Restaurant.swift ), ist die Anzeige der Daten nur noch eine Frage der Zuweisung einiger Ansichtseigenschaften. 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 Populate-Methode wird von der tableView(_:cellForRowAtIndexPath:) Methode der Datenquelle der Tabellenansicht aufgerufen, die sich um die Zuordnung der Sammlung von Werttypen von vorher zu den einzelnen Tabellenansichtszellen kümmert.

Führen Sie die App erneut aus und vergewissern Sie sich, 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. Sortieren und Filtern von Daten

Derzeit zeigt unsere App eine Liste von Restaurants an, aber der Benutzer hat keine Möglichkeit, nach seinen Bedürfnissen zu filtern. In diesem Abschnitt verwenden Sie die erweiterte Abfrage 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 whereField(_:isEqualTo:) Methode 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 mit der category "Dim Sum" heruntergeladen.

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

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 vergewissern Sie sich, dass Sie nach Preis, Stadt und Kategorie filtern können (achten Sie darauf, die Kategorie- und Städtenamen genau einzugeben). Während des Tests sehen Sie möglicherweise Fehler in Ihren Protokollen, 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 Indizes für die meisten zusammengesetzten Abfragen benötigt. Das Erfordernis von Indizes für Abfragen hält Firestore schnell und skalierbar. Wenn Sie den Link aus der Fehlermeldung öffnen, wird automatisch die Benutzeroberfläche für die Indexerstellung in der Firebase-Konsole mit den korrekt ausgefüllten Parametern geöffnet. Um mehr über Indexe in Firestore zu erfahren, besuchen Sie die Dokumentation .

7. Schreiben von Daten in eine Transaktion

In diesem Abschnitt fügen wir die Möglichkeit für Benutzer hinzu, Bewertungen an Restaurants zu senden. Bisher waren alle unsere Schreibvorgänge atomar und relativ einfach. Wenn bei einem von ihnen ein Fehler auftritt, werden wir den Benutzer wahrscheinlich einfach 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 eingereicht werden, und dann müssen die Bewertungsanzahl und die durchschnittliche Bewertung 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 übereinstimmen.

Glücklicherweise bietet Firestore Transaktionsfunktionen, mit denen wir mehrere Lese- und Schreibvorgänge in einer einzigen atomaren Operation durchführen können, um 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 Aktualisierungsblocks 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, wiederholt Firestore es automatisch einige Male. Dies bedeutet, dass unsere Fehlerbedingung höchstwahrscheinlich ein einzelner Fehler ist, der wiederholt auftritt, beispielsweise wenn das Gerät vollständig offline ist oder der Benutzer nicht berechtigt ist, in den Pfad zu schreiben, in 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 eine Bewertung abgeben dürfen. Es reicht nicht aus, guten Code auf dem Client zu schreiben, wir müssen unser Datensicherheitsmodell im Backend spezifizieren, um vollständig sicher zu sein. In diesem Abschnitt erfahren Sie, wie Sie Firebase-Sicherheitsregeln verwenden, um unsere Daten zu schützen.

Lassen Sie uns zunächst einen genaueren Blick auf die Sicherheitsregeln werfen, 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, irgendetwas 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 mächtigere Dinge zu tun.

Beschränken wir das Schreiben von Bewertungen so, dass die Benutzer-ID der Bewertung mit der ID des authentifizierten Benutzers übereinstimmen muss. Dadurch wird sichergestellt, dass Benutzer sich nicht gegenseitig ausgeben und betrügerische Bewertungen abgeben 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 Übereinstimmungsaussage stimmt mit der Untersammlung mit dem Namen ratings eines beliebigen Dokuments überein, das zur restaurants gehört. Die allow write “ verhindert dann, dass eine Bewertung übermittelt 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 wirklich gut für unsere Rezensionen, da wir Sicherheitsregeln verwendet haben, um die implizite Garantie, die wir zuvor in unsere App geschrieben haben, explizit anzugeben – dass Benutzer nur ihre eigenen Rezensionen schreiben können. Wenn wir eine Bearbeitungs- oder Löschfunktion für Bewertungen hinzufügen würden, würden genau dieselben Regeln Benutzer auch daran hindern, die Bewertungen anderer Benutzer zu ändern oder zu löschen. Aber Firestore-Regeln können auch in einer granulareren Weise verwendet werden, um Schreibzugriffe auf einzelne Felder innerhalb von Dokumenten statt auf die gesamten Dokumente selbst zu beschränken. Wir können dies verwenden, um Benutzern zu 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 angeben können, welche Vorgänge zulässig sein sollten. Jeder Benutzer kann Restaurants in die Datenbank schreiben, wobei die Funktionalität der Populate-Schaltfläche, die wir zu Beginn des Codelabs erstellt haben, erhalten bleibt, aber sobald ein Restaurant geschrieben ist, können Name, Ort, Preis und Kategorie nicht mehr geändert werden. Genauer gesagt erfordert die letzte Regel, dass jede Restaurantaktualisierungsoperation den gleichen Namen, die gleiche Stadt, den gleichen Preis und die gleiche Kategorie wie die bereits existierenden Felder in der Datenbank beibehält.

Um mehr darüber zu erfahren, was Sie mit Sicherheitsregeln tun können, werfen Sie einen Blick in 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: