Cloud Firestore-Daten mit Swift Codable zuordnen

Mit der Codable API von Swift, die in Swift 4 eingeführt wurde, können wir die Leistungsfähigkeit des Compilers nutzen, um Daten aus serialisierten Formaten einfacher Swift-Typen zuzuordnen.

Vielleicht haben Sie Codable verwendet, um Daten von einer Web-API dem Datenmodell Ihrer App zuzuordnen (und umgekehrt). Es ist jedoch viel flexibler.

In diesem Leitfaden wird beschrieben, wie Sie mit Codable Daten aus Cloud Firestore Swift-Typen zuordnen und umgekehrt.

Wenn Sie ein Dokument aus Cloud Firestore abrufen, erhält Ihre Anwendung ein Dictionary mit Schlüssel/Wert-Paaren (oder ein Array von Dictionaries, wenn Sie einen der Vorgänge verwenden, die mehrere Dokumente zurückgeben).

Sie können natürlich weiterhin Wörterbücher in Swift verwenden. Sie bieten eine große Flexibilität, die für Ihren Anwendungsfall genau das Richtige sein kann. Dieser Ansatz ist jedoch nicht typsicher und es ist leicht, schwer zu findende Fehler einzuführen, indem Attributnamen falsch geschrieben werden oder vergessen wird, das neue Attribut zuzuordnen, das Ihr Team hinzugefügt hat, als es letzte Woche diese spannende neue Funktion veröffentlicht hat.

In der Vergangenheit haben viele Entwickler diese Mängel umgangen, indem sie eine einfache Zuordnungsebene implementiert haben, 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 Anwendung manuell angegeben wird.

Mit der Unterstützung von Cloud Firestore für die Codable API von Swift wird dies viel einfacher:

  • Sie müssen keinen Zuordnungscode mehr manuell implementieren.
  • Es ist ganz einfach, festzulegen, wie Attribute mit unterschiedlichen Namen zugeordnet werden sollen.
  • Es bietet integrierte Unterstützung für viele Swift-Typen.
  • Außerdem lässt sich die Unterstützung für die Zuordnung benutzerdefinierter Typen ganz einfach hinzufügen.
  • Das Beste daran: Für einfache Datenmodelle müssen Sie überhaupt keinen Zuordnungscode schreiben.

Zuordnungsdaten

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

Das bedeutet, dass wir mit der Subskript-Syntax von Swift auf jedes einzelne Feld zugreifen können.

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 sein, ist aber instabil, schwer zu verwalten und fehleranfällig.

Wie Sie sehen, treffen wir Annahmen über die Datentypen der Dokumentfelder. Diese können richtig oder falsch sein.

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. Möglicherweise wählen Sie versehentlich „String“ für das Feld numberOfPages aus, was zu einem schwer zu findenden Zuordnungsproblem führen würde. Außerdem müssen Sie den Zuordnungscode jedes Mal aktualisieren, wenn ein neues Feld hinzugefügt wird, was ziemlich umständlich ist.

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

Was ist Codable?

Laut der Apple-Dokumentation ist Codable „ein Typ, der sich selbst in eine externe Darstellung und aus einer externen Darstellung konvertieren kann“. Tatsächlich ist „Codable“ ein Typ-Alias 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 minimalinvasiv. Wir mussten nur die Konformität mit dem Protokoll hinzufügen. Es waren keine weiteren Änderungen erforderlich.

Damit können wir ein Buch ganz einfach 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)")
}

Das Decodieren eines JSON-Objekts in eine Book-Instanz funktioniert so:

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

Zuordnung zu und von einfachen Typen in Cloud Firestore-Dokumenten mit Codable

