Codable cheat sheet

Codable var en af hjørnestenene i Swift 4.0, som medførte en utrolig smidig konvertering mellem Swift-datatyper og JSON. Det blev derefter endnu bedre i Swift 4.1 takket være nye funktioner, der blev tilføjet, og jeg forventer, at der kommer endnu større ting i fremtiden.

I denne artikel vil jeg give hurtige kodeeksempler, der kan hjælpe dig med at besvare almindelige spørgsmål og løse almindelige problemer, alt sammen ved hjælp af Codable.

Hacking with Swift er sponsoreret af RevenueCat

SPONSORERET Det er svært at opbygge og vedligeholde infrastruktur til abonnementer i apps. Heldigvis er der en bedre måde. Med RevenueCat kan du implementere abonnementer til din app på få timer, ikke måneder, så du kan vende tilbage til at bygge din app.

Prøv det gratis

Sponsor Hacking with Swift, og nå ud til verdens største Swift-fællesskab!

Kodning og afkodning af JSON

Lad os starte med det grundlæggende: konvertering af noget JSON til Swift-strukturer.

Først får du her noget JSON at arbejde med:

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

Den sidste linje konverterer det til et Data-objekt, fordi det er det, som Codable-dekodere arbejder med.

Næst skal vi definere en Swift-struct, der skal indeholde vores færdige data:

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

Nu kan vi gå videre og udføre afkodningen:

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

Det vil udskrive “Paul”, som er navnet på den første bruger i JSON.

Konvertering af case

Et almindeligt problem med JSON er, at det vil bruge en anden formatering for sine nøglebetegnelser, end vi ønsker at bruge i Swift. Du kan f.eks. få “first_name” i din JSON og skal konvertere det til en firstName-egenskab.

Nu er en indlysende løsning her bare at ændre enten JSON- eller Swift-typerne, så de bruger den samme navngivningskonvention, men det gør vi ikke her. I stedet vil jeg antage, at du har kode som denne:

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

For at få dette til at fungere skal vi kun ændre én egenskab i vores JSON-dekoder:

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

Det instruerer Swift om at mappe navne med slangeskrift (names_written_like_this) til navne med camel case (namesWrittenLikeLikeThis).

Mapping af forskellige nøgletalsnavne

Hvis du har JSON-nøgler, der er helt forskellige fra dine Swift-egenskaber, kan du mappe dem ved hjælp af et CodingKeys enum.

Tag et kig på denne JSON:

let json = """"""

Disse nøgletalsnavne er ikke gode, og vi vil egentlig gerne konvertere disse data til en struct som denne:

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

For at få det til at ske skal vi erklære et CodingKeys enum: en kortlægning, som Codable kan bruge til at konvertere JSON-navne til egenskaber for vores struct. Dette er et almindeligt enum, der bruger strenge til sine råværdier, så vi kan angive både vores egenskabsnavn (enum-betegnelsen) og JSON-navnet (enum-værdien) på samme tid. Det skal også overholde CodingKey-protokollen, hvilket er det, der får dette til at fungere med Codable-protokollen.

Så tilføj dette enum til struct’en:

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

Det vil nu kunne afkode JSON’en som planlagt.

Bemærk: Enummet hedder CodingKeys og protokollen hedder CodingKey.

Arbejde med ISO-8601-datoer

Der er mange måder at arbejde med datoer på internettet på, men ISO-8601 er den mest almindelige. Den koder den fulde dato i formatet ÅÅÅÅÅ-MM-DD, derefter bogstavet “T” for at signalere starten af tidsoplysninger, derefter klokkeslættet i formatet HH:MM:SS og til sidst en tidszone. Tidszonen “Z”, der er en forkortelse for “Zulu time”, bruges almindeligvis til at betyde UTC.

Codable kan håndtere ISO-8601 med en indbygget datokonverter. Så givet denne JSON:

let json = """"""

Vi kan afkode den således:

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

Det muliggør ISO-8601-dataparsning, som konverterer fra 1999-04-03T17:30:31Z til en Date-instans, samtidig med at den håndterer konverteringen fra slangeskrift til kamelskrift.

Arbejde med andre almindelige datoer

Swift leveres med indbygget understøttelse af tre andre vigtige datoformater. Du bruger dem ligesom du bruger ISO-8601-datoer som vist ovenfor, så jeg vil blot tale kort om dem:

  • .deferredToDateformatet er Apples eget datoformat, og det registrerer antallet af sekunder og millisekunder siden den 1. januar 2001. Det er ikke rigtig brugbart uden for Apples platforme.
  • Det .millisecondsSince1970-formatet registrerer antallet af sekunder og millisekunder siden den 1. januar 1970. Dette er ret almindeligt online.
  • Det .secondsSince1970-formatet registrerer antallet af hele sekunder siden 1. januar 1970. Dette er ekstremt almindeligt online og er kun overgået af ISO-8601.

Arbejde med brugerdefinerede datoer

