Cloud Firestore iOS Codelab

1. Übersicht

Ziele

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

  1. Lesen und Schreiben von Daten in Firestore über eine iOS-App
  2. Hören Sie sich Änderungen in Firestore-Daten in Echtzeit an
  3. Verwenden Sie Firebase Authentication und Sicherheitsregeln, um Firestore-Daten zu schützen
  4. Schreiben Sie komplexe Firestore-Abfragen

Voraussetzungen

Bevor Sie dieses Codelab starten, stellen Sie sicher, dass Sie Folgendes installiert haben:

  • Xcode-Version 8.3 (oder höher)
  • CocoaPods 1.2.1 (oder höher)

2. Firebase-Konsolenprojekt erstellen

Firebase zum Projekt hinzufügen

  1. Gehen Sie auf die Firebase Konsole .
  2. Erstellen Wählen Sie Neues Projekt und benennen Sie Ihr Projekt „Firestor iOS Codelab“.

3. Holen Sie sich das Beispielprojekt

Laden Sie den Code herunter

Beginnen Sie mit dem Klonen von Beispielprojekt und läuft pod update im Projektverzeichnis:

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

Öffnen FriendlyEats.xcworkspace in Xcode und führen Sie es (Cmd + R). Die App sollte richtig kompilieren und sofort beim Start abstürzen, da es eine fehlende GoogleService-Info.plist - Datei. Das werden wir im nächsten Schritt korrigieren.

Firebase einrichten

Folgen Sie in der Dokumentation ein neues Firestore - Projekt zu erstellen. Nachdem Sie Ihr Projekt haben, laden Sie Ihr Projekt GoogleService-Info.plist Datei von Firebase Konsole und ziehen Sie es an die Wurzel des Xcode - Projekt. Führen Sie das Projekt erneut aus, um sicherzustellen, dass die App richtig konfiguriert wird 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.

10a0671ce8f99704.png

4. Daten in Firestore schreiben

In diesem Abschnitt schreiben wir einige Daten in Firestore, damit wir die App-Benutzeroberfläche füllen können. Dies kann manuell über die getan wird Firebase - Konsole , aber wir werden es in der App selbst tun eine grundlegende Firestor schreiben zu demonstrieren.

Das Hauptmodellobjekt in unserer App ist ein Restaurant. Firestore-Daten werden in Dokumente, Sammlungen und Untersammlungen unterteilt. Wir speichern jedes Restaurant als Dokument in einer Top-Level - Kollektion namens restaurants . Wenn Sie möchten mehr über das Firestore - Datenmodell erfahren, lesen Sie über Dokumente und Sammlungen in der Dokumentation .

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

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 von einer Restaurant-Struktur erhalten.

Wir sind fast am Ziel – 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. Im Moment erlauben wir nur authentifizierten Benutzern, die gesamte Datenbank zu lesen und zu schreiben. Dies ist ein wenig zu freizügig für eine Produktions-App, aber während des App-Erstellungsprozesses möchten wir etwas entspannt genug, 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.

In der Registerkarte Regeln der Firebase - Konsole die folgenden Regeln hinzufügen , 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 Sicherheitsregeln im Detail besprechen später, aber wenn Sie in Eile sind, einen Blick auf die nehmen Sicherheitsregeln Dokumentation .

Führen Sie die App und Zeichen in. Tippen Sie dann auf die „Populate“ -Button in der oberen linken Ecke, die eine Charge von Restaurant Dokumenten erstellen, obwohl Sie diese in der App noch nicht sehen.

Als nächstes Navigieren Sie zu dem Firestor Registerkarte Daten in der Konsole Firebase. Sie sollten jetzt neue Einträge in der Restaurantsammlung sehen:

Screenshot 2017-07-06 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. 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 Updates in Echtzeit.

Lassen Sie uns zunächst die Abfrage erstellen, die die standardmäßige, ungefilterte Liste von Restaurants bereitstellt. Werfen Sie einen Blick auf die Umsetzung von RestaurantsTableViewController.baseQuery() :

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

