Arbejde med Codable og JSON i Swift

Skrevet af Reinder de Vries den 20. januar 2021 i App-udvikling, iOS, Swift

Arbejde med Codable og JSON i Swift

Du kan bruge Codable i Swift til at kode og afkode brugerdefinerede dataformater, som f.eks. JSON, til native Swift-objekter. Det er utrolig nemt at mappe Swift-objekter til JSON-data og omvendt ved blot at anvende Codable-protokollen.

Som pragmatisk iOS-udvikler vil du før eller siden komme i kontakt med JSON. Alle webtjenester, fra Facebook til Foursquare, bruger JSON-formatet til at få data ind og ud af din app. Hvordan kan du kode og afkode disse JSON-data til Swift-objekter effektivt?

I denne vejledning lærer du, hvordan du arbejder med JSON-objekter i Swift ved hjælp af Codable-protokollen. Det, du lærer, kan også nemt anvendes på andre dataformater. Vi kommer ind på kodning og afkodning med JSONEncoder og JSONDecoder, og jeg viser dig, hvordan du kan formidle mellem JSON- og Swift-data.

Er du klar? Så er vi i gang.

  1. Kom i gang: Kodning og afkodning
  2. Den forklarede Codable-protokol
  3. Arbejde med Codable
  4. Dekodning af JSON til Swift-objekter med Codable
  5. Kodning af Swift-objekter som JSON med Codable
  6. Arbejde med nested arrays og Codable
  7. Videre læsning

Kom i gang: Kodning og afkodning

Hvilket problem løser Codable-protokollen i Swift egentlig? Lad os starte med et eksempel.

Forestil dig, at du er ved at bygge en opskriftsapp. Appen viser forskellige opskrifter på en liste, herunder ingredienser, instruktioner og grundlæggende oplysninger om mad.

Du får dataene til appen fra en webservice og deres cloud-baserede API. Denne API bruger JSON-dataformatet. Hver gang du anmoder om en opskrift fra webtjenesten, får du JSON-data tilbage.

Her er et eksempel på JSON-data for en opskrift:

{ "name": "Spaghetti Bolognese", "author": "Reinder's Cooking Corner", "url": "https://cookingcorner.com/spaghetti-bolognese", "yield": 4, "ingredients": , "instructions": "Cook spaghetti, fry beef and garlic, add tomatoes, add love, eat!"}

Se på strukturen af JSON-dataene.

JSON-objekter er indpakket i snoede parenteser { og }, og arrays er indpakket i firkantede parenteser . Ejendomsnavne er strenge, der er omsluttet af anførselstegn ". Værdier i JSON kan være strenge, tal (uden anførselstegn), arrays eller andre objekter. Du kan også indlejre data, dvs. arrays i arrays, objekter i arrays osv. for at skabe et komplekst hierarki af data.

JSON er et tekstbaseret dataformat, som mange webtjenester bruger, herunder API’er fra Twitter, Facebook, Foursquare osv. Hvis du bygger apps, der bruger webbaserede ressourcer, vil du støde på JSON.

JSON-formatet er overlegen i forhold til XML, som er et almindeligt alternativ, fordi det er effektivt, nemt at analysere og læses af mennesker. JSON er et vedtaget format for webtjenester, API’er og apps. Det bruges overalt på nettet, i apps og onlinetjenester, fordi formatet er enkelt og fleksibelt.

Og JSON har én fantastisk egenskab: Du kan kode ethvert dataformat i JSON og afkode JSON tilbage til ethvert dataformat. Denne kodnings- og afkodningsproces er det, der gør JSON så kraftfuld.

Du kan tage dine Swift String-, Int-, Double-, URL-, Date-, Data-, Array– og Dictionary-værdier og kode dem som JSON. Derefter sender du dem til webtjenesten, som afkoder værdierne til et naturligt format, som den forstår. På samme måde sender webtjenesten data kodet som JSON til din app, og du afkoder dataene til native typer som String, Double og Array.

Når din opskriftsapp modtager JSON (se ovenfor), kan det derefter afkodes til en Swift-struktur, som denne:

struct Recipe { var name: String var author: String var url: URL var yield: Int var ingredients: var instructions: String}

I Swift bruges Codable-protokollen til at gå fra et JSON-dataobjekt til en egentlig Swift-klasse eller -struktur. Dette kaldes afkodning, fordi JSON-dataene afkodes til et format, som Swift forstår. Det fungerer også den anden vej: Kodning af Swift-objekter som JSON.

