Cloud Firestore-Daten mit Swift Codable zuordnen

Die Codable API von Swift, die in Swift 4 eingeführt wurde, ermöglicht es uns, die Leistung des Compilers zu nutzen, um die Zuordnung von Daten aus serialisierten Formaten zu Swift-Typen zu vereinfachen.

Möglicherweise haben Sie Codable verwendet, um Daten aus einer Webanwendung in das Datenmodell Ihrer App zuzuordnen (und umgekehrt). Es ist jedoch viel flexibler als das.

In diesem Leitfaden sehen wir uns an, wie mit Codable Daten von Cloud Firestore in Swift-Typen und umgekehrt zugeordnet werden können.

Wenn ein Dokument von Cloud Firestore abgerufen wird, erhält Ihre App ein Wörterbuch mit Schlüssel/Wert-Paaren (oder ein Array von Wörterbüchern, wenn Sie einen der Vorgänge verwenden, bei denen mehrere Dokumente zurückgegeben werden).

Sie können natürlich weiterhin direkt Wörterbücher in Swift verwenden. Sie bieten eine große Flexibilität, die genau für Ihren Anwendungsfall geeignet sein könnte. Dieser Ansatz ist jedoch nicht typsicher und es ist leicht, schwer zu findende Fehler zu verursachen, wenn Sie Attribute falsch schreiben oder vergessen, das neue Attribut zuzuordnen, das Ihr Team hinzugefügt hat, als es letzte Woche die spannende neue Funktion veröffentlicht hat.

In der Vergangenheit haben viele Entwickler diese Mängel durch die Implementierung einer einfachen Zuordnungsebene umgangen, mit der sie Wörterbücher Swift-Typen zuordnen konnten. Die meisten dieser Implementierungen basieren jedoch darauf, dass die Zuordnung zwischen Cloud Firestore-Dokumenten und den entsprechenden Typen des Datenmodells Ihrer App manuell angegeben wird.

Dank der Unterstützung der Codable API von Swift in Cloud Firestore wird das viel einfacher:

  • Sie müssen keinen Zuordnungscode mehr manuell implementieren.
  • Sie können ganz einfach festlegen, wie Attribute mit unterschiedlichen Namen zugeordnet werden.
  • Es bietet eine integrierte Unterstützung für viele der Swift-Typen.
  • Außerdem können Sie ganz einfach Unterstützung für die Zuordnung benutzerdefinierter Typen hinzufügen.
  • Das Beste daran: Bei einfachen Datenmodellen müssen Sie überhaupt keinen Mapping-Code schreiben.

Zuordnungsdaten

In Cloud Firestore werden Daten in Dokumenten gespeichert, die Schlüssel zu Werten zuordnen. Wenn wir Daten aus einem einzelnen Dokument abrufen möchten, können wir DocumentSnapshot.data() aufrufen. Dadurch wird ein Wörterbuch zurückgegeben, in dem die Feldnamen einem Any zugeordnet sind: func data() -> [String : Any]?.

Das bedeutet, dass wir die Unterstrichsyntax von Swift verwenden können, um auf jedes einzelne Feld zuzugreifen.

import FirebaseFirestore

#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        let id = document.documentID
        let data = document.data()
        let title = data?["title"] as? String ?? ""
        let numberOfPages = data?["numberOfPages"] as? Int ?? 0
        let author = data?["author"] as? String ?? ""
        self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
      }
    }
  }
}

Dieser Code mag zwar einfach und leicht zu implementieren erscheinen, ist aber empfindlich, schwer zu pflegen und fehleranfällig.

Wie Sie sehen, treffen wir Annahmen über die Datentypen der Dokumentfelder. Diese Angaben sind möglicherweise nicht korrekt.

Da es kein Schema gibt, können Sie der Sammlung ganz einfach ein neues Dokument hinzufügen und einen anderen Typ für ein Feld auswählen. Es kann passieren, dass Sie versehentlich „String“ für das Feld numberOfPages auswählen. Das würde zu einem schwer zu findenden Zuordnungsproblem führen. Außerdem müssen Sie den Zuordnungscode jedes Mal aktualisieren, wenn ein neues Feld hinzugefügt wird. Das ist ziemlich mühsam.