Cloud Firestore unterstützt eine Vielzahl von Datentypen, von einfachen Strings bis hin zu verschachtelten Maps. Die meisten davon entsprechen direkt den integrierten Typen von Swift. Sehen wir uns zuerst die Zuordnung einiger einfacher Datentypen an, bevor wir uns mit den komplexeren Datentypen befassen.

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

  1. Achten Sie darauf, dass Sie das FirebaseFirestore-Framework zu Ihrem Projekt hinzugefügt haben. Dazu können Sie entweder den Swift Package Manager oder CocoaPods verwenden.
  2. Importieren Sie FirebaseFirestore in Ihre Swift-Datei.
  3. Passen Sie Ihren Typ an Codable an.
  4. (Optional, wenn Sie den Typ in einer List-Ansicht verwenden möchten) Fügen Sie Ihrem Typ ein id-Attribut hinzu und verwenden Sie @DocumentID, um Cloud Firestore mitzuteilen, dass dieses Attribut der Dokument-ID zugeordnet werden soll. Darauf gehen wir weiter unten noch genauer ein.
  5. Verwenden Sie documentReference.data(as: ), um eine Dokumentreferenz einem Swift-Typ zuzuordnen.
  6. Verwenden Sie documentReference.setData(from: ), um Daten von Swift-Typen einem Cloud Firestore-Dokument zuzuordnen.
  7. (Optional, aber dringend empfohlen) Implementieren Sie eine ordnungsgemäße Fehlerbehandlung.

Aktualisieren wir den Typ Book 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 mit dem Property-Wrapper @DocumentID annotieren.

Wenn wir das vorherige Code-Snippet zum Abrufen und Zuordnen eines Dokuments verwenden, können wir den gesamten manuellen Zuordnungscode 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 dies noch prägnanter formulieren, indem Sie beim Aufrufen von getDocument(as:) den Typ des Dokuments angeben. Dadurch wird die Zuordnung für Sie durchgeführt und ein Result-Typ mit dem zugeordneten Dokument oder ein Fehler zurückgegeben, falls die Decodierung fehlgeschlagen ist:

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

Ein vorhandenes Dokument lässt sich ganz einfach durch Aufrufen von documentReference.setData(from: ) aktualisieren. Hier ist der Code zum Speichern einer Book-Instanz, einschließlich einer einfachen 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 sogar, 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)
  }
}

Zusätzlich zur Zuordnung einfacher Datentypen unterstützt Cloud Firestore eine Reihe anderer Datentypen, von denen einige strukturierte Typen sind, 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 Buchs oder der Name des Autors. Was aber, 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 der entsprechenden Swift-Struktur können wir die Tatsache nutzen, dass Cloud Firestore URLs unterstützt. Wenn ein Feld mit einer URL gespeichert wird, wird es in einen String konvertiert 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 CoverImages für die Cover-Karte definiert haben. Wenn wir das Attribut „cover“ für BookWithCoverImages als optional kennzeichnen, können wir berücksichtigen, dass einige Dokumente möglicherweise kein Attribut „cover“ enthalten.

Wenn Sie sich fragen, warum es kein Code-Snippet zum Abrufen oder Aktualisieren von Daten gibt, werden Sie sich freuen, dass der Code zum Lesen oder Schreiben von/in Cloud Firestore nicht angepasst werden muss: All dies funktioniert 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 Buchs sind ein gutes Beispiel: Ein Buch wie Per Anhalter durch die Galaxis kann in mehrere Kategorien fallen – in diesem Fall „Sci-Fi“ und „Comedy“:

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 (z. B. String, Int usw.) unterstützt. Im Folgenden wird gezeigt, wie Sie dem Book-Modell ein Array von Genres hinzufügen:

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. Zusammen mit dem Namen des Tags möchten wir auch die Farbe des Tags speichern, etwa so:

Array von benutzerdefinierten Typen in einem Firestore-Dokument speichern

Wenn Sie Tags auf diese Weise speichern möchten, müssen Sie nur eine Tag-Struktur implementieren, um ein Tag darzustellen und codierbar zu machen:

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

Und schon 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]
}

Einige Hinweise zur Zuordnung von Dokument-IDs

Bevor wir weitere Typen zuordnen, sprechen wir kurz über die Zuordnung von Dokument-IDs.

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

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

Ein als @DocumentID markiertes Attribut 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 für Book in einem früheren Beispiel in diesem Leitfaden), ist es nicht erforderlich, ein @DocumentID-Attribut hinzuzufügen: Verschachtelte Attribute sind Teil des Cloud Firestore-Dokuments und stellen kein separates Dokument dar. Daher benötigen sie keine Dokument-ID.

