Codable cheat sheet

Codable was een van de hoeksteenfuncties van Swift 4.0, en bracht een ongelooflijk soepele conversie tussen Swift-datatypen en JSON met zich mee. Vervolgens werd het nog beter in Swift 4.1 dankzij nieuwe functionaliteit die werd toegevoegd, en ik verwacht nog grotere dingen in de toekomst.

In dit artikel wil ik snelle codevoorbeelden geven om je te helpen veelvoorkomende vragen te beantwoorden en veelvoorkomende problemen op te lossen, allemaal met behulp van Codable.

Hacking with Swift is gesponsord door RevenueCat

SPONSORED Het bouwen en onderhouden van in-app abonnementsinfrastructuur is moeilijk. Gelukkig is er een betere manier. Met RevenueCat kunt u abonnementen voor uw app implementeren in uren, niet maanden, zodat u zich kunt bezighouden met het bouwen van uw app.

Probeer het gratis

Sponsor Hacking with Swift en bereik ’s werelds grootste Swift-gemeenschap!

Encoderen en decoderen van JSON

Laten we beginnen met de basis: het omzetten van wat JSON in Swift structs.

Eerst, hier is wat JSON om mee te werken:

let json = """"""let data = Data(json.utf8)

De laatste regel converteert het naar een Data object omdat dat is waar Codable decoders mee werken.

Volgende moeten we een Swift struct definiëren dat onze afgewerkte gegevens zal bevatten:

struct User: Codable { var name: String var age: Int}

Nu kunnen we doorgaan en de decodering uitvoeren:

let decoder = JSONDecoder()do { let decoded = try decoder.decode(.self, from: data) print(decoded.name)} catch { print("Failed to decode JSON")}

Dat zal “Paul” afdrukken, dat is de naam van de eerste gebruiker in de JSON.

Het omzetten van geval

Een veel voorkomend probleem met JSON is dat het een andere opmaak gebruikt voor zijn sleutelnamen dan we in Swift willen gebruiken. Bijvoorbeeld, je zou “first_name” in je JSON kunnen krijgen en dat moeten converteren naar een firstName property.

Nou, een voor de hand liggende oplossing hier is gewoon om ofwel de JSON of je Swift types te veranderen zodat ze dezelfde naamgevingsconventie gebruiken, maar dat gaan we hier niet doen. In plaats daarvan ga ik ervan uit dat je code hebt zoals deze:

let json = """"""let data = Data(json.utf8)struct User: Codable { var firstName: String var lastName: String}

Om dit te laten werken hoeven we maar één eigenschap in onze JSON-decoder te veranderen:

let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCase

Die instrueert Swift om slangenhalsnamen (names_written_like_this) te mappen naar kameelhalsnamen (namesWrittenLikeThis).

Het toewijzen van verschillende sleutelnamen

Als u JSON-sleutels hebt die totaal verschillend zijn van uw Swift-eigenschappen, kunt u ze toewijzen met behulp van een CodingKeys enum.

Kijk eens naar deze JSON:

let json = """"""

Die sleutelnamen zijn niet geweldig, en eigenlijk zouden we die gegevens willen omzetten in een struct zoals deze:

struct User: Codable { var firstName: String var lastName: String var age: Int}

Om dat te laten gebeuren moeten we een CodingKeys enum declareren: een mapping die Codable kan gebruiken om JSON namen om te zetten in eigenschappen voor onze struct. Dit is een gewone enum die strings gebruikt voor zijn ruwe waarden, zodat we zowel onze eigenschap naam (de enum case) en de JSON naam (de enum waarde) op hetzelfde moment kunnen specificeren. Het moet ook voldoen aan het CodingKey protocol, waardoor dit werkt met het Codable protocol.

Dus, voeg deze enum toe aan de struct:

enum CodingKeys: String, CodingKey { case firstName = "user_first_name" case lastName = "user_last_name" case age}

Dat zal nu in staat zijn om de JSON te decoderen zoals gepland.

Note: De enum heet CodingKeys en het protocol heet CodingKey.

Werken met ISO-8601 data