Außerdem nutzen wir nicht das strenge Typsystem von Swift, das genau den richtigen Typ für jede der Eigenschaften von Book kennt.

Was ist Codable?

Laut der Dokumentation von Apple ist Codable „ein Typ, der sich in eine externe Darstellung umwandeln und wieder daraus zurückwandeln lässt“. „Codable“ ist in Wirklichkeit ein Typalias für die Protokolle „Encodable“ und „Decodable“. Wenn ein Swift-Typ diesem Protokoll entspricht, synthetisiert der Compiler den Code, der zum Codieren/Decodieren einer Instanz dieses Typs aus einem serialisierten Format wie JSON erforderlich ist.

Ein einfacher Typ zum Speichern von Daten zu einem Buch könnte so aussehen:

struct Book: Codable {
  var title: String
  var numberOfPages: Int
  var author: String
}

Wie Sie sehen, ist die Anpassung des Typs an Codable nur minimal. Wir mussten nur die Konformität mit dem Protokoll hinzufügen. Es waren keine weiteren Änderungen erforderlich.

So können wir jetzt ganz einfach ein Buch in ein JSON-Objekt codieren:

do {
  let book = Book(title: "The Hitchhiker's Guide to the Galaxy",
                  numberOfPages: 816,
                  author: "Douglas Adams")
  let encoder = JSONEncoder()
  let data = try encoder.encode(book)
} 
catch {
  print("Error when trying to encode book: \(error)")
}

So decodierst du ein JSON-Objekt in eine Book-Instanz:

let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)

Mit Codable von und zu einfachen Typen in Cloud Firestore-Dokumenten
mappen

Cloud Firestore unterstützt eine breite Palette von Datentypen, von einfachen Strings bis hin zu verschachtelten Maps. Die meisten davon entsprechen direkt den vordefinierten Typen von Swift. Sehen wir uns zuerst einige einfache Datentypen an, bevor wir uns den komplexeren zuwenden.

So ordnen Sie Cloud Firestore-Dokumente Swift-Typen zu:

  1. Achten Sie darauf, dass Sie Ihrem Projekt das FirebaseFirestore-Framework hinzugefügt haben. Dazu können Sie den Swift Package Manager oder CocoaPods verwenden.
  2. Importieren Sie FirebaseFirestore in Ihre Swift-Datei.
  3. Der Typ muss Codable entsprechen.
  4. Optional, wenn Sie den Typ in einer List-Ansicht verwenden möchten: Fügen Sie dem Typ eine id-Property hinzu und verwenden Sie @DocumentID, um Cloud Firestore anzuweisen, diese der Dokument-ID zuzuordnen. Dazu kommen wir gleich.
  5. Mit documentReference.data(as: ) können Sie eine Dokumentreferenz einem Swift-Typ zuordnen.
  6. Mit documentReference.setData(from: ) kannst du Daten von Swift-Typen einem Cloud Firestore-Dokument zuordnen.
  7. (Optional, aber dringend empfohlen) Implementieren Sie eine ordnungsgemäße Fehlerbehandlung.

Aktualisieren wir unseren Book-Typ entsprechend:

struct Book: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
}

Da dieser Typ bereits codierbar war, mussten wir nur die Property id hinzufügen und sie mit dem Property-Wrapper @DocumentID annotieren.

Wenn wir das vorherige Code-Snippet zum Abrufen und Zuordnen eines Dokuments verwenden, können wir den gesamten Code für die manuelle Zuordnung durch eine einzige Zeile ersetzen:

func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        do {
          self.book = try document.data(as: Book.self)
        }
        catch {
          print(error)
        }
      }
    }
  }
}

Sie können diesen Code noch prägnanter formulieren, indem Sie beim Aufruf von getDocument(as:) den Dokumenttyp angeben. Dadurch wird die Zuordnung für Sie ausgeführt und es wird ein Result-Typ mit dem zugeordneten Dokument zurückgegeben. Falls die Dekodierung fehlgeschlagen ist, wird ein Fehler zurückgegeben:

private func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)
  
  docRef.getDocument(as: Book.self) { result in
    switch result {
    case .success(let book):
      // A Book value was successfully initialized from the DocumentSnapshot.
      self.book = book
      self.errorMessage = nil
    case .failure(let error):
      // A Book value could not be initialized from the DocumentSnapshot.
      self.errorMessage = "Error decoding document: \(error.localizedDescription)"
    }
  }
}

