Ordnen Sie Cloud Firestore-Daten mit Swift Codable zu

Die in Swift 4 eingeführte Codable API von Swift ermöglicht es uns, die Leistungsfähigkeit 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 von einer Web-API dem Datenmodell Ihrer App zuzuordnen (und umgekehrt), aber es ist viel flexibler.

In diesem Leitfaden schauen wir uns an, wie Codable verwendet werden kann, um Daten von Cloud Firestore Swift-Typen und umgekehrt zuzuordnen.

Beim Abrufen eines Dokuments aus Cloud Firestore 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, die mehrere Dokumente zurückgeben).

Jetzt können Sie Wörterbücher in Swift sicherlich weiterhin direkt verwenden, und sie bieten eine große Flexibilität, die genau das sein könnte, was Ihr Anwendungsfall erfordert. Allerdings ist dieser Ansatz nicht typsicher und es ist leicht, schwer aufzuspürende Fehler einzuführen, indem man Attributnamen falsch schreibt oder vergisst, das neue Attribut zuzuordnen, das Ihr Team hinzugefügt hat, als es letzte Woche diese aufregende neue Funktion ausgeliefert hat.

In der Vergangenheit haben viele Entwickler diese Mängel umgangen, indem sie eine einfache Zuordnungsebene implementiert haben, die es ihnen ermöglichte, Wörterbücher Swift-Typen zuzuordnen. Aber auch hier basieren die meisten dieser Implementierungen auf der manuellen Angabe der Zuordnung zwischen Cloud Firestore-Dokumenten und den entsprechenden Typen des Datenmodells Ihrer App.

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 einfach zu definieren, wie Attribute mit unterschiedlichen Namen zugeordnet werden.
  • Es bietet integrierte Unterstützung für viele Swift-Typen.
  • Und es ist einfach, Unterstützung für die Zuordnung benutzerdefinierter Typen hinzuzufügen.
  • Das Beste daran: Für einfache Datenmodelle müssen Sie überhaupt keinen Mapping-Code schreiben.

Kartierungsdaten

Cloud Firestore speichert Daten in Dokumenten, die Schlüssel Werten zuordnen. Um Daten aus einem einzelnen Dokument abzurufen, können wir DocumentSnapshot.data() aufrufen, das ein Wörterbuch zurückgibt, das die Feldnamen einem Any : func data() -> [String : Any]? zuordnet. .

Das bedeutet, dass wir die Indexsyntax 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)
      }
    }
  }
}

Auch wenn er unkompliziert und leicht zu implementieren erscheint, ist dieser Code fragil, schwer zu warten und fehleranfällig.

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

Denken Sie daran: Da es kein Schema gibt, können Sie problemlos ein neues Dokument zur Sammlung hinzufügen und einen anderen Typ für ein Feld auswählen. Sie könnten versehentlich eine Zeichenfolge für das Feld numberOfPages auswählen, was zu einem schwer zu findenden Zuordnungsproblem führen würde. Außerdem müssen Sie Ihren Zuordnungscode jedes Mal aktualisieren, wenn ein neues Feld hinzugefügt wird, was ziemlich umständlich ist.

Und vergessen wir nicht, dass wir nicht das starke Typsystem von Swift nutzen, das für jede Eigenschaft von Book genau den richtigen Typ kennt.

Was ist überhaupt Codable?

Laut der Dokumentation von Apple ist Codable „ein Typ, der sich selbst in eine externe Darstellung und aus dieser heraus konvertieren kann“. Tatsächlich ist Codable ein Typalias für die Protokolle Encodable und Decodable. Durch die Anpassung eines Swift-Typs an dieses Protokoll synthetisiert der Compiler den Code, der zum Kodieren/Dekodieren einer Instanz dieses Typs aus einem serialisierten Format wie JSON erforderlich ist.

Ein einfacher Typ zum Speichern von Daten zu einem Buch könnte wie folgt 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 zum Protokoll hinzufügen; Es waren keine weiteren Änderungen erforderlich.

