Analysieren und Arbeiten mit Codable in Swift 4



Das JSON-Format erfreut sich großer Beliebtheit und wird normalerweise für die Datenübertragung und die Ausführung von Abfragen in Client-Server-Anwendungen verwendet. Für die JSON-Analyse sind Codierungs- / Decodierungswerkzeuge dieses Formats erforderlich, die von Apple kürzlich aktualisiert wurden. In diesem Artikel werden wir uns mit JSON-Analysemethoden unter Verwendung des Decodable- Protokolls befassen , das neue Codable- Protokoll mit dem Vorgänger- NSCoding vergleichen , die Vor- und Nachteile bewerten, alles anhand spezifischer Beispiele analysieren und auch einige Funktionen berücksichtigen, die bei der Implementierung der Protokolle auftreten.


Was ist codierbar?

Auf der WWDC2017 stellte Apple zusammen mit der neuen Version von Swift 4 neue Tools zur Datencodierung / -decodierung vor, die von den folgenden drei Protokollen implementiert werden:

- Codierbar
- Codierbar
- Dekodierbar

In den meisten Fällen werden diese Protokolle für die Arbeit mit JSON verwendet. Darüber hinaus werden sie jedoch auch zum Speichern von Daten auf der Festplatte, zum Übertragen über das Netzwerk usw. verwendet. Encodable wird verwendet, um Swift-Datenstrukturen in JSON-Objekte zu konvertieren, während Decodable im Gegensatz dazu hilft, JSON-Objekte in Swift-Datenmodelle zu konvertieren. Das codierbare Protokoll kombiniert die beiden vorherigen und ist ihre Typealien:

typealias Codable = Encodable & Decodable 


Um diesen Protokollen zu entsprechen, müssen Datentypen die folgenden Methoden implementieren:

Codierbar
encode (to :) - codiert das Datenmodell in den angegebenen Encodertyp

Dekodierbar
init (from :) - initialisiert das Datenmodell vom bereitgestellten Decoder

Codierbar
codieren (zu :)
init (von :)

Ein einfacher Anwendungsfall

Betrachten Sie nun ein einfaches Beispiel für die Verwendung von Codable , da es sowohl Encodable als auch Decodable implementiert . In diesem Beispiel können Sie sofort alle Protokollfunktionen sehen. Angenommen, wir haben die einfachste JSON-Datenstruktur:

 { "title": "Nike shoes", "price": 10.5, "quantity": 1 } 


Das Datenmodell für die Arbeit mit diesem JSON sieht folgendermaßen aus:
 struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(price, forKey: .price) try container.encode(quantity, forKey: .quantity) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) price = try container.decode(Double.self, forKey: .price) quantity = try container.decode(Int.self, forKey: .quantity) } } 


Beide notwendigen Methoden sind implementiert, die Aufzählung wird auch beschrieben, um die Liste der Codierungs- / Decodierungsfelder zu bestimmen. Tatsächlich kann das Schreiben erheblich vereinfacht werden, da Codable die automatische Generierung der Methoden encode (to :) und init (from :) sowie die erforderliche Aufzählung unterstützt. Das heißt, in diesem Fall können Sie die Struktur wie folgt schreiben:

 struct Product: Codable { var title:String var price:Double var quantity:Int } 


Extrem einfach und minimalistisch. Vergessen Sie nur nicht, dass eine so präzise Aufnahme nicht funktioniert, wenn:

- Die Struktur Ihres Datenmodells unterscheidet sich von der, die Sie codieren / decodieren möchten

- Möglicherweise müssen Sie neben den Eigenschaften Ihres Datenmodells weitere Eigenschaften codieren / decodieren

- Einige Eigenschaften Ihres Datenmodells unterstützen das codierbare Protokoll möglicherweise nicht. In diesem Fall müssen Sie sie vom / zum codierbaren Protokoll konvertieren

- falls die Variablennamen im Datenmodell und die Feldnamen im Container nicht mit Ihnen übereinstimmen

Da wir bereits die einfachste Definition eines Datenmodells in Betracht gezogen haben, lohnt es sich, ein kleines Beispiel für seine praktische Verwendung zu geben:

In einer Zeile können Sie also die Serverantwort im JSON-Format analysieren:
 let product: Product = try! JSONDecoder().decode(Product.self, for: data) 


Im Gegensatz dazu erstellt der folgende Code ein JSON-Objekt aus dem Datenmodell:
 let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject) 


