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