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 Sie ein Dokument von Cloud Firestore abrufen, 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 auch hier auf der manuellen Angabe der Zuordnung zwischen Cloud Firestore-Dokumenten und den entsprechenden Typen des Datenmodells Ihrer Anwendung.
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
Cloud Firestore speichert Daten in Dokumenten, die Schlüssel/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 tiefgestellte Syntax 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 richtig oder falsch.
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ührt. Außerdem muss der Zuordnungscode jedes Mal aktualisiert werden, wenn ein neues Feld hinzugefügt wird, was ziemlich umständlich ist.
Außerdem nutzen wir nicht das starke Typsystem von Swift, das für jedes Attribut von Book
den genauen Typ 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“. Durch die Konformität eines Swift-Typs mit diesem Protokoll 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.
Jetzt 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)")
}
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)
Zuordnung zu und aus einfachen Typen in Cloud Firestore Dokumenten
mithilfe von Codable
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:
- Prüfen Sie, ob Sie Ihrem Projekt das Framework
FirebaseFirestore
hinzugefügt haben. Sie können dazu den Swift Package Manager oder CocoaPods verwenden. - Importieren Sie
FirebaseFirestore
in Ihre Swift-Datei. - Der Typ muss
Codable
entsprechen. - Optional, wenn Sie den Typ in einer
List
-Ansicht verwenden möchten: Fügen Sie dem Typ eineid
-Property hinzu und verwenden Sie@DocumentID
, um Cloud Firestore anzuweisen, diese der Dokument-ID zuzuordnen. Dazu kommen wir gleich. - Verwenden Sie
documentReference.data(as: )
, um eine Dokumentreferenz einem Swift-Typ zuzuordnen. - Mit
documentReference.setData(from: )
kannst du Daten von Swift-Typen einem Cloud Firestore-Dokument zuordnen. - (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 dies noch präziser schreiben, indem Sie den Typ des Dokuments beim Aufrufen von getDocument(as:)
angeben. Dadurch wird die Zuordnung für Sie ausgeführt und es wird ein Result
-Typ mit dem zugeordneten Dokument zurückgegeben. Bei einem Dekodierungsfehler 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:
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.
Sie fragen sich vielleicht, warum es kein Code-Snippet zum Abrufen oder Aktualisieren von Daten gibt. Der Code zum Lesen oder Schreiben von/nach Cloud Firestore muss nicht angepasst werden. 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“:
In Cloud Firestore kann dies mithilfe eines Arrays von Werten modelliert werden. Dies wird für alle codierbaren Typen unterstützt, z. B. String
und Int
. Das folgende Beispiel zeigt, wie Sie unserem Book
-Modell ein Array von Genres hinzufügen können:
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:
Um Tags auf diese Weise zu speichern, müssen wir nur eine Tag
-Struktur implementieren, die ein Tag darstellt und codierbar ist:
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]
}
Eine kurze Anmerkung zur 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. Dies ist aus mehreren 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 ElementeIdentifiable
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 können sie ganz einfach verwendet werden.
Sehen wir uns dieses Dokument an, das die Mutter aller Programmiersprachen darstellt, Ada, die 1843 erfunden wurde:
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 mit @ServerTimestamp
gekennzeichnete Attribute: Wenn das Attribut beim Speichern den Wert nil
hat (z. B. mit addDocument()
), füllt Cloud Firestore das Feld mit dem aktuellen Serverzeitstempel beim Schreiben in die Datenbank aus. Wenn das Feld beim Aufrufen von addDocument()
oder updateData()
nicht nil
ist, bleibt der Attributwert bei Cloud Firestore unverändert. Auf diese Weise lassen sich Felder wie createdAt
und lastUpdatedAt
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 Ortes 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
. Mit dem folgenden Vorgang können wir eine Zuordnung zwischen diesen beiden Typen erstellen:
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-Liste Status
verwenden:
enum Status: String, Codable {
case draft
case inReview
case approved
case published
}
Cloud Firestore unterstützt Aufzählungen nicht nativ, d. h., er kann den Satz von Werten nicht erzwingen. Wir können aber trotzdem 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 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 welche 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 kümmert sich um alles.
In solchen Fällen können wir CodingKeys
nutzen. Dies ist ein Enum, das wir einem codierbaren Typ hinzufügen können, um anzugeben, wie bestimmte Attribute zugeordnet werden.
Betrachten Sie dieses Dokument:
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 Sie den codable-Typen CodingKeys
nicht 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 Sie beispielsweise das Attribut reasonWhyILoveThis
von der Zuordnung ausschließen möchten, müssen wir es nur aus der 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. Swift hat das Konzept von optionalen Werten, um auf das Fehlen eines Werts hinzuweisen, und 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.
Verwendung eines benutzerdefinierten Encoders und Decoders für die Zuordnung von Farben
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. Dies gilt 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 die CSS-Hexadezimal-Farbnotation)? Sie sind einfach zu verwenden (im Wesentlichen 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-UI Ihrer App verwenden, ohne sie vorher 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 ordnungsgemäß behandeln.
Hier ist ein Code-Snippet, das zeigt, wie Sie mit möglichen Fehlersituationen umgehen:
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 dem Datenmodell Ihrer Anwendung zuzuordnen. In dieser Anleitung haben Sie gesehen, wie einfach die Verwendung in Anwendungen ist, deren Datenspeicher Cloud Firestore ist.
Ausgehend von einem einfachen Beispiel mit einfachen Datentypen haben wir die Komplexität des Datenmodells schrittweise erhöht. Gleichzeitig können wir uns auf die Implementierung von Codable und der Firebase-Implementierung verlassen, um die Zuordnung für uns vorzunehmen.
Weitere Informationen zu Codable findest du in den folgenden Ressourcen:
- John Sundell hat einen guten Artikel zu den Grundlagen von Codable.
- Wenn Sie lieber Bücher lesen, empfehlen wir Ihnen Mattt's Flight School Guide to Swift Codable.
- Und schließlich hat Donny Wals eine ganze Reihe zu Codable.
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.