Er zijn vele manieren om met data op het internet te werken, maar ISO-8601 is de meest voorkomende. Het codeert de volledige datum in YYYY-MM-DD formaat, dan de letter “T” om het begin van tijd informatie aan te geven, dan de tijd in HH:MM:SS formaat, en tenslotte een tijdzone. De tijdzone “Z”, kort voor “Zulu time” wordt vaak gebruikt om UTC aan te duiden.

Codable is in staat om ISO-8601 te verwerken met een ingebouwde datum converter. Dus, gegeven deze JSON:

let json = """"""

We kunnen het decoderen als volgt:

struct Baby: Codable { var firstName: String var timeOfBirth: Date}let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCasedecoder.dateDecodingStrategy = .iso8601

Dat maakt ISO-8601 datum parsing, die converteert van 1999-04-03T17:30:31Z naar een Date instantie, terwijl ook de slang case naar camel case conversie.

Werken met andere gemeenschappelijke datums

Swift wordt geleverd met ingebouwde ondersteuning voor drie andere belangrijke datum formaten. U gebruikt ze net zoals u ISO-8601 datums gebruikt zoals hierboven getoond, dus ik zal ze kort bespreken:

  • Het .deferredToDate formaat is Apple’s eigen datum formaat, en het houdt het aantal seconden en milliseconden bij sinds 1 januari 2001. Dit is niet echt bruikbaar buiten Apple’s platforms.
  • Het .millisecondsSince1970-formaat geeft het aantal seconden en milliseconden aan sinds 1 januari 1970. Dit komt online vrij veel voor.
  • Het .secondsSince1970-formaat geeft het aantal hele seconden aan sinds 1 januari 1970. Dit komt online zeer veel voor, en is de tweede na ISO-8601.

Werken met aangepaste datums

Als uw datumnotatie niet overeenkomt met een van de ingebouwde opties, hoeft u niet te wanhopen: Codable kan aangepaste datums parsen op basis van een datumformatter die u maakt.

Deze JSON houdt bijvoorbeeld bij op welke dag een student is afgestudeerd aan de universiteit:

let json = """"""

Dat gebruikt het datumformaat DD-MM-YYYY, dat niet een van de ingebouwde opties van Swift is. Gelukkig kan je een voorgeconfigureerde DateFormatter instantie voorzien als een datum decodeerstrategie, zoals deze:

let formatter = DateFormatter()formatter.dateFormat = "dd-MM-yyyy"let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCasedecoder.dateDecodingStrategy = .formatted(formatter)

Werken met vreemde datums

Soms krijg je datums die zo vreemd zijn dat zelfs DateFormatter er niet mee overweg kan. U zou bijvoorbeeld JSON kunnen krijgen die datums opslaat met het aantal dagen dat verstreken is sinds 1 januari 1970:

let json = """"""

Om dat te laten werken moeten we een aangepaste decoder voor de datum schrijven. Al het andere zal nog steeds worden afgehandeld door Codable – we zijn gewoon het verstrekken van een aangepaste sluiting die zal verwerken de datum deel.

U zou kunnen proberen wat hacky wiskunde hier, zoals het vermenigvuldigen van de dag tellen met 86400 (het aantal seconden in een dag), dan met behulp van de addTimeInterval() methode van Date. Dat houdt echter geen rekening met zomertijd en andere datumproblemen, dus een betere oplossing is om DateComponents en Calendar als volgt te gebruiken:

let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCasedecoder.dateDecodingStrategy = .custom { decoder in // pull out the number of days from Codable let container = try decoder.singleValueContainer() let numberOfDays = try container.decode(Int.self) // create a start date of Jan 1st 1970, then a DateComponents instance for our JSON days let startDate = Date(timeIntervalSince1970: 0) var components = DateComponents() components.day = numberOfDays // create a Calendar and use it to measure the difference between the two let calendar = Calendar(identifier: .gregorian) return calendar.date(byAdding: components, to: startDate) ?? Date()}

Waarschuwing: Als je veel datums moet parsen, onthoud dan dat deze sluiting voor elke datum zal worden uitgevoerd – maak het snel!

Parsing hiërarchische gegevens op de gemakkelijke manier

Elke niet-triviale JSON heeft waarschijnlijk hiërarchische gegevens – een verzameling gegevens genesteld in een andere. Bijvoorbeeld:

let json = """"""

Codable kan hier prima mee overweg, zolang je de relaties maar duidelijk beschrijft.

Ik vind de makkelijkste manier om dit te doen het gebruik van geneste structs, zoals deze:

struct User: Codable { struct Name: Codable { var firstName: String var lastName: String } var name: Name var age: Int}

Het nadeel is dat als je de voornaam van een gebruiker wilt lezen, je user.name.firstName moet gebruiken, maar het eigenlijke parseerwerk is in ieder geval triviaal – onze bestaande code werkt al!

Parsing hierarchical data the hard way

Als u hiërarchische gegevens wilt parsen in een platte struct – d.w.z, u wilt user.firstName kunnen schrijven in plaats van user.name.firstName – dan moet u zelf wat parsing doen. Dit is echter niet al te moeilijk, en Codable maakt het prachtig type veilig.

Maak eerst de struct waar u mee wilt eindigen:

struct User: Codable { var firstName: String var lastName: String var age: Int}

Ten tweede, we moeten coderingssleutels definiëren die beschrijven waar gegevens kunnen worden gevonden in de hiërarchie.

Laten we nog eens naar de JSON kijken:

let json = """"""

Zoals u kunt zien, is er aan de wortel een sleutel genaamd “naam” en een andere genaamd “leeftijd”, dus we moeten die toevoegen als onze root coderingssleutels. Zet dit in je struct:

enum CodingKeys: String, CodingKey { case name, age}

In “name” zaten nog twee sleutels, “first_name” en “last_name”, dus we gaan wat coderingssleutels voor die twee maken. Voeg dit toe:

enum NameCodingKeys: String, CodingKey { case firstName, lastName}

Nu het moeilijke gedeelte: we moeten een aangepaste initializer en aangepaste codeermethode schrijven voor ons type. Begin met het toevoegen van deze lege methode aan uw struct:

init(from decoder: Decoder) throws {}

Daarbinnen is het eerste wat we moeten doen een container tevoorschijn te halen die we kunnen lezen met behulp van de sleutels van onze CodingKeys enum, zoals dit:

let container = try decoder.container(keyedBy: CodingKeys.self)

Als dat eenmaal is gebeurd, kunnen we proberen onze age eigenschap te lezen. Dit gebeurt op een type-veilige manier: u vertelt het type dat u wilt decoderen (Int.self voor onze leeftijd), samen met een sleutelnaam uit de CodingKeys enum:

age = try container.decode(Int.self, forKey: .age)

Nog een niveau lager moeten we gaan graven om onze naamgegevens te lezen. Zoals je eerder zag, is “naam” een top-level sleutel in onze CodingKeys enum, maar het is eigenlijk een geneste container van andere waarden die we moeten lezen. Dus moeten we die container tevoorschijn halen:

let name = try container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)

En tenslotte kunnen we twee strings lezen voor de .firstName en .lastName sleutels:

firstName = try name.decode(String.self, forKey: .firstName)lastName = try name.decode(String.self, forKey: .lastName)

Dat maakt de aangepaste initializer af, maar we moeten nog één methode schrijven: encode(to:). Dit is in feite het omgekeerde van de initializer die we zojuist hebben geschreven, omdat het de taak is om onze eigenschappen weer om te zetten in geneste container zoals nodig.

Dit betekent dat het creëren van een container op basis van onze CodingKeys enum en het schrijven van age daar, dan het creëren van een geneste container op basis van onze NameCodingKeys enum, en het schrijven van zowel firstName en lastName daar:

func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(age, forKey: .age) var name = container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name) try name.encode(firstName, forKey: .firstName) try name.encode(lastName, forKey: .lastName)}

Dat is het einde van al onze code. U kunt nu de firstName eigenschap direct lezen, wat veel prettiger is!

Hacken met Swift wordt gesponsord door RevenueCat

SPONSORED Het bouwen en onderhouden van in-app abonnementsinfrastructuur is moeilijk. Gelukkig is er een betere manier. Met RevenueCat kunt u abonnementen voor uw app implementeren in uren, niet maanden, zodat u zich kunt bezighouden met het bouwen van uw app.

Probeer het gratis

Sponsor Hacking with Swift en bereik ’s werelds grootste Swift-gemeenschap!

Leave a Reply