Alles ist sehr bequem und schnell. Nachdem Sie die Datenmodelle korrekt beschrieben und codierbar gemacht haben , können Sie Daten buchstäblich in einer Zeile codieren / decodieren. Wir haben jedoch das einfachste Datenmodell betrachtet, das eine kleine Anzahl von Feldern eines einfachen Typs enthält. Betrachten Sie die möglichen Probleme:

Nicht alle Felder im Datenmodell sind codierbar.

Damit Ihr Datenmodell das codierbare Protokoll implementieren kann , müssen alle Felder des Modells dieses Protokoll unterstützen. Standardmäßig unterstützt das Codable- Protokoll die folgenden Datentypen: String, Int, Double, Data, URL . Codable unterstützt auch Array, Dictionary, Optional , jedoch nur, wenn sie codierbare Typen enthalten. Wenn einige Eigenschaften des Datenmodells nicht Codable entsprechen, müssen sie dorthin gebracht werden.

 struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(to encoder: Encoder) throws { . . . } } 


Wenn wir in unserem codierbaren Datenmodell einen benutzerdefinierten Typ verwenden, z. B. PetType , und ihn codieren / decodieren möchten, muss er auch dessen Init und Codierung implementieren.

Das Datenmodell stimmt nicht mit JSON-Feldern überein

Wenn in Ihrem Datenmodell 3 Felder definiert sind und 5 Felder im JSON-Objekt zu Ihnen kommen, von denen 2 zusätzlich zu 3 sind, ändert sich beim Parsen nichts. Sie erhalten nur Ihre 3 Felder aus diesen 5. Wenn das Gegenteil passiert Situation und im JSON-Objekt gibt es mindestens ein Feld des Datenmodells, ein Laufzeitfehler wird auftreten.
Wenn einige Felder in einem JSON-Objekt optional sind und regelmäßig fehlen, müssen Sie sie in diesem Fall optional machen:

 class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var brand: Brand? } 


Verwendung komplexerer JSON-Strukturen

Oft ist die Serverantwort ein Array von Entitäten, dh Sie fordern beispielsweise eine Liste von Geschäften an und erhalten eine Antwort in der Form:

 { "items": [ { "id": 1, "title": " ", "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw", "created_at": 1497868174, "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg" }, { "id": 2, "title": "  2", "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8", "created_at": 1525952040, "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg" } ] } 

In diesem Fall können Sie es einfach als Array von Shop- Entitäten schreiben und dekodieren.

 struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] } 


In diesem Beispiel funktioniert die automatische Funktion init. Wenn Sie jedoch selbst eine Dekodierung schreiben möchten, müssen Sie den dekodierten Typ als Array angeben:

 self.items = try container.decode([Shop].self, forKey: .items) 


Die Shop- Struktur sollte auch das Decodable- Protokoll implementieren .

 struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: String, CodingKey { case id case title case address case shortAddress = "short_address" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try? container.decode(Int.self, forKey: .id) self.title = try? container.decode(String.self, forKey: .title) self.address = try? container.decode(String.self, forKey: .address) self.shortAddress = try? container.decode(String.self, forKey: .shortAddress) self.createdAt = try? container.decode(Date.self, forKey: .createdAt) } } 


Das Parsen dieses Arrays von Elementen sieht folgendermaßen aus:

 let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data) 


Auf diese Weise können Sie problemlos mit Arrays von Datenmodellen arbeiten und diese in anderen Modellen verwenden.

Datumsformat

In diesem Beispiel gibt es noch eine Nuance. Hier haben wir zuerst die Verwendung des Datums- Typs festgestellt . Bei Verwendung dieses Typs können Probleme mit der Datumscodierung auftreten, und normalerweise stimmt dieses Problem mit dem Backend überein. Das Standardformat ist .deferToDate :

 struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo) 


myDate sieht folgendermaßen aus:
 { "date" : 519751611.12542897 } 


Wenn wir beispielsweise das .iso8601- Format verwenden müssen, können wir das Format mithilfe der dateEncodingStrategy- Eigenschaft einfach ändern:

 encoder.dateEncodingStrategy = .iso8601 


Jetzt sieht das Datum folgendermaßen aus:

 { "date" : "2017-06-21T15:29:32Z" } 

Sie können auch ein benutzerdefiniertes Datumsformat verwenden oder sogar Ihren eigenen Datumsdecoder mit den folgenden Formatierungsoptionen schreiben:

.formatiert (DateFormatter) - ein eigenes Datumsdecoderformat
.custom ((Date, Encoder) throw -> Void) - Erstellen Sie Ihr eigenes Datumsdecodierungsformat vollständig