Damit können wir nun ganz einfach ein Buch in ein JSON-Objekt kodieren:

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 Dekodieren eines JSON-Objekts in eine Book Instanz funktioniert wie folgt:

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 breite Palette von Datentypen, die von einfachen Zeichenfolgen bis hin zu verschachtelten Karten reichen. Die meisten davon entsprechen direkt den integrierten Typen von Swift. Werfen wir zunächst einen Blick auf die Zuordnung einiger einfacher Datentypen, bevor wir uns mit den komplexeren befassen.

Gehen Sie folgendermaßen vor, um Cloud Firestore-Dokumente Swift-Typen zuzuordnen:

  1. Stellen Sie sicher, dass Sie das FirebaseFirestore Framework zu Ihrem Projekt hinzugefügt haben. Sie können dazu 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 verwenden möchten) Fügen Sie Ihrem Typ eine id Eigenschaft hinzu und weisen Sie Cloud Firestore mit @DocumentID an, diese der Dokument-ID zuzuordnen. Wir werden dies weiter unten ausführlicher besprechen.
  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 unseren 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 id Eigenschaft hinzufügen und sie mit dem @DocumentID Eigenschaftswrapper mit Anmerkungen versehen.

Mit dem vorherigen Codeausschnitt zum Abrufen und Zuordnen eines Dokuments 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 Aufruf getDocument(as:) den Typ des Dokuments angeben. Dies führt die Zuordnung für Sie durch und gibt einen Result zurück, der das zugeordnete Dokument enthält, oder einen Fehler, falls die Dekodierung 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)"
    }
  }
}

Das Aktualisieren eines vorhandenen Dokuments ist so einfach wie der Aufruf documentReference.setData(from: ) . Hier ist der Code zum Speichern einer Book Instanz, einschließlich einiger grundlegender 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)
    }
  }
}

Beim Hinzufügen eines neuen Dokuments kümmert sich Cloud Firestore automatisch darum, dem Dokument eine neue Dokument-ID zuzuweisen. Dies 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)
  }
}

Neben der Zuordnung einfacher Datentypen unterstützt Cloud Firestore eine Reihe anderer Datentypen, von denen einige strukturierte Typen sind, die Sie zum Erstellen verschachtelter Objekte in einem Dokument verwenden können.

Verschachtelte benutzerdefinierte Typen

Die meisten Attribute, die wir in unseren Dokumenten abbilden möchten, sind einfache Werte, wie zum Beispiel der Buchtitel oder der Name des Autors. Aber was ist mit den Fällen, in denen wir ein komplexeres Objekt speichern müssen? Beispielsweise möchten wir möglicherweise die URLs zum Buchcover in verschiedenen Auflösungen speichern.

Der einfachste Weg, dies in Cloud Firestore zu tun, ist die Verwendung einer Karte:

Speichern eines verschachtelten benutzerdefinierten Typs in einem Firestore-Dokument

Beim Schreiben der entsprechenden Swift-Struktur können wir uns die Tatsache zunutze machen, dass Cloud Firestore URLs unterstützt – wenn ein Feld gespeichert wird, das eine URL enthält, wird diese 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, wie wir eine Struktur, CoverImages , für die Cover-Map im Cloud Firestore-Dokument definiert haben. Indem wir die Cover-Eigenschaft für BookWithCoverImages als optional markieren, können wir die Tatsache bewältigen, dass einige Dokumente möglicherweise kein Cover-Attribut enthalten.

Wenn Sie neugierig sind, warum es kein Code-Snippet zum Abrufen oder Aktualisieren von Daten gibt, werden Sie erfreut sein zu hören, dass für das Lesen oder Schreiben von/in Cloud Firestore keine Anpassung des Codes erforderlich ist: All dies funktioniert mit dem Code, den wir verwenden Habe im ersten Abschnitt geschrieben.

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“ kann in mehrere Kategorien fallen – in diesem Fall „Sci-Fi“ und „Komödie“:

Speichern eines Arrays in einem Firestore-Dokument

In Cloud Firestore können wir dies mithilfe eines Arrays von Werten modellieren. Dies wird für jeden codierbaren Typ unterstützt (z. B. String , Int usw.). Im Folgenden wird gezeigt, wie Sie unserem Book eine Reihe 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. Stellen Sie sich vor, 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, etwa so:

Speichern eines Arrays benutzerdefinierter Typen in einem Firestore-Dokument

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

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

Und schon können wir eine Reihe von Tags in unseren Book speichern!

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

Ein kurzes Wort zur Zuordnung von Dokument-IDs

Bevor wir mit der Zuordnung weiterer Typen fortfahren, sprechen wir kurz über die Zuordnung von Dokument-IDs.

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

  • Es hilft uns zu wissen, welches Dokument aktualisiert werden muss, falls der Benutzer lokale Änderungen vornimmt.
  • List von SwiftUI erfordert, dass ihre Elemente Identifiable sind, um zu verhindern, dass Elemente beim Einfügen herumspringen.

Es ist erwähnenswert, dass ein als @DocumentID gekennzeichnetes Attribut beim Zurückschreiben des Dokuments nicht vom Encoder von Cloud Firestore codiert wird. Dies 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 im Book in einem früheren Beispiel in diesem Handbuch), ist es nicht erforderlich, eine @DocumentID Eigenschaft hinzuzufügen: Verschachtelte Eigenschaften sind Teil des Cloud Firestore-Dokuments und stellen kein Dokument dar ein separates Dokument. Daher benötigen sie keinen Dokumentenausweis.

Termine und Uhrzeiten

Cloud Firestore verfügt über einen integrierten Datentyp für die Verarbeitung von Datums- und Uhrzeitangaben. Dank der Unterstützung von Codable durch Cloud Firestore ist deren Verwendung unkompliziert.

Werfen wir einen Blick auf dieses Dokument, das die Mutter aller Programmiersprachen, Ada, darstellt, die 1843 erfunden wurde:

Speichern von Daten in einem Firestore-Dokument

Ein Swift-Typ zum Zuordnen dieses Dokuments könnte wie folgt aussehen:

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

Wir können diesen Abschnitt über Datum und Uhrzeit nicht verlassen, ohne ein Gespräch über @ServerTimestamp zu führen. Dieser Eigenschafts-Wrapper ist ein Kraftpaket, wenn es um den Umgang mit Zeitstempeln in Ihrer App geht.

In jedem verteilten System besteht die Möglichkeit, dass die Uhren der einzelnen Systeme nicht immer vollständig synchron sind. Sie denken vielleicht, dass das keine große Sache ist, aber stellen Sie sich die Auswirkungen vor, wenn die 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 mehreren Millionen Dollar führen.

Cloud Firestore behandelt mit @ServerTimestamp markierte Attribute wie folgt: Wenn das Attribut beim Speichern nil ist (z. B. mit addDocument() ), füllt Cloud Firestore das Feld mit dem aktuellen Server-Zeitstempel zum Zeitpunkt des Schreibens in die Datenbank . Wenn das Feld beim Aufruf addDocument() oder updateData() nicht nil ist, lässt Cloud Firestore den Attributwert unverändert. Auf diese Weise ist es einfach, Felder wie createdAt und lastUpdatedAt zu implementieren.

Geopunkte

Geolokalisierungen sind in unseren Apps allgegenwärtig. Durch die Speicherung 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 verfügt über einen integrierten Datentyp, GeoPoint , der den Längen- und Breitengrad eines beliebigen Standorts speichern kann. Um Standorte von/zu einem Cloud Firestore-Dokument zuzuordnen, können wir den GeoPoint Typ verwenden:

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

Der entsprechende Typ in Swift ist CLLocationCoordinate2D , und wir können mit der folgenden Operation 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 .

Aufzählungen