Codableprotokollen i Swift er faktisk et alias for Decodable– og Encodable-protokollerne. Fordi du ofte bruger kodning og afkodning sammen, bruger du Codable-protokollen for at få begge protokoller på én gang.

Det centrale element i arbejdsgangen for kodning/afkodning er Swifts Codable-protokol. Lad os nu finde ud af, hvordan den fungerer!

Kan du ikke kende forskel på “kodning” og “afkodning”? Tænk på det på denne måde: Vi konverterer data fra og til “kode”, ligesom en Enigma-maskine eller en hemmelig krypto-chiffer. Kodning betyder konvertering af data til kode; en-kodning, eller “i/med kode”. Afkodning betyder konvertering af kode til data; de-kodning, eller “fra/af kode”.

Bliv ansat som iOS-udvikler

Lær at bygge iOS 14-apps med Swift 5

Tilmeld dig mit iOS-udviklingskursus, og lær at starte din karriere som professionel iOS-udvikler.

The Codable Protocol Explained

Brug af JSON før Swift 4 var lidt af en PITA. Du skulle selv serialisere JSON’en med JSONSerialization og derefter typekaste hver egenskab i JSON’en til den rigtige Swift-type.

let json = try? JSONSerialization.jsonObject(with: data, options: )if let recipe = json as? { if let yield = recipe as? Int { recipeObject.yield = yield }}

Overstående uddrag henter kun en yield-værdi fra JSON’en og kaster den til Int. Det er ekstremt mundret, og det er svært at reagere korrekt på potentielle fejl og typeafvigelser. Selv om det virker, er det ikke ideelt.

Biblioteker som SwiftyJSON gør det meget nemmere at arbejde med JSON, men du skal stadig mappe JSON-dataene til deres respektive Swift-objekter og -egenskaber.

Med Swift 4 og senere kan du i stedet bruge Codable-protokollen. Din Swift-struktur eller -klasse skal blot vedtage Codable-protokollen, og du får JSON-kodning og -afkodning helt gratis ud af boksen.

Codableprotokollen er en sammensætning af to protokoller, Decodable og Encodable. Begge protokoller er ret minimale; de definerer kun henholdsvis funktionerne init(from: Decoder) og encode(to: Encoder). Med andre ord betyder disse funktioner, at for at en type kan være “decodable” eller “encodable”, skal den “decode fra noget” og “encode til noget”.

Den virkelige magi i Codable sker med protokollerne Decoder og Encoder. Disse protokoller vedtages af de komponenter, der koder/afkoder forskellige dataformater, som f.eks. JSON. I Swift har vi et par -kodere:

  • PropertyListEncoder og PropertyListDecoder for .plist-egenskabslister
  • JSONEncoder og JSONDecoder for JSON – det er os!
  • NSKeyedArchiver kan arbejde med Codable, via PropertyListEncoder, Data og NSCoding

JSONDecoder og JSONEncoder-klasserne bruger Decoder– og Encoder-protokollerne til at levere funktionaliteten til at afkode/kode JSON. Det betyder også, at du kan skrive din egen brugerdefinerede encoder/decoder til Codable, forudsat at du anvender Decoder– og Encoder-protokollerne!

Har du brug for en genopfriskning af protokoller i Swift? Læs Protocols In Swift Explained for at få mere at vide.

Arbejde med Codable

Lad os tage et kig på et eksempel. Vi skal mappe nogle JSON-data til en Swift-struct. Vi afkoder i det væsentlige JSON til et egentligt Swift-objekt.

Først opretter vi en struct kaldet User. Sådan her:

struct User: Codable { var first_name: String var last_name: String var country: String}

Den User struct har tre enkle egenskaber af typen String og er i overensstemmelse med Codable-protokollen.

Dernæst skriver vi en smule JSON. Dette er den JSON, vi skal arbejde med:

{ "first_name": "John", "last_name": "Doe", "country": "United Kingdom"}

JSON-data kommer typisk ind i din app som svar på en webserviceanmodning eller via en .json-fil, men i dette eksempel lægger vi blot JSON i en String. Som dette:

let jsonString = """{ "first_name": "John", "last_name": "Doe", "country": "United Kingdom"}"""