Verschachtelte Objekte analysieren

Wir haben bereits untersucht, wie Sie Datenmodelle in anderen Modellen verwenden können. Manchmal ist es jedoch erforderlich, die in anderen Feldern enthaltenen JSON-Felder zu analysieren, ohne ein separates Datenmodell zu verwenden. Das Problem wird klarer, wenn wir es anhand eines Beispiels betrachten. Wir haben den folgenden JSON:

 { "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } } 


Wir müssen die Felder "Durchschnitt" und "Stimmenzahl" analysieren . Dies kann auf zwei Arten gelöst werden: Erstellen Sie entweder ein Bewertungsdatenmodell mit zwei Feldern und speichern Sie die Daten darin, oder Sie können nestedContainer verwenden . Wir haben bereits den ersten Fall besprochen, und die Verwendung des zweiten wird folgendermaßen aussehen:

 class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.art = try? container.decode(String.self, forKey: .art) self.title = try? container.decode(String.self, forKey: .title) // Nested ratings let ratingsContainer = try container.nestedContainer(keyedBy: RatingsCodingKeys.self, forKey: .ratings) self.votesCount = try ratingsContainer.decode(Int.self, forKey: .votesCount) self.averageRating = try ratingsContainer.decode(Double.self, forKey: .averageRating) } } 


Das heißt, dieses Problem wird gelöst, indem ein weiterer zusätzlicher Container mit nestedContainter und dessen weiterer Analyse erstellt wird. Diese Option ist praktisch, wenn die Anzahl der verschachtelten Felder nicht so groß ist. Andernfalls ist es besser, ein zusätzliches Datenmodell zu verwenden.

Nicht übereinstimmende JSON-Feldnamen und Datenmodelleigenschaften

Wenn Sie darauf achten, wie die Aufzählungen in unseren Datenmodellen definiert sind, können Sie sehen, dass den Elementen der Aufzählungen manchmal eine Zeichenfolge zugewiesen wird, die den Standardwert ändert, zum Beispiel:

 enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } 


Dies geschieht, um die Namen der Modellvariablen und JSON-Felder korrekt abzugleichen. Dies ist normalerweise für Felder erforderlich, deren Name aus mehreren Wörtern besteht, und in JSON werden sie durch Unterstriche getrennt. Im Prinzip ist eine solche Neudefinition der Aufzählung am beliebtesten und sieht einfach aus, aber selbst dann hat Apple eine elegantere Lösung gefunden. Dieses Problem kann mit keyDecodingStrategy in einer Zeile gelöst werden . Diese Funktion wurde in Swift 4.1 angezeigt

Angenommen, Sie haben einen JSON der Form:

 let jsonString = """ [ { "name": "MacBook Pro", "screen_size": 15, "cpu_count": 4 }, { "name": "iMac Pro", "screen_size": 27, "cpu_count": 18 } ] """ let jsonData = Data(jsonString.utf8) 


Erstellen wir ein Datenmodell dafür:

 struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int } 

Variablen im Modell werden gemäß der Vereinbarung aufgezeichnet, beginnen mit einem Kleinbuchstaben und jedes Wort beginnt mit einem Großbuchstaben (dem sogenannten camelCase ). In JSON werden Felder jedoch mit Unterstrichen geschrieben (dem sogenannten snake_case ). Damit das Parsen erfolgreich ist, müssen wir entweder eine Aufzählung im Datenmodell definieren, in der die Entsprechung der Namen der JSON-Felder mit den Namen der Variablen hergestellt wird, oder es wird ein Laufzeitfehler angezeigt. Jetzt ist es jedoch möglich, keyDecodingStrategy einfach zu definieren

 let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) } 


Für die Codierungsfunktion können Sie dementsprechend die inverse Transformation verwenden:

 encoder.keyEncodingStrategy = .convertToSnakeCase 


Es ist auch möglich, keyDecodingStrategy mithilfe des folgenden Abschlusses anzupassen:

 let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! } 


Dieser Eintrag ermöglicht beispielsweise die Verwendung des Trennzeichens "-" für JSON. Ein Beispiel für den verwendeten JSON:

 { "first-Name": "Taylor", "last-Name": "Swift", "age": 28 } 

Daher kann eine zusätzliche Definition einer Aufzählung häufig vermieden werden.

Fehlerbehandlung