Zum Aktualisieren eines vorhandenen Dokuments müssen Sie einfach nur documentReference.setData(from: ) aufrufen. Hier ist der Code zum Speichern einer Book-Instanz, einschließlich einer grundlegenden Fehlerbehandlung:

func updateBook(book: Book) {
  if let id = book.id {
    let docRef = db.collection("books").document(id)
    do {
      try docRef.setData(from: book)
    }
    catch {
      print(error)
    }
  }
}

Wenn Sie ein neues Dokument hinzufügen, wird ihm von Cloud Firestore automatisch eine neue Dokument-ID zugewiesen. Das funktioniert auch, wenn die App gerade offline ist.

func addBook(book: Book) {
  let collectionRef = db.collection("books")
  do {
    let newDocReference = try collectionRef.addDocument(from: self.book)
    print("Book stored with new document reference: \(newDocReference)")
  }
  catch {
    print(error)
  }
}

Neben der Zuordnung einfacher Datentypen unterstützt Cloud Firestore eine Reihe weiterer Datentypen, darunter strukturierte Typen, mit denen Sie verschachtelte Objekte in einem Dokument erstellen können.

Verschachtelte benutzerdefinierte Typen

Die meisten Attribute, die wir in unseren Dokumenten zuordnen möchten, sind einfache Werte wie der Titel des Buches oder der Name des Autors. Aber was ist, wenn wir ein komplexeres Objekt speichern müssen? Beispielsweise möchten wir die URLs zum Buchcover in verschiedenen Auflösungen speichern.

Am einfachsten geht das in Cloud Firestore mit einer Karte:

Verschachtelten benutzerdefinierten Typ in einem Firestore-Dokument speichern

Beim Schreiben des entsprechenden Swift-Structs können wir davon ausgehen, dass Cloud Firestore URLs unterstützt. Beim Speichern eines Felds, das eine URL enthält, wird es in einen String umgewandelt und umgekehrt:

struct CoverImages: Codable {
  var small: URL
  var medium: URL
  var large: URL
}

struct BookWithCoverImages: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var cover: CoverImages?
}

Beachten Sie, dass wir im Dokument Cloud Firestore eine Struktur namens CoverImages für die Abdeckungskarte definiert haben. Durch die Kennzeichnung des Attributs „Cover“ unter BookWithCoverImages als optional können wir damit umgehen, dass einige Dokumente möglicherweise kein Cover-Attribut enthalten.

Falls Sie sich fragen, warum es kein Code-Snippet zum Abrufen oder Aktualisieren von Daten gibt: Sie müssen den Code zum Lesen oder Schreiben von/nach Cloud Firestore nicht anpassen. Das funktioniert alles mit dem Code, den wir im ersten Abschnitt geschrieben haben.

Arrays

Manchmal möchten wir eine Sammlung von Werten in einem Dokument speichern. Die Genres eines Buches sind ein gutes Beispiel: Ein Buch wie Per Anhalter durch die Galaxis könnte in mehrere Kategorien fallen – in diesem Fall „Science-Fiction“ und „Komödie“:

Array in einem Firestore-Dokument speichern

In Cloud Firestore können wir dies mit einem Array von Werten modellieren. Dies wird für alle codierbaren Typen unterstützt, z. B. String und Int. Im Folgenden wird gezeigt, wie unserem Book-Modell eine Reihe von Genres hinzugefügt wird:

public struct BookWithGenre: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var genres: [String]
}

Da dies für jeden codierbaren Typ funktioniert, können wir auch benutzerdefinierte Typen verwenden. Angenommen, wir möchten für jedes Buch eine Liste mit Tags speichern. Neben dem Namen des Tags möchten wir auch die Farbe des Tags speichern, so:

Array von benutzerdefinierten Typen in einem Firestore-Dokument speichern

Um Tags auf diese Weise zu speichern, müssen wir lediglich eine Tag-Struktur implementieren, um ein Tag darzustellen und codierbar zu machen:

struct Tag: Codable, Hashable {
  var title: String
  var color: String
}

So können wir ein Array von Tags in unseren Book-Dokumenten speichern.

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

Zuordnung von Dokument-IDs