Datums- und Uhrzeitwerte

Cloud Firestore hat einen integrierten Datentyp für die Verarbeitung von Datums- und Uhrzeitangaben. Dank der Unterstützung von Codable in Cloud Firestore ist die Verwendung ganz einfach.

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

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 über Datums- und Uhrzeitangaben nicht abschließen, ohne über @ServerTimestamp zu sprechen. Dieser Property-Wrapper ist ein echtes Kraftpaket, wenn es um die Verarbeitung von Zeitstempeln in Ihrer App geht.

In jedem verteilten System ist es wahrscheinlich, dass die Uhren der einzelnen Systeme nicht immer vollständig synchron sind. Sie denken vielleicht, dass das kein großes Problem ist, aber stellen Sie sich die Auswirkungen einer leicht asynchronen Uhr für ein Börsenhandelssystem vor: 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 (z. B. mit addDocument()) nil ist, füllt Cloud Firestore das Feld mit dem aktuellen Serverzeitstempel, wenn es in die Datenbank geschrieben wird. Wenn das Feld beim Aufrufen von addDocument() oder updateData() nicht nil ist, bleibt der Attributwert von Cloud Firestore unverändert. So lassen sich Felder wie createdAt und lastUpdatedAt einfach implementieren.

Geopunkte

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

Cloud Firestore hat einen integrierten Datentyp, GeoPoint, mit dem der Längen- und Breitengrad eines beliebigen Standorts gespeichert werden kann. Wenn Sie Standorte aus einem Cloud Firestore-Dokument oder in ein Dokument zuordnen möchten, können Sie den Typ GeoPoint verwenden:

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

Der entsprechende Typ in Swift ist CLLocationCoordinate2D. Mit dem folgenden Vorgang können wir zwischen diesen beiden Typen zuordnen:

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

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

Enums

Enums sind wahrscheinlich eines der am meisten unterschätzten Sprachfeatures in Swift. Sie bieten viel mehr, als man auf den ersten Blick sieht. Ein häufiger Anwendungsfall für Enums ist die Modellierung der diskreten Zustände eines Objekts. Angenommen, wir schreiben eine App zum Verwalten von Artikeln. Um den Status eines Artikels zu verfolgen, können wir ein Enum Status verwenden:

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

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

Und da Swift benutzerdefinierte Rohwerte unterstützt, können wir sogar anpassen, welche Werte sich auf welchen Enum-Fall beziehen. Wenn wir beispielsweise den Fall Status.inReview als „in review“ speichern möchten, können wir das obige Enum 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 zuordnen möchten, nicht mit den Namen der Properties in unserem Datenmodell in Swift überein. Beispielsweise könnte einer unserer Kollegen ein Python-Entwickler sein und sich für snake_case für alle seine Attributnamen entschieden haben.

Keine Sorge: Codable hat die Lösung!

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

Betrachten Sie dieses Dokument:

Ein Firestore-Dokument mit einem Attributnamen in Snake-Case-Schreibweise

Um dieses Dokument einer Struktur mit einem Namensattribut vom Typ String zuzuordnen, müssen wir der Struktur 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 Attributnamen der Swift-Typen, um die Attributnamen in den Cloud Firestore-Dokumenten zu bestimmen, die zugeordnet werden sollen. Solange die Attributnamen übereinstimmen, müssen wir unseren codierbaren Typen also nicht CodingKeys hinzufügen. Wenn wir jedoch CodingKeys für einen bestimmten Typ verwenden, müssen wir alle Attributnamen hinzufügen, die wir zuordnen möchten.

Im obigen Code-Snippet haben wir ein id-Attribut definiert, das wir als Kennung in einer SwiftUI-List-Ansicht verwenden können. Wenn wir es nicht in CodingKeys angegeben hätten, würde es beim Abrufen von Daten nicht zugeordnet und somit zu nil werden. Dadurch wird die Ansicht List mit dem ersten Dokument gefüllt.

