Google is committed to advancing racial equity for Black communities. See how.
Diese Seite wurde von der Cloud Translation API übersetzt.
Switch to English

Cloud Firestore iOS Codelab

Tore

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 von Daten aus einer iOS-App in den Firestore
  2. Hören Sie Änderungen in Firestore-Daten in Echtzeit ab
  3. Verwenden Sie Firebase-Authentifizierungs- und Sicherheitsregeln, um Firestore-Daten zu sichern
  4. Schreiben Sie komplexe Firestore-Abfragen

Voraussetzungen

Stellen Sie vor dem Starten dieses Codelabs sicher, dass Sie Folgendes installiert haben:

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

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".

Laden Sie den Code herunter

Beginnen Sie mit dem Klonen des Beispielprojekts und dem Ausführen des 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 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. Wenn Sie Ihr Projekt erhalten haben, laden Sie die GoogleService-Info.plist Datei 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 dem Anmelden sollte ein leerer Bildschirm wie im folgenden Beispiel angezeigt werden. Wenn Sie sich nicht anmelden können, stellen Sie sicher, dass Sie die Anmeldemethode für E-Mail / Kennwort in der Firebase-Konsole unter Authentifizierung aktiviert haben.

10a0671ce8f99704.png

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

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

Bevor wir dem Firestore Daten 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")

Nachdem wir eine Sammlungsreferenz haben, können wir einige Daten schreiben. Fügen Sie unmittelbar nach der letzten hinzugefügten Codezeile 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 sind fast da - bevor wir Dokumente an Firestore schreiben können, müssen wir die Sicherheitsregeln von Firestore öffnen und beschreiben, welche Teile unserer Datenbank von welchen Benutzern beschreibbar sein sollen. Derzeit dürfen nur authentifizierte Benutzer in die gesamte Datenbank lesen und schreiben. Dies ist für eine Produktions-App etwas zu tolerant, 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 werden wir darüber sprechen, 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 ausführlich besprechen. Wenn Sie es jedoch eilig haben, lesen Sie 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 " Auffüllen ", um einen Stapel von Restaurantdokumenten zu erstellen, obwohl dies in der App noch nicht angezeigt wird.

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

Screenshot 2017-07-06 um 12.45.38 PM.png

Herzlichen Glückwunsch, Sie haben gerade Daten von einer iOS-App an Firestore geschrieben! Im nächsten Abschnitt erfahren Sie, wie Sie Daten aus dem Firestore abrufen und in der App anzeigen.

In diesem Abschnitt erfahren Sie, wie Sie Daten aus dem 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 mit der Abfrage übereinstimmen, und erhält Aktualisierungen in Echtzeit.

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

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 eine Abfrage haben, müssen wir einen Snapshot-Listener anhängen, um Daten aus dem Firestore in unsere App zu laden. Fügen Sie der Methode RestaurantsTableViewController.observeQuery() unmittelbar nach dem Aufruf von stopObserving() den folgenden Code 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 aus dem Firestore herunter und speichert sie lokal in einem Array. Der 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 vornehmen. Denken Sie daran, dass dieser Snapshot-Listener aufgrund einer serverseitigen Änderung jederzeit aufgerufen werden kann. Daher ist es wichtig, dass unsere App Änderungen verarbeiten kann.

Nach dem Zuordnen unserer Wörterbücher zu Strukturen (siehe Restaurant.swift ) müssen zum Anzeigen der Daten nur einige Ansichtseigenschaften zugewiesen werden. 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)

Diese tableView(_:cellForRowAtIndexPath:) wird von der tableView(_:cellForRowAtIndexPath:) -Methode der Tabellenansichtsdatenquelle tableView(_:cellForRowAtIndexPath:) , mit der die Sammlung von tableView(_:cellForRowAtIndexPath:) von zuvor den einzelnen Zellen der Tabellenansicht zugeordnet wird.

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!

2ca7f8c6052f7f79.png

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 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, whereField(_:isEqualTo:) unsere Abfrage mit der whereField(_:isEqualTo:) -Methode nur Mitglieder der Sammlung herunter, deren Felder den von uns festgelegten Einschränkungen entsprechen. In diesem Fall werden nur Restaurants heruntergeladen, deren category "Dim Sum" .