Diese Abfrage ruft bis zu 50 Restaurants der obersten Sammlung namens "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 den folgenden Code in der RestaurantsTableViewController.observeQuery() Methode nur nach dem Aufruf von 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()
}

Der obige Code lädt die Sammlung von Firestore herunter und speichert sie lokal in einem Array. Die addSnapshotListener(_:) Aufruf fügt einen Schnappschuss Hörer auf die Abfrage, die die View - Controller jedes Mal , wenn die Datenänderungen auf dem Server aktualisiert. 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 dem Abbilden unserer Wörterbücher structs (siehe Restaurant.swift nur eine Frage der Zuordnung von ein paar Ansichtseigenschaften), ist die Anzeige der Daten. Fügen Sie die folgenden Zeilen zu RestaurantTableViewCell.populate(restaurant:) in 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)

Dieses Populate Verfahren ist aus der Tabellenansicht Datenquelle der genannten tableView(_:cellForRowAtIndexPath:) Methode, die aus der Zuordnung die Sammlung von Werttypen übernimmt , bevor auf die einzelnen Zellen der Tabellendarstellung.

Führen Sie die App erneut aus und überprüfen Sie, ob 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!

2ca7f8c6052f7f79.png

6. Daten sortieren und filtern

Derzeit zeigt unsere App eine Liste von Restaurants an, aber es gibt keine Möglichkeit für den Benutzer, nach seinen Bedürfnissen zu filtern. In diesem Abschnitt verwenden Sie die erweiterten Abfragen 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, die whereField(_:isEqualTo:) Methode wird unsere Abfrage Download nur Mitglieder der Sammlung , dessenderen Felder erfüllen die Einschränkungen macht wir eingestellt. In diesem Fall wird es nur Restaurants herunterladen , wo category ist "Dim Sum" .

In dieser App kann der Benutzer mehrere Filter verketten, um spezifische Abfragen zu erstellen, wie "Pizza in San Francisco" oder "Seafood in Los Angeles, sortiert nach Beliebtheit".

Öffnen RestaurantsTableViewController.swift und fügen Sie den folgenden Code - Block 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)
}

Die Schnipsel fügt über mehrere whereField und order Klauseln eine einzelne Verbindung Abfrageeingabe basierend auf Benutzer aufzubauen. Jetzt gibt unsere Abfrage nur Restaurants zurück, die den Anforderungen des Benutzers entsprechen.

Führen Sie Ihr Projekt aus und überprüfen Sie, ob Sie nach Preis, Stadt und Kategorie filtern können (achten Sie darauf, die Kategorie- und Städtenamen genau einzugeben). Während des Testens werden möglicherweise Fehler in Ihren Protokollen 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/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=...}

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 skaliert. Die Verbindung von der Fehlermeldung öffnet , wird die Indexerstellung UI in der Konsole Firebase mit den richtigen Parametern ausgefüllt automatisch geöffnet. Mehr über Indizes in Firestor, Um zu erfahren , besuchen Sie die Dokumentation .

7. Schreiben von Daten in eine Transaktion

In diesem Abschnitt fügen wir Benutzern die Möglichkeit hinzu, Bewertungen an Restaurants zu senden. Bisher waren alle unsere Schriften atomar und relativ einfach. Wenn einer von ihnen einen Fehler aufweist, werden wir den Benutzer wahrscheinlich einfach auffordern, es erneut zu versuchen oder es automatisch erneut zu versuchen.

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 Bewertungszahl und die durchschnittliche Bewertung des Restaurants aktualisiert werden. Wenn einer dieser Schritte fehlschlägt, der 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 einem einzigen atomaren Vorgang ausführen können, um sicherzustellen, dass unsere Daten konsistent bleiben.

Fügen Sie den folgenden Code unter allen den let Erklärungen in 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)
    }
  }
}