Aufzählungen sind wahrscheinlich eine der am meisten unterschätzten Sprachfunktionen in Swift. In ihnen steckt viel mehr, als man auf den ersten Blick sieht. Ein häufiger Anwendungsfall für Aufzählungen besteht darin, die diskreten Zustände von etwas zu modellieren. Beispielsweise könnten wir eine App zum Verwalten von Artikeln schreiben. Um den Status eines Artikels zu verfolgen, möchten wir möglicherweise einen Enum- Status verwenden:

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

Cloud Firestore unterstützt Aufzählungen nicht nativ (d. h. es kann den Wertesatz nicht erzwingen), aber wir können trotzdem die Tatsache nutzen, dass Aufzählungen typisiert werden können, und einen codierbaren Typ auswählen. In diesem Beispiel haben wir String ausgewählt, was bedeutet, dass alle Enumerationswerte beim Speichern in einem Cloud Firestore-Dokument einem/von einem String zugeordnet werden.

Und da Swift benutzerdefinierte Rohwerte unterstützt, können wir sogar anpassen, welche Werte sich auf welchen Aufzählungsfall beziehen. Wenn wir uns beispielsweise dazu entschließen würden, den Status.inReview Fall als „in Überprüfung“ zu speichern, könnten wir die obige Aufzählung einfach wie folgt aktualisieren:

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

Anpassen der Zuordnung

Manchmal stimmen die Attributnamen der Cloud Firestore-Dokumente, die wir zuordnen möchten, nicht mit den Namen der Eigenschaften in unserem Datenmodell in Swift überein. Beispielsweise könnte einer unserer Kollegen ein Python-Entwickler sein und beschlossen, für alle seine Attributnamen „snake_case“ zu wählen.

Keine Sorge: Codable ist für uns da!

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

Betrachten Sie dieses Dokument:

Ein Firestore-Dokument mit dem Attributnamen „snake_cased“.

Um dieses Dokument einer Struktur zuzuordnen, die über eine Namenseigenschaft vom Typ String verfügt, müssen wir der Struktur ProgrammingLanguage eine CodingKeys Enumeration 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 Eigenschaftsnamen unserer Swift-Typen, um die Attributnamen in den Cloud Firestore-Dokumenten zu bestimmen, die wir zuzuordnen versuchen. Solange die Attributnamen übereinstimmen, besteht keine Notwendigkeit, CodingKeys zu unseren codierbaren Typen hinzuzufügen. Sobald wir jedoch CodingKeys für einen bestimmten Typ verwenden, müssen wir alle Eigenschaftsnamen hinzufügen, die wir zuordnen möchten.

Im obigen Codeausschnitt haben wir eine id Eigenschaft definiert, die wir möglicherweise als Bezeichner in einer SwiftUI- List verwenden möchten. Wenn wir es nicht in CodingKeys angeben würden, würde es beim Abrufen von Daten nicht zugeordnet werden und somit zu nil werden. Dies würde dazu führen, dass die List mit dem ersten Dokument gefüllt wird.

Jede Eigenschaft, die nicht als Fall in der entsprechenden CodingKeys Enumeration aufgeführt ist, wird während des Zuordnungsprozesses ignoriert. Dies kann tatsächlich praktisch sein, wenn wir bestimmte Eigenschaften gezielt von der Zuordnung ausschließen möchten.

Wenn wir beispielsweise die Eigenschaft reasonWhyILoveThis von der Zuordnung ausschließen möchten, müssen wir sie lediglich aus der CodingKeys Enumeration 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 möglicherweise ein leeres Attribut zurück in das Cloud Firestore-Dokument schreiben. Swift kennt den Begriff „Optionale“, um das Fehlen eines Werts anzuzeigen, und Cloud Firestore unterstützt auch null . Das Standardverhalten beim Codieren von optionalen Elementen, die einen nil haben, besteht jedoch darin, sie einfach wegzulassen. @ExplicitNull gibt uns eine gewisse Kontrolle darüber, wie Swift-Optionale beim Codieren behandelt werden: Indem wir eine optionale Eigenschaft als @ExplicitNull kennzeichnen, können wir Cloud Firestore anweisen, diese Eigenschaft mit einem Nullwert in das Dokument zu schreiben, wenn es den Wert nil enthält.