Bevor wir mit der Zuordnung weiterer Typen fortfahren, sehen wir uns die Zuordnung von Dokument-IDs an.

In einigen der vorherigen Beispiele haben wir den Property-Wrapper @DocumentID verwendet, um die Dokument-ID unserer Cloud Firestore-Dokumente der id-Property unserer Swift-Typen zuzuordnen. Das ist aus verschiedenen Gründen wichtig:

  • So wissen wir, welches Dokument aktualisiert werden muss, falls der Nutzer lokale Änderungen vornimmt.
  • Für die List von SwiftUI müssen die Elemente Identifiable sein, damit sie beim Einfügen nicht herumspringen.

Hinweis: Ein Attribut, das als @DocumentID gekennzeichnet ist, wird beim Zurückschreiben des Dokuments nicht vom Encoder von Cloud Firestore codiert. Das liegt daran, dass die Dokument-ID kein Attribut des Dokuments selbst ist. Es wäre also ein Fehler, sie in das Dokument zu schreiben.

Wenn Sie mit verschachtelten Typen arbeiten (z. B. dem Array von Tags auf der Book in einem früheren Beispiel in diesem Leitfaden), müssen Sie keine @DocumentID-Property hinzufügen: verschachtelte Properties sind Teil des Cloud Firestore-Dokuments und stellen kein separates Dokument dar. Daher ist keine Dokument-ID erforderlich.

Datums- und Uhrzeitwerte

Cloud Firestore hat einen integrierten Datentyp für die Verarbeitung von Datumsangaben und Uhrzeiten. Dank der Codable-Unterstützung von Cloud Firestore lassen sich diese Daten ganz einfach verwenden.

Sehen wir uns dieses Dokument an, das die Mutter aller Programmiersprachen darstellt, Ada, die 1843 erfunden wurde:

Datumsangaben in einem Firestore-Dokument speichern

Ein Swift-Typ für die Zuordnung dieses Dokuments könnte so aussehen:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
}

Wir können diesen Abschnitt zu Terminen und Uhrzeiten nicht verlassen, ohne über @ServerTimestamp zu sprechen. Dieser Property-Wrapper ist ein echter Allrounder, wenn es um Zeitstempel in Ihrer App geht.

In jedem verteilten System ist die Wahrscheinlichkeit hoch, dass die Uhren der einzelnen Systeme nicht immer vollständig synchronisiert sind. Das mag Ihnen nicht so wichtig erscheinen, aber stellen Sie sich vor, was passiert, wenn eine Uhr für ein Aktienhandelssystem leicht asynchron läuft: Selbst eine Abweichung von einer Millisekunde kann bei der Ausführung eines Handels zu einer Differenz von Millionen von Dollar führen.

Cloud Firestore verarbeitet Attribute, die mit @ServerTimestamp gekennzeichnet sind, so: Wenn das Attribut beim Speichern nil ist (z. B. mit addDocument()), wird das Feld von Cloud Firestore beim Schreiben in die Datenbank mit dem aktuellen Serverzeitstempel ausgefüllt. Wenn das Feld beim Aufrufen von addDocument() oder updateData() nicht nil ist, bleibt der Attributwert bei Cloud Firestore unverändert. So lassen sich Felder wie createdAt und lastUpdatedAt ganz einfach implementieren.

Geopunkte

Standortinformationen sind in unseren Apps allgegenwärtig. Durch das Speichern werden viele spannende Funktionen möglich. Es kann beispielsweise nützlich sein, einen Standort für eine Aufgabe zu speichern, damit Ihre App Sie an eine Aufgabe erinnern kann, wenn Sie ein Ziel erreichen.

Cloud Firestore hat den integrierten Datentyp GeoPoint, mit dem der Längen- und Breitengrad eines beliebigen Orts gespeichert werden kann. Um Orte aus/in einem Cloud Firestore-Dokument abzugleichen, können wir den Typ GeoPoint verwenden:

struct Office: Codable {
  @DocumentID var id: String?
  var name: String
  var location: GeoPoint
}

Der entsprechende Typ in Swift ist CLLocationCoordinate2D. Wir können diese beiden Typen mit dem folgenden Vorgang abgleichen:

CLLocationCoordinate2D(latitude: office.location.latitude,
                      longitude: office.location.longitude)