Hvis dit datoformat ikke passer til en af de indbyggede muligheder, skal du ikke fortvivle: Codable kan analysere brugerdefinerede datoer baseret på en datoformatter, som du opretter.

For eksempel sporer denne JSON den dag, hvor en studerende dimitterede fra universitetet:

let json = """"""

Det bruger datoformatet DD-MM-YYYYYY, som ikke er en af Swifts indbyggede muligheder. Heldigvis kan du levere en forudkonfigureret DateFormatter-instans som en datodekodningsstrategi, som her:

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

Arbejde med mærkelige datoer

Sommetider får du datoer, der er så mærkelige, at selv DateFormatter ikke kan håndtere dem. Du kan f.eks. få JSON, der gemmer datoer ved hjælp af antallet af dage, der er gået siden 1. januar 1970:

let json = """"""

For at få det til at fungere skal vi skrive en brugerdefineret dekoder til datoen. Alt andet vil stadig blive håndteret af Codable – vi leverer bare en brugerdefineret lukning, der behandler datadelen.

Du kan prøve at lave noget hacky matematik her, f.eks. ved at gange dagstallet med 86400 (antallet af sekunder i en dag) og derefter bruge addTimeInterval()-metoden i Date. Det tager dog ikke højde for sommertid og andre datoproblemer, så en bedre løsning er at bruge DateComponents og Calendar på denne måde:

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()}

Varsling: Hvis du skal analysere mange datoer, skal du huske, at denne lukning vil blive kørt for hver eneste dato – gør det hurtigt!

Parse hierarkiske data på den nemme måde

Alle ikke-trivielle JSON-filer vil sandsynligvis have hierarkiske data – en samling af data, der er indlejret i en anden. For eksempel:

let json = """"""

Codable er i stand til at håndtere dette fint, så længe du kan beskrive relationerne tydeligt.

Jeg finder den nemmeste måde at gøre dette på ved at bruge nested structs, som her:

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

Ulemperne er, at hvis du vil læse en brugers fornavn, skal du bruge user.name.firstName, men i det mindste er selve parsingarbejdet trivielt – vores eksisterende kode fungerer allerede!

Parse hierarkiske data på den hårde måde

Hvis du ønsker at parse hierarkiske data til en flad struct – dvs, du vil være i stand til at skrive user.firstName i stedet for user.name.firstName – så skal du selv lave noget parsing. Det er dog ikke så svært, og Codable gør det smukt typesikkert.

Først skal du oprette den struct, du ønsker at ende med:

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

For det andet skal vi definere kodningsnøgler, der beskriver, hvor data kan findes i hierarkiet.

Lad os se på JSON igen:

let json = """"""

Som du kan se, er der ved roden en nøgle kaldet “name” og en anden kaldet “age”, så vi er nødt til at tilføje det som vores rodkodningsnøgler. Sæt dette ind i din struct:

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

Inden for “name” var der yderligere to nøgler, “first_name” og “last_name”, så vi vil oprette nogle kodningsnøgler for disse to. Tilføj dette:

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

Nu kommer den svære del: Vi skal skrive en brugerdefineret initializer og en brugerdefineret kodningsmetode til vores type. Start med at tilføje denne tomme metode til din struct:

init(from decoder: Decoder) throws {}

Derinde skal vi først forsøge at trække en container frem, som vi kan læse ved hjælp af nøglerne i vores CodingKeys enum, som her:

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

Når det er gjort, kan vi forsøge at læse vores age-egenskab. Dette gøres på en type-sikker måde: Du fortæller den typen, du vil afkode (Int.self for vores alder), sammen med et nøgle-navn fra CodingKeys enummet:

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

Næst skal vi grave et niveau ned for at læse vores navnedata. Som du så tidligere, er “name” en nøgle på øverste niveau inden for vores CodingKeys enum, men det er faktisk en indlejret container med andre værdier, som vi skal læse inden for. Så vi skal trække denne container ud:

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

Og endelig kan vi læse to strenge for nøglerne .firstName og .lastName:

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

Dermed er den brugerdefinerede initialisator færdig, men vi har stadig en metode mere at skrive: encode(to:). Dette er i praksis det omvendte af den initialisator, vi lige har skrevet, for dens opgave er at konvertere vores egenskaber tilbage til en nestet container, som det er relevant:

Det betyder, at vi skal oprette en container baseret på vores CodingKeys enum og skrive age der, og derefter oprette en nestet container baseret på vores NameCodingKeys enum og skrive både firstName og lastName der:

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)}

Dermed er al vores kode færdiggjort. Med det på plads kan du nu læse firstName-egenskaben direkte, hvilket er meget pænere!

Hacking with Swift er sponsoreret af RevenueCat

SPONSORERET Det er svært at opbygge og vedligeholde infrastruktur til abonnementer i apps. Heldigvis er der en bedre måde. Med RevenueCat kan du implementere abonnementer til din app på få timer, ikke måneder, så du kan vende tilbage til at bygge din app.

Prøv det gratis

Sponsor Hacking with Swift, og nå ud til verdens største Swift-fællesskab!

Leave a Reply