Verwendung eines benutzerdefinierten Encoders und Decoders zum Zuordnen von Farben

Als letztes Thema unserer Berichterstattung über Mapping-Daten 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-Apps äußerst 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 Wörterbuch ab, das aus seinen RGB-Komponenten besteht.

Es scheint, dass es eine bessere und einfachere Lösung geben sollte. Warum verwenden wir nicht Webfarben (oder genauer gesagt die CSS-Hex-Farbnotation) – sie sind einfach zu verwenden (im Wesentlichen nur eine Zeichenfolge) und unterstützen sogar Transparenz!

Um eine Swift- Color ihrem Hexadezimalwert zuordnen zu können, müssen wir eine Swift-Erweiterung erstellen, die Codable zu Color 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)
  }

}

Durch die Verwendung decoder.singleValueContainer() können wir einen String in sein Color dekodieren, 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!

Damit können wir den Code für die Zuordnung von Tags aktualisieren, wodurch es einfacher wird, die Tag-Farben direkt zu verarbeiten, 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]
}

Umgang mit Fehlern

In den obigen Codeausschnitten haben wir die Fehlerbehandlung absichtlich auf ein Minimum beschränkt, aber in einer Produktions-App sollten Sie sicherstellen, dass alle Fehler ordnungsgemäß behandelt werden.

Hier ist ein Codeausschnitt, der zeigt, wie Sie mit eventuell auftretenden 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 in Live-Updates

Der vorherige Codeausschnitt zeigt, wie mit Fehlern beim Abrufen eines einzelnen Dokuments umgegangen wird. Neben dem einmaligen Abrufen von Daten unterstützt Cloud Firestore auch die Bereitstellung von Updates für Ihre App, sobald diese erfolgen, mithilfe sogenannter Snapshot-Listener: Wir können einen Snapshot-Listener für eine Sammlung (oder Abfrage) registrieren, und Cloud Firestore ruft unseren Listener immer dann auf, wenn er dort ist ist ein Update.

Hier ist ein Codeausschnitt, der zeigt, wie man einen Snapshot-Listener registriert, Daten mit Codable zuordnet und eventuell auftretende Fehler behandelt. Außerdem wird gezeigt, wie Sie der Sammlung ein neues Dokument hinzufügen. Wie Sie sehen werden, besteht keine Notwendigkeit, das lokale Array, das die zugeordneten Dokumente enthält, selbst zu aktualisieren, da dies vom Code im Snapshot-Listener erledigt 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 Codeausschnitte sind Teil einer Beispielanwendung, die Sie aus diesem GitHub-Repository herunterladen können.

Gehen Sie los und nutzen Sie Codable!

Die Codable API von Swift bietet eine leistungsstarke und flexible Möglichkeit, Daten aus serialisierten Formaten Ihrem Anwendungsdatenmodell zuzuordnen. 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 und konnten uns dabei auf die Implementierung von Codable und Firebase verlassen, um das Mapping für uns durchzuführen.

Für weitere Informationen zu Codable empfehle ich die folgenden Ressourcen:

Obwohl wir unser Bestes getan haben, um einen umfassenden Leitfaden zum Zuordnen von Cloud Firestore-Dokumenten zusammenzustellen, erhebt dieser keinen Anspruch auf Vollständigkeit, und Sie verwenden möglicherweise andere Strategien zum Zuordnen Ihrer Typen. Teilen Sie uns über die Schaltfläche „Feedback senden“ unten mit, welche Strategien Sie zum Zuordnen anderer Arten von Cloud Firestore-Daten oder zum Darstellen von Daten in Swift verwenden.

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