Weitere Informationen zum Abfragen von Dokumenten nach Standort finden Sie in diesem Lösungsleitfaden.

Enums

Enums sind wahrscheinlich eine der am meisten unterschätzten Sprachfunktionen in Swift. Sie sind vielseitiger, als es auf den ersten Blick scheint. Ein häufiger Anwendungsfall für Enumerationen ist die Modellierung der diskreten Zustände von etwas. Angenommen, wir entwickeln eine App zum Verwalten von Artikeln. Um den Status eines Artikels zu verfolgen, können wir eine Enum-Status verwenden:

enum Status: String, Codable {
  case draft
  case inReview
  case approved
  case published
}

Cloud Firestore unterstützt keine Enumerationen nativ (d. h., es kann die Werte nicht erzwingen). Wir können jedoch trotzdem die Tatsache nutzen, dass Enumerationen typisiert werden können, und einen codierbaren Typ auswählen. In diesem Beispiel haben wir String ausgewählt. Das bedeutet, dass alle Werte der Aufzählung beim Speichern in einem Cloud Firestore-Dokument in einen String umgewandelt werden.

Da Swift benutzerdefinierte Rohwerte unterstützt, können wir sogar anpassen, welche Werte sich auf welchen enum-Fall beziehen. Wenn wir beispielsweise die Anfrage Status.inReview als „Wird geprüft“ speichern möchten, könnten wir die obige Enum-Liste einfach so aktualisieren:

enum Status: String, Codable {
  case draft
  case inReview = "in review"
  case approved
  case published
}

Zuordnung anpassen

Manchmal stimmen die Attributnamen der Cloud Firestore-Dokumente, die wir abgleichen möchten, nicht mit den Namen der Properties in unserem Datenmodell in Swift überein. Angenommen, einer unserer Mitarbeiter ist Python-Entwickler und hat sich entschieden, für alle Attributnamen die Schreibweise „Kamel-Case“ zu verwenden.

Keine Sorge: Codable ist für uns da.

In solchen Fällen können wir CodingKeys verwenden. Dies ist ein Enum, das wir einem codierbaren Typ hinzufügen können, um anzugeben, wie bestimmte Attribute zugeordnet werden.

Betrachten Sie dieses Dokument:

Ein Firestore-Dokument mit einem Attributnamen im Snake Case

Wenn wir dieses Dokument einem Typ mit einer Namenseigenschaft vom Typ String zuordnen möchten, müssen wir dem Typ ProgrammingLanguage ein CodingKeys-Enum hinzufügen und den Namen des Attributs im Dokument angeben:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

Standardmäßig verwendet die Codable API die Property-Namen unserer Swift-Typen, um die Attributnamen in den Cloud Firestore-Dokumenten zu ermitteln, die wir zuordnen möchten. Solange die Attributnamen übereinstimmen, müssen wir unseren codierbaren Typen also nicht CodingKeys hinzufügen. Wenn wir CodingKeys jedoch für einen bestimmten Typ verwenden, müssen wir alle Property-Namen hinzufügen, die wir zuordnen möchten.

Im obigen Code-Snippet haben wir eine id-Property definiert, die wir als Kennung in einer SwiftUI-List-Ansicht verwenden können. Wenn wir sie nicht in CodingKeys angeben, wird sie beim Abrufen von Daten nicht zugeordnet und wird zu nil. Dies würde dazu führen, dass die Ansicht List mit dem ersten Dokument gefüllt wird.

Alle Properties, die in der jeweiligen CodingKeys-Enumeration nicht als Fall aufgeführt sind, werden beim Zuordnungsprozess ignoriert. Das kann praktisch sein, wenn wir einige der Unterkünfte von der Kartierung ausschließen möchten.

Wenn wir beispielsweise die Property reasonWhyILoveThis von der Zuordnung ausschließen möchten, müssen wir sie nur aus dem CodingKeys-Enum entfernen:

struct ProgrammingLanguage: Identifiable, Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  var reasonWhyILoveThis: String = ""
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

