Kódolható puskázó lap

Codable A Swift 4.0 egyik sarkalatos funkciója volt, amely hihetetlenül zökkenőmentes konverziót hozott magával a Swift adattípusok és a JSON között. Ez aztán a Swift 4.1-ben még jobb lett, köszönhetően az új funkciók hozzáadásának, és a jövőben még nagyobb dolgokra számítok.

Ebben a cikkben gyors kódmintákat szeretnék nyújtani, hogy segítsek a gyakori kérdések megválaszolásában és a gyakori problémák megoldásában, mindezt a Codable használatával.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Az alkalmazáson belüli előfizetési infrastruktúra kiépítése és karbantartása nehéz feladat. Szerencsére van egy jobb megoldás. A RevenueCat segítségével órák, nem hónapok alatt implementálhat előfizetéseket az alkalmazásához, így visszatérhet az alkalmazás építéséhez.

Kipróbálja ingyen

Szponzorálja a Hacking with Swiftet, és érje el a világ legnagyobb Swift közösségét!

A JSON kódolása és dekódolása

Kezdjük az alapokkal: néhány JSON átalakítása Swift-struktúrákká.

Először is, itt van néhány JSON, amivel dolgozhatunk:

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

Az utolsó sor egy Data objektummá alakítja, mert a Codable dekóderek ezzel dolgoznak.

A következőkben definiálnunk kell egy Swift struktúrát, amely a kész adatainkat fogja tárolni:

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

Most már mehetünk is tovább, és elvégezhetjük a dekódolást:

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

Ez kiírja a “Paul”-t, ami az első felhasználó neve a JSON-ban.

Konvertáló eset

A JSON-nal gyakori probléma, hogy más formázást használ a kulcsnevekhez, mint amit mi a Swiftben használni szeretnénk. Például előfordulhat, hogy a JSON-ban a “first_name” szerepel, és ezt kell átalakítanunk egy firstName tulajdonsággá.

Az egyik nyilvánvaló megoldás itt az, hogy egyszerűen megváltoztatjuk akár a JSON, akár a Swift típusokat, hogy ugyanazt a névadási konvenciót használják, de ezt most nem fogjuk megtenni. Ehelyett feltételezem, hogy ilyen kódod van:

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

Hogy ez működjön, csak egy tulajdonságot kell megváltoztatnunk a JSON dekódolónkban:

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

Ez utasítja a Swiftet, hogy a kígyós esetű neveket (names_written_like_this) camel case nevekre (namesWrittenLikeThis) képezze le.

Eltérő kulcsnevek leképezése

Ha olyan JSON-kulcsaid vannak, amelyek teljesen eltérnek a Swift tulajdonságaitól, akkor egy CodingKeys enum segítségével leképezheted őket.

Vessünk egy pillantást erre a JSON-ra:

let json = """"""

Ezek a kulcsnevek nem túl jók, és valójában szeretnénk ezeket az adatokat egy ilyen struktúrává alakítani:

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

Hogy ez megtörténjen, egy CodingKeys enumot kell deklarálnunk: egy leképezést, amelyet Codable használhatunk arra, hogy a JSON neveket a struktúránk tulajdonságaiba alakítsuk. Ez egy szabályos enum, amely stringeket használ a nyers értékeihez, így egyszerre adhatjuk meg a tulajdonságunk nevét (az enum esetét) és a JSON nevet (az enum értékét). Emellett meg kell felelnie a CodingKey protokollnak, ami miatt ez a Codable protokollal is működik.

Ezt az enumot tehát adjuk hozzá a structhoz:

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

Ez most már képes lesz dekódolni a JSON-t a tervek szerint.

Megjegyzés: Az enum neve CodingKeys, a protokollé pedig CodingKey.

Munka ISO-8601 dátumokkal

Az interneten sokféleképpen lehet dátumokkal dolgozni, de az ISO-8601 a legelterjedtebb. Ez kódolja a teljes dátumot ÉÉÉÉÉ-HH-HH-HH-NN formátumban, majd a “T” betűt az időinformáció kezdetének jelzésére, majd az időt HH:MM:SS formátumban, végül pedig az időzónát. A “Z” időzóna, a “Zulu time” rövidítése általában az UTC-t jelenti.

Codable képes kezelni az ISO-8601-et a beépített dátumkonvertálóval. Adott tehát ez a JSON:

let json = """"""

Ezzel így dekódolhatjuk:

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

Ez lehetővé teszi az ISO-8601 dátumelemzést, amely az 1999-04-03T17:30:31Z dátumot egy Date példányra konvertálja, miközben kezeli a kígyós esetek teve esetre való átalakítását is.

Munka más gyakori dátumokkal

A Swift beépített támogatással rendelkezik három másik fontos dátumformátumhoz. Ezeket ugyanúgy használhatod, mint az ISO-8601-es dátumokat a fentiek szerint, ezért csak röviden beszélek róluk:

  • A .deferredToDate formátum az Apple saját dátumformátuma, és a 2001. január 1. óta eltelt másodpercek és milliszekundumok számát követi. Ez nem igazán hasznos az Apple platformjain kívül.
  • A .millisecondsSince1970 formátum az 1970. január 1. óta eltelt másodpercek és milliszekundumok számát követi. Ez elég gyakori az interneten.
  • A .secondsSince1970 formátum az 1970. január 1. óta eltelt egész másodpercek számát követi. Ez rendkívül gyakori az interneten, és az ISO-8601 után a második.

Egyéni dátumokkal való munka

Ha a dátumformátumod nem felel meg a beépített lehetőségek egyikének sem, ne ess kétségbe: A Codable képes elemezni az egyéni dátumokat egy általad létrehozott dátumformázó alapján.

