Die in Swift 4 eingeführte Codable API ermöglicht es uns, die Leistungsfähigkeit des Compilers zu nutzen, um die Zuordnung von Daten aus serialisierten Formaten zu Swift-Typen zu vereinfachen.
Sie haben Codable vielleicht schon verwendet, um Daten aus einer Web-API Ihrem Datenmodell der App zuzuordnen (und umgekehrt), aber es ist viel flexibler.
In diesem Leitfaden sehen wir uns an, wie mit Codable Daten aus Cloud Firestore in Swift-Typen und umgekehrt abgebildet werden können.
Wenn Sie ein Dokument aus Cloud Firestore abrufen, erhält Ihre App ein Dictionary mit Schlüssel/Wert-Paaren (oder ein Array von Dictionaries, wenn Sie einen der Vorgänge verwenden, der mehrere Dokumente zurückgibt).
Sie können Wörterbücher in Swift natürlich weiterhin direkt verwenden. Sie bieten eine große Flexibilität, die für Ihren Anwendungsfall genau das Richtige sein könnte. Dieser Ansatz ist jedoch nicht typsicher und es ist leicht, schwer aufzufindende Fehler einzuführen, indem Attributnamen falsch geschrieben oder vergessen wird, das neue Attribut zuzuordnen, das Ihr Team letzte Woche mit der Einführung der neuen Funktion hinzugefügt hat.
In der Vergangenheit haben viele Entwickler diese Einschränkungen umgangen, indem sie eine einfache Mapping-Ebene implementiert haben, mit der sie Dictionaries 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.
Da Cloud Firestore die Codable API von Swift unterstützt, wird dies viel einfacher:
- Sie müssen keinen Zuordnungscode mehr manuell implementieren.
- Es ist ganz einfach, die Zuordnung von Attributen mit unterschiedlichen Namen zu definieren.
- Es bietet integrierte Unterstützung für viele Swift-Typen.
- Und es ist ganz einfach, Unterstützung für die Zuordnung benutzerdefinierter Typen hinzuzufügen.
- Das Beste daran: Bei einfachen Datenmodellen müssen Sie überhaupt keinen Mapping-Code schreiben.
Zuordnungsdaten
In Cloud Firestore werden Daten in Dokumenten gespeichert, in denen Schlüssel Werten zugeordnet sind. Um Daten aus einem einzelnen Dokument abzurufen, können wir DocumentSnapshot.data()
aufrufen. Dadurch wird ein Dictionary zurückgegeben, in dem die Feldnamen einem Any
zugeordnet sind:
func data() -> [String : Any]?
.
Das bedeutet, dass wir mit der Subscript-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 einfach zu implementieren sein, ist aber fehleranfällig und schwer zu warten.
Wie Sie sehen, treffen wir Annahmen über die Datentypen der Dokumentfelder. Diese können richtig sein oder nicht.
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 müssen Sie Ihren Mapping-Code 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 Apple-Dokumentation ist „Codable“ ein Typ, der sich in eine externe Darstellung konvertieren kann und umgekehrt. Tatsächlich ist „Codable“ ein Typalias für die Protokolle „Encodable“ und „Decodable“. Wenn Sie einen Swift-Typ an dieses Protokoll anpassen, generiert 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“ 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)")
}
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 entsprechen direkt den integrierten Swift-Typen. 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:
- 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. - Importieren Sie
FirebaseFirestore
in Ihre Swift-Datei. - Richte deinen Typ an
Codable
aus. - Optional (wenn Sie den Typ in einer
List
-Ansicht verwenden möchten): Fügen Sie Ihrem Typ eineid
-Property hinzu und verwenden Sie@DocumentID
, um Cloud Firestore mitzuteilen, dass diese der Dokument-ID zugeordnet werden soll. Das wird unten genauer erläutert. - Verwenden Sie
documentReference.data(as: )
, um einen Dokumentverweis einem Swift-Typ zuzuordnen. - Mit
documentReference.setData(from: )
können Sie Daten aus Swift-Typen einem Cloud Firestore-Dokument zuordnen. - (Optional, aber dringend empfohlen) Implementieren Sie eine angemessene 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 id
-Property hinzufügen und mit dem @DocumentID
-Property-Wrapper 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äziser formulieren, indem Sie den Typ des Dokuments beim Aufrufen von getDocument(as:)
angeben. Dadurch wird die Zuordnung für Sie ausgefü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 aktualisieren, indem Sie 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, weist Cloud Firestore dem Dokument automatisch eine neue Dokument-ID zu. 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)
}
}
Neben der 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.
Geschachtelte 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? Wir möchten beispielsweise die URLs zum Cover des Buchs in verschiedenen Auflösungen speichern.
Am einfachsten geht das in Cloud Firestore mit einer Karte:
Beim Schreiben des entsprechenden Swift-Structs 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, wie wir im Dokument Cloud Firestore eine Struktur, CoverImages
, für die Abdeckungskarte definiert haben. Indem wir das Attribut „cover“ in BookWithCoverImages
als optional kennzeichnen, können wir berücksichtigen, dass einige Dokumente möglicherweise kein Cover-Attribut enthalten.
Wenn Sie sich fragen, warum es keinen Code-Snippet zum Abrufen oder Aktualisieren von Daten gibt, werden Sie sich freuen zu hören, dass der Code zum Lesen oder Schreiben von/in Cloud Firestore nicht angepasst werden muss. 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 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“:
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
, Int
usw.). Im Folgenden sehen Sie, wie Sie unserem 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:
Um Tags auf diese Weise zu speichern, müssen wir nur eine Tag
-Struktur implementieren, um ein Tag darzustellen und es 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]
}
Kurzer Hinweis zur Zuordnung von Dokument-IDs
Bevor wir weitere Typen zuordnen, sprechen wir kurz über die Zuordnung von Dokument-IDs.
Wir haben den @DocumentID
-Property-Wrapper in einigen der vorherigen Beispiele verwendet, um die Dokument-ID unserer Cloud Firestore-Dokumente der id
-Property 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
in SwiftUI müssen die ElementeIdentifiable
sein, damit sie beim Einfügen nicht hin- und herspringen.
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 für Book
im vorherigen Beispiel in diesem Leitfaden), ist es nicht erforderlich, eine @DocumentID
-Property hinzuzufügen. Verschachtelte Properties 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 Zeitangaben. Da Cloud Firestore „Codable“ unterstützt, ist die Verwendung dieser Datentypen ganz einfach.
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 Datums- und Zeitangaben nicht beenden, ohne über @ServerTimestamp
zu sprechen. Dieser Property-Wrapper ist sehr nützlich, wenn es um Zeitstempel in Ihrer App geht.
In jedem verteilten System ist es wahrscheinlich, dass die Uhren der einzelnen Systeme nicht immer vollständig synchronisiert sind. Das mag nicht viel erscheinen, aber stellen Sie sich die Auswirkungen einer Uhr vor, die in einem Aktienhandelssystem leicht aus dem Takt gerät: Selbst eine Millisekunde Abweichung kann beim Ausführen eines Handels zu einem Unterschied 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 zum Zeitpunkt des Schreibens in die Datenbank. Wenn das Feld beim Aufrufen von addDocument()
oder updateData()
nicht nil
ist, lässt Cloud Firestore den Attributwert unverändert. So lassen sich Felder wie createdAt
und lastUpdatedAt
ganz einfach implementieren.
Geopunkte
Geolokalisierungen sind in unseren Apps allgegenwärtig. Durch die Speicherung werden viele interessante Funktionen möglich. Es kann beispielsweise sinnvoll 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
, in dem der Längen- und Breitengrad eines beliebigen Ortes gespeichert werden kann. Um Orte aus/in einem Cloud Firestore-Dokument zuzuordnen, 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
. Die beiden Typen können mit dem folgenden Vorgang zugeordnet werden:
CLLocationCoordinate2D(latitude: office.location.latitude,
longitude: office.location.longitude)
Weitere Informationen zum Abfragen von Dokumenten nach physischem Standort
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 entwickeln eine App zum Verwalten von Artikeln. Um den Status eines Artikels zu verfolgen, können wir das 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), aber wir können 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 Enum-Werte beim Speichern in einem Cloud Firestore-Dokument in einen String umgewandelt bzw. aus einem 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 beschließen, den Fall Status.inReview
als „in Überprüfung“ zu speichern, 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 Attribute in unserem Datenmodell in Swift überein. Ein Kollege ist beispielsweise Python-Entwickler und hat sich entschieden, für alle Attributnamen snake_case zu verwenden.
Keine Sorge: Codable hat uns geholfen!
Für solche Fälle 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.
Stellen Sie sich folgendes Dokument vor:
Um dieses Dokument einem struct mit einer name-Eigenschaft vom Typ String
zuzuordnen, müssen wir dem ProgrammingLanguage
-struct 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 Eigenschaftsnamen unserer Swift-Typen, um die Attributnamen in den Cloud Firestore-Dokumenten zu bestimmen, die wir zuordnen möchten. Solange die Attributnamen übereinstimmen, muss CodingKeys
nicht zu unseren codierbaren Typen hinzugefügt werden. Wenn wir CodingKeys
für einen bestimmten Typ verwenden, müssen wir jedoch alle Property-Namen hinzufügen, die wir zuordnen möchten.
Im obigen Code-Snippet haben wir eine id
-Property definiert, die wir möglicherweise als Kennung in einer SwiftUI-Ansicht List
verwenden möchten. Wenn wir sie nicht in CodingKeys
angegeben hätten, würde sie beim Abrufen von Daten nicht zugeordnet werden und somit nil
werden.
In der Ansicht List
würde dann das erste Dokument angezeigt.
Alle Properties, die nicht als Fall in der entsprechenden CodingKeys
-Enumeration aufgeführt sind, werden während der Zuordnung ignoriert. Das kann praktisch sein, wenn wir bestimmte Eigenschaften nicht zuordnen möchten.
Wenn wir beispielsweise die Eigenschaft reasonWhyILoveThis
aus 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 in das Cloud Firestore-Dokument zurückschreiben. In Swift gibt es das Konzept von Optionals, um das Fehlen eines Werts anzugeben. Cloud Firestore unterstützt auch null
-Werte.
Das Standardverhalten für die Codierung optionaler Elemente mit dem Wert nil
besteht jedoch darin, sie einfach wegzulassen. @ExplicitNull
bietet uns einige Möglichkeiten, die Verarbeitung von Swift-Optionals bei der Codierung zu steuern: 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 einen Wert von nil
enthält.
Benutzerdefinierten Encoder und Decoder zum Zuordnen von Farben verwenden
Als letztes Thema in unserem Überblick über das Zuordnen von Daten mit „Codable“ möchten wir benutzerdefinierte Encoder und Decoder vorstellen. In diesem Abschnitt wird kein nativer Cloud Firestore-Datentyp behandelt, aber benutzerdefinierte Encoder und Decoder sind in Ihren Cloud Firestore-Apps 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 scheint eine bessere, einfachere Lösung zu geben. Warum verwenden wir keine Webfarben (oder genauer gesagt die CSS-Hexadezimalnotation für Farben)? Sie sind einfach zu verwenden (im Grunde nur ein String) und unterstützen sogar Transparenz.
Damit wir eine Swift-Color
ihrem 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 einen String
in das entsprechende Color
-Format decodieren, ohne die RGBA-Komponenten verschachteln zu müssen. Außerdem können Sie diese Werte in der Weboberfläche Ihrer App verwenden, ohne sie vorher konvertieren zu müssen.
So 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 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 oben genannten 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.
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)"
}
}
}
}
}
Fehler bei Live-Updates beheben
Das vorherige Code-Snippet zeigt, wie Fehler beim Abrufen eines einzelnen Dokuments behandelt werden. Neben dem einmaligen Abrufen von Daten unterstützt Cloud Firestore auch die Bereitstellung von Updates für Ihre App, sobald sie eintreten. Dazu werden sogenannte Snapshot-Listener verwendet: Wir können einen Snapshot-Listener für eine Sammlung (oder Abfrage) registrieren und Cloud Firestore ruft unseren Listener auf, sobald ein Update erfolgt.
Das folgende Code-Snippet zeigt, wie Sie einen Snapshot-Listener registrieren, Kartendaten mit „Codable“ zuordnen und alle Fehler behandeln, die auftreten können. Außerdem wird gezeigt, wie der Sammlung ein neues Dokument hinzugefügt wird. Wie Sie sehen, müssen wir das lokale Array mit den zugeordneten Dokumenten nicht selbst aktualisieren, da dies durch den Code im Snapshot-Listener erfolgt.
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 Apps ist, die Cloud Firestore als Datenspeicher verwenden.
Wir begannen mit einem einfachen Beispiel mit einfachen Datentypen und erhöhten die Komplexität des Datenmodells schrittweise. Dabei konnten wir uns immer auf Codable und die Implementierung von Firebase verlassen, um die Zuordnung für uns durchzuführen.
Weitere Informationen zu „Codable“ finden Sie in den folgenden Ressourcen:
- John Sundell hat einen guten Artikel zu den Grundlagen von „Codable“ geschrieben.
- Wenn Sie lieber Bücher lesen, sollten Sie sich Mattts Flight School Guide to Swift Codable ansehen.
- Und schließlich hat Donny Wals eine ganze Reihe über „Codable“.
Wir haben unser Bestes getan, einen umfassenden Leitfaden für die Zuordnung von Cloud Firestore-Dokumenten zu erstellen. Dieser ist jedoch nicht vollständig und Sie verwenden möglicherweise andere Strategien für die Zuordnung Ihrer Typen. Über die Schaltfläche Feedback geben 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 wirklich keinen Grund, die Codable-Unterstützung von Cloud Firestore nicht zu nutzen.