Bemærk: Ovenstående kode bruger det tredobbelte citationstegn """ til at oprette strenge på flere linjer. Pænt!

Det næste, vi gør, er at afkode JSON’en og omdanne den til et User-objekt. Sådan her:

Her er, hvad der sker:

  • Først forvandler du jsonString til et Data-objekt ved at kalde data(using:)-funktionen på strengen. Dette er et nødvendigt mellemtrin.
  • Dernæst opretter du et JSONDecoder-objekt og kalder straks decode(_:from:)-funktionen på det. Dette forvandler jsonData til et objekt af typen User ved at afkode JSON’en. User.self-typen angives som parameter.
  • Slutteligt udskriver du brugerens efternavn med print(user.last_name). Denne user-værdi har User som type, så det er et egentligt Swift-objekt!

Nemt, ikke? Du har i det væsentlige “mappet” JSON-objektet til en Swift-struct og afkodet JSON-formatet til et indfødt objekt, som Swift kan arbejde med.

I ovenstående kode ignorerer vi alle fejl, der kastes af decode(_:from:) med try!. I din egen kode skal du håndtere fejl med en do-try-catch-blok. En fejl, der kan blive kastet, kommer f.eks. ved at levere ugyldig JSON.

User.self er en metatype, en henvisning til selve typen User. Vi fortæller dekoderen, at vi ønsker at afkode dataene med User struct, ved at give den en reference til denne type.

En almindelig fejl, når du definerer din Codable type, som User struct, er fejlmatchede nøgler og egenskaber. Hvis du har tilføjet en egenskab til din struct, som ikke er til stede i JSON’en eller har en anden type, får du en fejl, når du afkoder JSON’en. Det modsatte, dvs. en egenskab, der ikke findes i din struct, men som findes i JSON, er ikke et problem. Det er nemmest at debugge dette én egenskab ad gangen, dvs. bliv ved med at tilføje egenskaber, indtil JSON’en ikke længere afkodes uden fejl. Du kan også bestemme, hvilke egenskaber/nøgler der skal medtages eller ignoreres med CodingKeys enummet (se nedenfor).

Afkodning af JSON til Swift-objekter med Codable

Lad os tage et nyt kig på eksemplet fra det foregående afsnit og udvide det. Her er den JSON, vi arbejder med:

let jsonString = """{ "first_name": "John", "last_name": "Doe", "country": "United Kingdom"}"""

Vi omdanner den derefter til et Data-objekt. Sådan her:

let jsonData = jsonString.data(using: .utf8)!

Dette er et nødvendigt mellemtrin. I stedet for at repræsentere JSON som en streng gemmer vi nu JSON’en som et indfødt Data-objekt. Den tegnkodning, vi bruger til denne streng, er UTF8.

Hvis du kigger nærmere efter, vil du se, at ovenstående kode bruger force unwrapping til at arbejde med den valgfrie returværdi fra data(using:). Lad os afvikle den valgfri værdi på en mere elegant måde!

if let jsonData = jsonString.data(using: .utf8) { // Use `jsonData`} else { // Respond to error }

Med ovenstående kode kan vi reagere på fejl, når data(using:) returnerer nil. Du kan f.eks. vise en fejlmeddelelse eller stille og roligt lade opgaven fejle og gemme diagnostiske oplysninger i en log.

Næste gang opretter vi et nyt JSONDecoder-objekt. Sådan her:

let decoder = JSONDecoder()

Vi bruger derefter denne dekoder til at afkode JSON-dataene. Sådan her:

let user = try! decoder.decode(User.self, from: jsonData)

Den decode(_:from:)-funktion kan dog kaste fejl. Ovenstående kode går ned, når det sker. Igen ønsker vi at reagere på alle fejl, der måtte opstå. Som dette:

do { let user = try decoder.decode(User.self, from: jsonData) print(user.last_name)} catch { print(error.localizedDescription)}

Og hele kodeblokken ser nu ud som følgende. Kan du se, hvordan det er anderledes?

if let jsonData = jsonString.data(using: .utf8){ let decoder = JSONDecoder() do { let user = try decoder.decode(User.self, from: jsonData) print(user.last_name) } catch { print(error.localizedDescription) }}

Tavshed aldrig om fejl. Fang fejlen, og reager på den, enten med UI/UX, ved at prøve opgaven igen eller ved at logge, crashe og rette den.

Arbejde med CodingKeys

Hvad sker der, hvis vores JSON-egenskaber, som first_name og/eller firstName, er anderledes end Swift-struct-egenskaberne? Det er her, CodingKeys kommer ind i billedet.

Alle klasser eller structs, der er i overensstemmelse med Codable, kan deklarere en særlig nested enumeration kaldet CodingKeys. Du bruger den til at definere egenskaber, der skal kodes og afkodes, herunder deres navne.

Lad os se på et eksempel. I User struct’en nedenfor har vi ændret egenskabsnavnene fra snake_case til camelCase. F.eks. hedder nøglen first_name nu firstName.

struct User:Codable { var firstName: String var lastName: String var country: String enum CodingKeys: String, CodingKey { case firstName = "first_name" case lastName = "last_name" case country }}

Når du bruger ovenstående struct sammen med eksemplerne i de foregående afsnit, vil du se, at du kan bruge et User-objekt med de nye egenskabsnavne. Som her:

print(user.firstName)// Output: Johnprint(user.country)// Output: United Kingdom

Den CodingKeys opregning mapper sine tilfælde til egenskaber og bruger strengværdierne til at vælge de rigtige egenskabsnavne i JSON-dataene. Tilfældet i enummet er navnet på den egenskab, du vil bruge i strukturen, og dets værdi er navnet på nøglen i JSON-dataene.

Kodning af Swift-objekter som JSON med Codable

Så langt har vi fokuseret på afkodning af JSON-data til Swift-objekter. Hvad med at gå den anden vej? Kan vi også kode objekter som JSON? Ja, hvorfor ikke!

var user = User()user.firstName = "Bob"user.lastName = "and Alice"user.country = "Cryptoland"let jsonData = try! JSONEncoder().encode(user)let jsonString = String(data: jsonData, encoding: .utf8)!print(jsonString)

Og outputtet er:

{"country":"Cryptoland","first_name":"Bob","last_name":"and Alice"}

Hvad sker der her?

  • Først opretter du et User objekt og tildeler nogle værdier til dets egenskaber
  • Dernæst bruger du encode(_:) til at kode user objektet til et Data JSON Data objekt
  • Endeligt, konverterer du Data-objektet til String og udskriver det

Hvis du kigger nærmere efter, kan du se, at kodningen følger de samme trin som afkodningen, bortset fra omvendt. Går fra Swift-objektet gennem dekoderen og resulterer i en JSON-streng.

Som før kan vi udvide eksemplet til at håndtere fejl? Ja! Som her:

let encoder = JSONEncoder()encoder.outputFormatting = .prettyPrinteddo { let jsonData = try encoder.encode(user) if let jsonString = String(data: jsonData, encoding: .utf8) { print(jsonString) }} catch { print(error.localizedDescription)}

I ovenstående eksempel bruger vi også outputFormatting-egenskaben i enkoderen til at “pænt udskrive” JSON-dataene. Dette tilføjer mellemrum, tabulatorer og newlines for at gøre JSON-strengen lettere at læse. Sådan her:

{ "country" : "Cryptoland", "first_name" : "Bob", "last_name" : "and Alice"}

Awesome!

Det er værd at bemærke, at ordbøger i Swift, og i JSON, ikke har en fast sorteringsrækkefølge. Derfor vil du se varierende sorteringsrækkefølger for nøglerne country, first_name osv. i JSON, hvis du kører ovenstående kode et par gange. De fleste webservices, der udsender JSON, vil håndhæve en fast sorteringsrækkefølge via et skema, hvilket bestemt gør det nemmere at læse og navigere i JSON.

Arbejde med nestede arrays og codable

Så langt har vi arbejdet med simple JSON-objekter – blot en klasse med et par egenskaber. Men hvad nu, hvis de data, du arbejder med, er mere komplekse?

De JSON-data, du kan få tilbage fra en Twitter- eller Facebook-webservice, er f.eks. ofte nested. Det vil sige, at de data, du ønsker at få adgang til, er begravet i 2-3 niveauer af JSON-arrays og ordbøger. Hvad nu?

Lad os først se på et simpelt eksempel. Her er den Swift-struktur, vi skal arbejde med:

struct User: Codable { var first_name: String var last_name: String}

Dernæst er her de JSON-data, vi ønsker at afkode:

let jsonString = """"""

Hvis du ser godt efter, kan du se, at de data, vi har brug for, er gemt som et array. JSON er ikke ét enkelt objekt, men et array med 3 User-objekter. Vi ved, at det er et array af objekter, på grund af de firkantede parenteser (array) og de snørklede parenteser { } (objekt). Flere objekter er altid adskilt med kommaer.

Hvordan kan vi afkode disse User objekter? Sådan gør vi:

let jsonData = jsonString.data(using: .utf8)!let users = try! JSONDecoder().decode(.self, from: jsonData)for user in users { print(user.first_name)}

Overstående kodestykke ligner meget det, du har set før, men der er én vigtig forskel. Kan du se den type, vi leverer til decode(_:from:)-funktionen? Typen er (med .self), eller “array of User objects”. I stedet for at arbejde med ét User-objekt ønsker vi at afkode en masse af dem, og det betegnes med -array-typen.

Næste gang skal vi diskutere, hvordan du kan arbejde med mere komplekse nested-typer. Overvej for eksempel, at arrayet af User-objekter er indlejret i en JSON-ordbog. På denne måde:

let jsonString = """{ "users": }"""

I ovenstående JSON-snippet er elementet på øverste niveau en ordbog (eller “objekt”). Det har kun ét nøgle-værdipar: et users array. De data, vi ønsker, befinder sig i dette array. Hvordan kommer vi til dem?

Det er vigtigt, at vores fremgangsmåde ikke er for kompliceret. Det er let at tro, at du får brug for avanceret JSON-parsing, men som det viser sig, kan vi faktisk også indlejre Swift-strukturer. Vi vil beskrive hele JSON som en struct med User struct indeni.

Her kan du se det her:

struct Response: Codable{ struct User: Codable { var first_name: String var last_name: String } var users: }

Her er, hvad der foregår:

  • Den øverste JSON-ordbog svarer til Response-typen. Ligesom tidligere er denne type i overensstemmelse med Codable for at understøtte JSON-kodning og -afkodning.
  • Inden for denne struct har vi defineret en anden struct kaldet User. Dette er nøjagtig den samme type, som vi har brugt før. Denne struct er “nested”.
  • Den Response struct har en egenskab kaldet users af typen , eller array af User-objekter.

Det fede er, at semantisk set er både JSON- og Swift-datastrukturerne nøjagtig ens. De bruger bare en anden syntaks. Vi har defineret et indlejret array inde i ordbogen på øverste niveau, ligesom Response struct har en indlejret User struct og users egenskab inde i den.

Det er nu en leg at få det til at fungere. Tjek dette ud:

let jsonData = jsonString.data(using: .utf8)!let response = try! JSONDecoder().decode(Response.self, from: jsonData)for user in response.users { print(user.first_name)}

Se, hvordan vi bruger Response typen til at afkode JSON og derefter løbe over response.users egenskaben? Kontrollér det med Response struct’en og JSON’en. Vi vælger users nøgle-værdiparret i ordbogen på øverste niveau og mapper objekterne indeni til User-objekter. Pænt!

Brug af indlejrede typer er en fantastisk generel tilgang til komplekse JSON-datastrukturer. Når du støder på en del af JSON, som du ikke let kan repræsentere i en håndgribelig type, f.eks. User eller Tweet, kan du prøve at udvide typen til noget som Response eller UserCollection. I stedet for at gå dybere, skal du gå bredere, og indarbejde hele JSON’en. Brug indlejrede typer for at nå frem til de data, du ønsker. Det er også værd at bemærke her, at du ikke behøver at tilføje User struct i Response struct – den kan også gå uden for den.

Bliv ansat som iOS-udvikler

Lær at bygge iOS 14-apps med Swift 5

Tilmeld dig mit iOS-udviklingskursus, og lær hvordan du starter din karriere som professionel iOS-udvikler.

Yderligere læsning

Og det er alt, hvad der er at sige! Du ved nu:

  • Hvordan du bruger Codable til at oprette objekter, der kan kodes og afkodes
  • Hvordan du bruger JSONDecoder og JSONEncoder til at kode og afkode JSON-objekter og deres Swift-modstykker
  • Hvad kodning og afkodning er til, og hvorfor det er vigtigt i daglig iOS-udvikling
  • Hvordan man arbejder med mere komplekse JSON-data, og hvordan man bruger nested types

Vil du lære mere? Tjek disse ressourcer:

  • Arrays, ordbøger og strukturer
  • Arbejde med JSON i Swift med SwiftyJSON
  • Introduktion af objektorienteret programmering i Swift
  • Sådan gør du: Sådan finder du et element i en array i Swift
  • Sådan bruger du Apples udviklerdokumentation til sjov og profit
  • Sådan finder du strings med regulære udtryk i Swift
  • Hvorfor App Architecture Matters
  • Strings in Swift Explained
  • Arbejde med filer på iOS med Swift
  • Lagring af data med NSCoding og NSKeyedArchiver

Skriv en kommentar