Wenn Sie JSON analysieren und Daten von einem Format in ein anderes konvertieren, sind Fehler unvermeidlich. Schauen wir uns also die Optionen für die Behandlung verschiedener Fehlertypen an. Beim Decodieren sind folgende Fehlertypen möglich:

  • DecodingError.dataCorrupted (DecodingError.Context) - Daten sind beschädigt. Normalerweise bedeutet dies, dass die Daten, die Sie dekodieren möchten, nicht mit dem erwarteten Format übereinstimmen. Beispielsweise haben Sie anstelle des erwarteten JSON ein völlig anderes Format erhalten.
  • DecodingError.keyNotFound (CodingKey, DecodingError.Context) - Das angeforderte Feld wurde nicht gefunden. Bedeutet, dass das Feld, das Sie erwartet haben, fehlt
  • DecodingError.typeMismatch (Any.Type, DecodingError.Context) - Typ Mismatch. Wenn der Datentyp im Modell nicht mit dem Typ des empfangenen Felds übereinstimmt
  • DecodingError.valueNotFound (Any.Type, DecodingError.Context) - fehlender Wert für ein bestimmtes Feld. Das Feld, das Sie im Datenmodell definiert haben, konnte nicht initialisiert werden. Wahrscheinlich ist dieses Feld in den empfangenen Daten gleich Null. Dieser Fehler tritt nur bei nicht optionalen Feldern auf. Wenn das Feld keinen Wert haben muss, vergessen Sie nicht, es optional zu machen.


Beim Codieren von Daten ist ein Fehler möglich:

EncodingError.invalidValue (Any.Type, DecodingError.Context) - Das Datenmodell konnte nicht in ein bestimmtes Format konvertiert werden

Ein Beispiel für die Fehlerbehandlung beim Parsen von JSON:

  do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let error{ print(error) } 


Die Fehlerverarbeitung ist natürlich besser, um eine separate Funktion einzurichten, aber hier wird aus Gründen der Klarheit die Fehleranalyse zusammen mit dem Parsen durchgeführt. Die Fehlerausgabe, wenn für das Feld "Produkt" kein Wert vorhanden ist, sieht beispielsweise folgendermaßen aus:

Bild

Vergleich von codierbarer und NSCoding

Natürlich ist das codierbare Protokoll ein großer Fortschritt beim Codieren / Decodieren von Daten, aber das NSCoding-Protokoll existierte zuvor. Versuchen wir, sie zu vergleichen und herauszufinden, welche Vorteile Codable hat:

  • Bei Verwendung des NSCoding- Protokolls muss das Objekt eine Unterklasse von NSObject sein , was automatisch impliziert, dass unser Datenmodell eine Klasse sein sollte. In Codable ist keine Vererbung erforderlich. Das Datenmodell kann sowohl Klasse als auch Struktur und Aufzählung sein .
  • Wenn Sie separate Codierungs- und Decodierungsfunktionen benötigen, z. B. beim Parsen von über die API empfangenen JSON-Daten, können Sie nur ein decodierbares Protokoll verwenden. Das heißt, es besteht keine Notwendigkeit, die manchmal unnötigen Init- oder Codierungsmethoden zu implementieren.
  • Codable kann automatisch die erforderlichen Init- und Codierungsmethoden sowie die optionale CodingKeys- Aufzählung generieren . Dies funktioniert natürlich nur, wenn Sie einfache Felder in der Datenstruktur haben, andernfalls ist eine zusätzliche Anpassung erforderlich. In den meisten Fällen, insbesondere für grundlegende Datenstrukturen, können Sie die automatische Generierung verwenden, insbesondere wenn Sie keyDecodingStrategy neu definieren . Dies ist praktisch und reduziert unnötigen Code.


Mit den Protokollen Codable, Decodable und Encodable konnten wir einen weiteren Schritt zur Bequemlichkeit der Datenkonvertierung unternehmen. Neue, flexiblere Parsing-Tools wurden angezeigt, die Codemenge wurde reduziert und ein Teil der Konvertierungsprozesse wurde automatisiert. Protokolle werden nativ in Swift 4 implementiert und ermöglichen es, die Verwendung von Bibliotheken von Drittanbietern wie SwiftyJSON zu reduzieren und gleichzeitig die Benutzerfreundlichkeit zu gewährleisten . Protokolle ermöglichen es auch, die Codestruktur ordnungsgemäß zu organisieren, indem Datenmodelle und Methoden für die Arbeit mit ihnen in separate Module unterteilt werden.

Source: https://habr.com/ru/post/de414221/


All Articles