Gelegentlich möchten wir ein leeres Attribut wieder in das Cloud Firestore-Dokument schreiben. In Swift gibt es das Konzept von Optionals, um das Fehlen eines Werts anzugeben. Cloud Firestore unterstützt auch null-Werte. Standardmäßig werden optionale Parameter mit dem Wert nil jedoch einfach weggelassen. Mit @ExplicitNull können wir die Verarbeitung von optionalen Swift-Werten bei der Codierung steuern: Wenn wir eine optionale Eigenschaft als @ExplicitNull kennzeichnen, können wir Cloud Firestore anweisen, diese Eigenschaft mit einem Nullwert in das Dokument zu schreiben, wenn sie den Wert nil enthält.

Benutzerdefinierten Encoder und Decoder für die Farbzuordnung verwenden

Als letztes Thema in unserer Übersicht zum Zuordnen von Daten mit Codable stellen wir benutzerdefinierte Encoder und Decoder vor. In diesem Abschnitt wird kein nativer Cloud Firestore-Datentyp behandelt. Benutzerdefinierte Encoder und Decoder sind jedoch in Ihren Cloud Firestore-Apps weit verbreitet.

„Wie kann ich Farben zuordnen?“ ist eine der am häufigsten gestellten Fragen von Entwicklern, nicht nur für Cloud Firestore, sondern auch für die Zuordnung zwischen Swift und JSON. Es gibt viele Lösungen, aber die meisten konzentrieren sich auf JSON und fast alle ordnen Farben als verschachteltes Wörterbuch zu, das aus den RGB-Komponenten besteht.

Es sollte eine bessere, einfachere Lösung geben. Warum verwenden wir keine Webfarben (oder genauer gesagt die CSS-Hex-Farbnotation)? Sie sind einfach zu verwenden (im Grunde nur ein String) und unterstützen sogar Transparenz.

Damit wir eine Swift-Color einem Hexadezimalwert zuordnen können, müssen wir eine Swift-Erweiterung erstellen, die Color Codable hinzufügt.

extension Color {

 init(hex: String) {
    let rgba = hex.toRGBA()

    self.init(.sRGB,
              red: Double(rgba.r),
              green: Double(rgba.g),
              blue: Double(rgba.b),
              opacity: Double(rgba.alpha))
    }

    //... (code for translating between hex and RGBA omitted for brevity)

}

extension Color: Codable {
  
  public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)

    self.init(hex: hex)
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(toHex)
  }

}

Mit decoder.singleValueContainer() können wir ein String in sein Color-Äquivalent decodieren, ohne die RGBA-Komponenten verschachteln zu müssen. Außerdem können Sie diese Werte in der Web-Benutzeroberfläche Ihrer App verwenden, ohne sie zuerst konvertieren zu müssen.

So können wir den Code zum Zuordnen von Tags aktualisieren, was die direkte Verwendung der Tag-Farben erleichtert, anstatt sie manuell im UI-Code unserer App zuordnen zu müssen:

struct Tag: Codable, Hashable {
  var title: String
  var color: Color
}

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

Fehlerbehebung

In den obigen Code-Snippets haben wir die Fehlerbehandlung bewusst auf ein Minimum beschränkt. In einer Produktions-App sollten Sie jedoch alle Fehler möglichst nutzerfreundlich behandeln.

Hier ist ein Code-Snippet, das zeigt, wie Sie mit möglichen Fehlersituationen umgehen können:

class MappingSimpleTypesViewModel: ObservableObject {
  @Published var book: Book = .empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  
  func fetchAndMap() {
    fetchBook(documentId: "hitchhiker")
  }
  
  func fetchAndMapNonExisting() {
    fetchBook(documentId: "does-not-exist")
  }
  
  func fetchAndTryMappingInvalidData() {
    fetchBook(documentId: "invalid-data")
  }
  