Alle Attribute, die nicht als Fall im entsprechenden CodingKeys-Enum aufgeführt sind, werden während des Zuordnungsprozesses ignoriert. Das kann praktisch sein, wenn wir bestimmte Properties explizit von der Zuordnung ausschließen möchten.

Wenn wir beispielsweise die Property reasonWhyILoveThis von der Zuordnung ausschließen möchten, müssen wir sie nur aus dem Enum CodingKeys 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 in das Cloud Firestore-Dokument zurückschreiben. Swift verwendet Optionals, um das Fehlen eines Werts anzugeben. Cloud Firestore unterstützt auch null-Werte. Das Standardverhalten für die Codierung von optionalen Werten mit dem Wert nil besteht jedoch darin, sie einfach wegzulassen. @ExplicitNull gibt uns eine gewisse Kontrolle darüber, wie Swift-Optionals beim Codieren behandelt werden: Wenn wir eine optionale Property als @ExplicitNull kennzeichnen, können wir Cloud Firestore anweisen, diese Property 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 im Rahmen der Datenzuordnung mit Codable stellen wir benutzerdefinierte Encoder und Decoder vor. In diesem Abschnitt wird kein nativer Cloud Firestore-Datentyp behandelt, aber benutzerdefinierte Encoder und Decoder sind in Ihren Cloud Firestore-Anwendungen sehr nützlich.

„Wie kann ich Farben zuordnen?“ ist eine der am häufigsten gestellten Entwicklerfragen, 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 bilden Farben als verschachteltes Dictionary ab, das aus den RGB-Komponenten besteht.

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

Damit ein Swift-Color seinem Hex-Wert zugeordnet werden kann, 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 das entsprechende Color-Äquivalent decodieren, ohne die RGBA-Komponenten verschachteln zu müssen. Außerdem können Sie diese Werte in der Web-UI Ihrer App verwenden, ohne sie vorher konvertieren zu müssen.

Damit können wir den Code für die Zuordnung von Tags aktualisieren und die Tag-Farben direkt verarbeiten, anstatt sie manuell im UI-Code unserer Anwendung 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 darauf achten, alle Fehler ordnungsgemäß zu behandeln.

Das folgende Code-Snippet zeigt, wie Sie mit Fehlersituationen umgehen können, die auftreten 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)"
        }
      }
    }
  }
}

Fehler bei Livemeldungen beheben

Das vorherige Code-Snippet zeigt, wie Fehler beim Abrufen eines einzelnen Dokuments behandelt werden. Zusätzlich zum einmaligen Abrufen von Daten unterstützt Cloud Firestore auch die Bereitstellung von Updates für Ihre App, sobald sie auftreten, mithilfe sogenannter Snapshot-Listener: Wir können einen Snapshot-Listener für eine Sammlung (oder Abfrage) registrieren und Cloud Firestore ruft unseren Listener auf, wenn ein Update verfügbar ist.

Hier ist ein Code-Snippet, das zeigt, wie Sie einen Snapshot-Listener registrieren, Daten mit Codable zuordnen und eventuell auftretende Fehler beheben. Außerdem wird gezeigt, wie Sie der Sammlung ein neues Dokument hinzufügen. Wie Sie sehen werden, müssen wir das lokale Array mit den zugeordneten Dokumenten 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 zu übertragen. In diesem Leitfaden haben Sie gesehen, wie einfach die Verwendung in Anwendungen 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 darauf verlassen, dass Codable und die Firebase-Implementierung die Zuordnung für uns übernehmen.

Weitere Informationen zu Codable finden Sie in den folgenden Ressourcen:

Wir haben uns bemüht, einen umfassenden Leitfaden für die Zuordnung von Cloud Firestore-Dokumenten zu erstellen. Er ist jedoch nicht vollständig und Sie verwenden möglicherweise andere Strategien, um Ihre Typen zuzuordnen. Über die Schaltfläche Feedback geben unten können Sie uns mitteilen, welche Strategien Sie für die Zuordnung anderer Arten von Cloud Firestore-Daten oder für die Darstellung von Daten in Swift verwenden.

Es gibt wirklich keinen Grund, die Codable-Unterstützung von Cloud Firestore nicht zu verwenden.