Innerhalb des Update-Blocks werden alle Operationen, 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 unsere Fehlerbedingung höchstwahrscheinlich ein einzelner Fehler ist, der wiederholt auftritt, beispielsweise wenn das Gerät vollständig offline ist oder der Benutzer nicht autorisiert ist, in den Pfad zu schreiben, in den er zu schreiben versucht.

8. Sicherheitsregeln

Benutzer unserer App sollten nicht alle Daten in unserer Datenbank lesen und schreiben können. Zum Beispiel 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 angeben, um absolut sicher zu sein. In diesem Abschnitt erfahren Sie, wie Sie die Firebase-Sicherheitsregeln zum Schutz unserer Daten verwenden.

Lassen Sie uns zunächst einen tieferen Blick auf die Sicherheitsregeln werfen, die wir zu Beginn des Codelabs geschrieben haben. Öffnen Sie die Konsole Firebase und navigieren Sie zu Datenbank> Regeln in den Firestor Registerkarte .

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 Variable in den oben genannten Regeln ist eine globale Variable in allen Regeln zur Verfügung, und die bedingt wir sicher fügten hinzu , dass die Anforderung , bevor die Benutzer authentifiziert ist , etwas zu tun. Dadurch wird verhindert, dass nicht authentifizierte Benutzer die Firestore API verwenden, um nicht autorisierte Änderungen an Ihren Daten vorzunehmen. Dies ist ein guter Anfang, aber wir können Firestore-Regeln verwenden, um viel leistungsfähigere Dinge zu tun.

Lassen Sie uns die Schreibvorgänge für Bewertungen einschränken, sodass die Benutzer-ID der Bewertung mit der ID des authentifizierten Benutzers übereinstimmen muss. Dadurch wird sichergestellt, dass sich Benutzer nicht gegenseitig ausgeben und betrügerische Bewertungen hinterlassen. Ersetzen Sie Ihre Sicherheitsregeln durch die folgenden:

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 Spiel Anweisung paßt die Untersammlung namens ratings von Dokumenten zu der dazugehörigen restaurants Sammlung. Der allow write bedingte verhindert dann jegliche Überprüfung von vorgelegt werden , wenn die Benutzer - ID die Bewertung entspricht nicht der des Benutzers. Die zweite match-Anweisung ermöglicht es jedem authentifizierten Benutzer, Restaurants zu lesen und in die Datenbank zu schreiben.

Dies funktioniert für unsere Bewertungen sehr gut, da wir Sicherheitsregeln verwendet haben, um die implizite Garantie, die wir zuvor in unsere App geschrieben haben, ausdrücklich 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 die gleichen Regeln auch verhindern, dass Benutzer die Bewertungen anderer Benutzer ändern oder löschen. Firestore-Regeln können jedoch auch granularer verwendet werden, um Schreibvorgänge auf einzelne Felder innerhalb von Dokumenten statt auf die gesamten Dokumente selbst einzuschrä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 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 create und update aufgeteilt, damit wir genauer bestimmen können, welche Operationen erlaubt sein sollten. Jeder Benutzer kann Restaurants in die Datenbank schreiben und dabei die Funktionalität der Schaltfläche Befüllen beibehalten, die wir zu Beginn des Codelabs erstellt haben, aber sobald ein Restaurant geschrieben ist, können dessen Name, Lage, Preis und Kategorie nicht mehr geändert werden. Genauer gesagt erfordert die letzte Regel, dass jede Restaurantaktualisierungsoperation denselben Namen, dieselbe Stadt, denselben Preis und dieselbe Kategorie wie die bereits vorhandenen Felder in der Datenbank beibehält.

Um mehr darüber zu erfahren , was Sie mit Sicherheitsregeln tun, 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 sowie den Datenzugriff mit Sicherheitsregeln sichern. Sie können die vollständige Lösung auf dem finden codelab-complete Zweig .

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