Ez a JSON például nyomon követi, hogy egy diák mikor végzett az egyetemen:

let json = """"""

Ez a DD-MM-YYYY dátumformátumot használja, ami nem tartozik a Swift beépített opciói közé. Szerencsére megadhatsz egy előre konfigurált DateFormatter példányt dátumdekódolási stratégiaként, például így:

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

Furcsa dátumokkal való munka

Néha olyan furcsa dátumokat kapsz, hogy még a DateFormatter sem tudja kezelni őket. Például kaphatunk olyan JSON-t, amely a dátumokat az 1970. január 1. óta eltelt napok számával tárolja:

let json = """"""

Hogy ez működjön, írnunk kell egy saját dekódolót a dátumhoz. Minden mást továbbra is a Codable fog kezelni – mi csak egy egyéni lezárást biztosítunk, amely feldolgozza a dátumokat tartalmazó részt.

Megpróbálhatsz itt egy kis matematikát csinálni, például megszorozni a napok számát 86400-zal (a másodpercek száma egy napban), majd használni a Date addTimeInterval() módszerét. Ez azonban nem veszi figyelembe a nyári időszámítást és más dátumproblémákat, ezért jobb megoldás a DateComponents és a Calendar használata így:

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

Figyelmeztetés:

Hierarchikus adatok elemzése egyszerű módon

Minden nem triviális JSON valószínűleg hierarchikus adatokat tartalmaz – egy adatgyűjteményt egy másikba ágyazva. Például:

let json = """"""

Codable ezt remekül tudja kezelni, ha a kapcsolatokat világosan le tudjuk írni.

A legegyszerűbb megoldásnak az egymásba ágyazott struktúrák használatát tartom, például így:

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

A hátránya, hogy ha egy felhasználó keresztnevét akarjuk kiolvasni, akkor a user.name.firstName-t kell használnunk, de legalább a tényleges elemzési munka triviális – a meglévő kódunk már működik!

Hierarchikus adatok elemzése a nehezebb úton

Ha hierarchikus adatokat akarunk elemezni egy lapos struktúrába – azaz, user.name.firstName helyett user.firstName-t akarsz írni – akkor magadnak kell elvégezned egy kis elemzést. Ez azonban nem túl nehéz, és Codable gyönyörűen típusvédetté teszi.

Először is hozzuk létre azt a struktúrát, amit a végén szeretnénk:

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

Második lépésként meg kell határoznunk azokat a kódolási kulcsokat, amelyek leírják, hogy az adatok hol találhatók a hierarchiában.

Nézzük meg újra a JSON-t:

let json = """"""

Amint láthatjuk, a gyökérnél van egy “name” és egy másik “age” nevű kulcs, tehát ezeket kell hozzáadnunk a gyökér kódoló kulcsokhoz. Tegyük ezt a struktúránkba:

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

A “name”-ben volt még két kulcs, a “first_name” és a “last_name”, így létrehozunk néhány kódoló kulcsot ehhez a kettőhöz. Adjuk hozzá ezt:

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

Most jön a neheze: írnunk kell egy egyéni inicializálót és egy egyéni kódolási metódust a típusunkhoz. Kezdjük azzal, hogy hozzáadjuk ezt az üres metódust a struktúránkhoz:

init(from decoder: Decoder) throws {}

Az első dolog, amit tennünk kell, hogy megpróbálunk kihúzni egy olyan tárolót, amelyet a CodingKeys enumunk kulcsaival tudunk olvasni, így:

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

Ha ez megtörtént, megpróbálhatjuk kiolvasni a age tulajdonságunkat. Ez típusbiztos módon történik: megmondjuk neki a dekódolni kívánt típust (Int.self a mi korunk esetében), a CodingKeys enumból származó kulcsnévvel együtt:

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

A következőkben egy szinttel lejjebb kell ásnunk, hogy kiolvassuk a névadatainkat. Ahogy korábban láttuk, a “név” egy legfelső szintű kulcs a CodingKeys enumunkon belül, de ez valójában más értékek egymásba ágyazott tárolója, amiket be kell olvasnunk. Tehát ki kell húznunk ezt a konténert:

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

És végül kiolvashatjuk a .firstName és .lastName kulcsok két karakterláncát:

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

Ezzel befejeztük az egyéni inicializálót, de még egy metódust meg kell írnunk: encode(to:). Ez gyakorlatilag a fordítottja az imént írt inicializálónak, mivel az a feladata, hogy a tulajdonságainkat visszaalakítsa a megfelelő módon beágyazott konténerré.

Ez azt jelenti, hogy létrehozunk egy konténert a CodingKeys enumunk alapján, és oda írjuk a age-t, majd létrehozunk egy beágyazott konténert a NameCodingKeys enumunk alapján, és oda írjuk a firstName és a lastName-t is:

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

Ezzel befejeződött az összes kódunk. Ezzel a helyére került, most már közvetlenül olvashatjuk a firstName tulajdonságot, ami sokkal szebb!

Hacking with Swift is sponsored by RevenueCat

SPONSORED Az alkalmazáson belüli előfizetési infrastruktúra kiépítése és karbantartása nehéz. Szerencsére van egy jobb megoldás. A RevenueCat segítségével órák, nem pedig hónapok alatt megvalósíthatja az előfizetéseket az alkalmazásához, így visszatérhet az alkalmazás építéséhez.

Kipróbálja ingyen

Szponzorálja a Hacking with Swiftet és érje el a világ legnagyobb Swift közösségét!

Leave a Reply