  private func fetchBook(documentId: String) {
    let docRef = db.collection("books").document(documentId)
    
    docRef.getDocument(as: Book.self) { result in
      switch result {
      case .success(let book):
        // A Book value was successfully initialized from the DocumentSnapshot.
        self.book = book
        self.errorMessage = nil
      case .failure(let error):
        // A Book value could not be initialized from the DocumentSnapshot.
        switch error {
        case DecodingError.typeMismatch(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.valueNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.keyNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.dataCorrupted(let key):
          self.errorMessage = "\(error.localizedDescription): \(key)"
        default:
          self.errorMessage = "Error decoding document: \(error.localizedDescription)"
        }
      }
    }
  }
}

Umgang mit Fehlern bei Liveaktualisierungen

Im vorherigen Code-Snippet wird gezeigt, wie mit Fehlern beim Abrufen eines einzelnen Dokuments umgegangen wird. Neben dem einmaligen Abrufen von Daten unterstützt Cloud Firestore auch die Übermittlung von Updates an Ihre App in Echtzeit mithilfe von sogenannten Snapshot-Listenern: Wir können einen Snapshot-Listener für eine Sammlung (oder Abfrage) registrieren und Cloud Firestore ruft unseren Listener bei jeder Aktualisierung auf.

Im folgenden Code-Snippet wird gezeigt, wie Sie einen Snapshot-Listener registrieren, Daten mit Codable zuordnen und mögliche Fehler behandeln. Außerdem wird gezeigt, wie Sie der Sammlung ein neues Dokument hinzufügen. Wie Sie sehen, müssen wir das lokale Array, das die zugeordneten Dokumente enthält, nicht selbst aktualisieren, da dies vom Code im Snapshot-Listener übernommen wird.

class MappingColorsViewModel: ObservableObject {
  @Published var colorEntries = [ColorEntry]()
  @Published var newColor = ColorEntry.empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  private var listenerRegistration: ListenerRegistration?
  
  public func unsubscribe() {
    if listenerRegistration != nil {
      listenerRegistration?.remove()
      listenerRegistration = nil
    }
  }
  
  func subscribe() {
    if listenerRegistration == nil {
      listenerRegistration = db.collection("colors")
        .addSnapshotListener { [weak self] (querySnapshot, error) in
          guard let documents = querySnapshot?.documents else {
            self?.errorMessage = "No documents in 'colors' collection"
            return
          }
          
          self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
            let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
            
            switch result {
            case .success(let colorEntry):
              if let colorEntry = colorEntry {
                // A ColorEntry value was successfully initialized from the DocumentSnapshot.
                self?.errorMessage = nil
                return colorEntry
              }
              else {
                // A nil value was successfully initialized from the DocumentSnapshot,
                // or the DocumentSnapshot was nil.
                self?.errorMessage = "Document doesn't exist."
                return nil
              }
            case .failure(let error):
              // A ColorEntry value could not be initialized from the DocumentSnapshot.
              switch error {
              case DecodingError.typeMismatch(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.valueNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.keyNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.dataCorrupted(let key):
                self?.errorMessage = "\(error.localizedDescription): \(key)"
              default:
                self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
              }
              return nil
            }
          }
        }
    }
  }
  
  func addColorEntry() {
    let collectionRef = db.collection("colors")
    do {
      let newDocReference = try collectionRef.addDocument(from: newColor)
      print("ColorEntry stored with new document reference: \(newDocReference)")
    }
    catch {
      print(error)
    }
  }
}

Alle in diesem Beitrag verwendeten Code-Snippets sind Teil einer Beispielanwendung, die Sie aus diesem GitHub-Repository herunterladen können.

Viel Spaß mit Codable!

Die Codable API von Swift bietet eine leistungsstarke und flexible Möglichkeit, Daten aus serialisierten Formaten in das Datenmodell Ihrer Anwendung und umgekehrt abzubilden. In diesem Leitfaden haben Sie gesehen, wie einfach die Verwendung in Apps ist, die Cloud Firestore als Datenspeicher verwenden.

Ausgehend von einem einfachen Beispiel mit einfachen Datentypen haben wir die Komplexität des Datenmodells schrittweise erhöht. Dabei konnten wir uns auf Codable und die Firebase-Implementierung verlassen, um die Zuordnung für uns auszuführen.

Weitere Informationen zu Codable findest du in den folgenden Ressourcen:

Wir haben zwar unser Bestes getan, um einen umfassenden Leitfaden für die Zuordnung von Cloud Firestore-Dokumenten zu erstellen, dieser ist jedoch nicht vollständig. Möglicherweise verwenden Sie andere Strategien, um Ihre Typen abzugleichen. Über die Schaltfläche Feedback senden unten können Sie uns mitteilen, welche Strategien Sie zum Zuordnen anderer Arten von Cloud Firestore-Daten oder zum Darstellen von Daten in Swift verwenden.

Es gibt keinen Grund, den Codable-Support von Cloud Firestore nicht zu verwenden.