In dieser App kann der Benutzer mehrere Filter verketten, um bestimmte 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 der Mitte der query(withCategory:city:price:sortBy:) den folgenden Codeblock hinzu 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 werden bei unserer Abfrage nur Restaurants zurückgegeben, 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 (geben Sie die Kategorien- und Städtenamen genau ein). Während des Testens werden möglicherweise Fehler in Ihren Protokollen angezeigt, die folgendermaßen 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 das Erfordernis von Indizes für Abfragen bleibt der Firestore schnell skaliert. Durch Öffnen des Links über die Fehlermeldung wird automatisch die Benutzeroberfläche zur Indexerstellung in der Firebase-Konsole mit den richtigen Parametern geöffnet. Weitere Informationen zu Indizes in Firestore finden Sie in der Dokumentation .

In diesem Abschnitt können Benutzer Bewertungen an Restaurants senden. Bisher waren alle unsere Schriften atomar und relativ einfach. Wenn einer von ihnen einen Fehler aufweist, werden wir den Benutzer wahrscheinlich nur auffordern, ihn erneut oder automatisch zu wiederholen.

Um einem Restaurant eine Bewertung hinzuzufügen, müssen mehrere Lese- und Schreibvorgänge koordiniert werden. Zuerst muss die Bewertung selbst eingereicht werden, und dann müssen die Anzahl der Bewertungen und die durchschnittliche Bewertung des Restaurants aktualisiert werden. Wenn einer dieser Fehler auftritt, 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 einer einzigen atomaren Operation ausführen können, um sicherzustellen, dass unsere Daten konsistent bleiben.

Fügen Sie den folgenden Code unter allen let-Deklarationen 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 Aktualisierungsblocks werden alle Operationen, die wir mit dem Transaktionsobjekt ausführen, von Firestore als einzelne atomare Aktualisierung behandelt. Wenn das Update auf dem Server fehlschlägt, versucht Firestore es einige Male automatisch. Dies bedeutet, dass unsere Fehlerbedingung höchstwahrscheinlich ein einzelner Fehler ist, der wiederholt auftritt, z. B. 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.

Benutzer unserer App sollten nicht in der Lage sein, alle Daten in unserer Datenbank zu lesen und zu schreiben. Zum Beispiel sollte jeder in der Lage sein, die Bewertungen eines Restaurants zu sehen, aber nur ein authentifizierter Benutzer sollte eine Bewertung abgeben dürfen. Es reicht nicht aus, guten Code auf den Client zu schreiben. Wir müssen unser Datensicherheitsmodell im Backend angeben, um vollständig 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. Die von uns hinzugefügte Bedingung stellt sicher, dass die Anforderung authentifiziert wird, bevor Benutzer etwas tun können. Dies 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 mächtigere Dinge zu tun.

Lassen Sie uns die Überprüfungsschreibvorgänge so einschränken, dass die Benutzer-ID der Überprüfung mit der ID des authentifizierten Benutzers übereinstimmen muss. Dies stellt sicher, dass Benutzer sich nicht gegenseitig 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 entspricht der Untersammlung mit dem Namen " ratings eines Dokuments, das zur restaurants . Die Bedingung " allow write verhindert dann, dass eine Überprüfung gesendet wird, wenn die Benutzer-ID der Überprüfung nicht mit der des Benutzers übereinstimmt. Mit der zweiten Übereinstimmungsanweisung kann jeder authentifizierte Benutzer Restaurants lesen und in die Datenbank schreiben.

Dies funktioniert sehr gut für unsere Bewertungen, 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ürde genau dieses Regelwerk auch verhindern, dass Benutzer die Bewertungen anderer Benutzer ändern oder löschen. Firestore-Regeln können jedoch auch detaillierter verwendet werden, um das Schreiben auf einzelne Felder innerhalb von Dokumenten und nicht 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 einen Restaurantnamen oder -ort ä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, wobei die Funktionalität der Schaltfläche "Auffüllen" erhalten bleibt, die wir zu Beginn des Codelabs erstellt haben. Sobald ein Restaurant geschrieben ist, können Name, Standort, Preis und Kategorie nicht mehr geändert werden. Insbesondere erfordert die letzte Regel, dass bei jeder Restaurantaktualisierung der Name, die Stadt, der Preis und die Kategorie der bereits vorhandenen Felder in der Datenbank beibehalten werden.

Weitere Informationen dazu, wie Sie mit Sicherheitsregeln umgehen können, finden Sie in der Dokumentation .

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

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