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
.
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!
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