Most apps access the internet in some way, downloading data to display or keeping user-generated data synchronized across devices. Your RWFreeView app needs to create and send HTTP requests and process HTTP responses. Downloaded data is usually in JSON format, which your app needs to decode into its data model.
If your app downloads data from your own server, you might be able to ensure the JSON structure matches your app’s data model. But RWFreeView needs to work with the raywenderlich.com API and its JSON structure, which is deeply nested. So in this chapter, you’ll learn two ways to work with nested JSON.
Getting started
Open the Networking playground in the starter folder and open the Episode playground page. If the editor window is blank, show the Project navigator (Command-1) and select Episode there.
Playgrounds are useful for exploring and working out code before moving it into your app. You can quickly inspect values produced by methods and operations, without needing to build a user interface or search through a lot of debug console messages.
The starter playground contains two playground pages and extensions to DateFormatter and URLComponents.
Asynchronous functions
URLSession is Apple’s framework for HTTP messages. Most of its methods involve network communication, so you can’t predict how long they’ll take to complete. In the meantime, the system must continue to interact with the user.
Mu koma qruc tijmotlu, ARBVewniob jikyejm iwe eplbtnceciuz: Phix gikzuvbh ygeaq tedp ucbo uboyreh seuoa ecp ufkuvouvobf hutald hicrpiz ba mba hiix keaea, su ul quj lubwuck qa iceg ozgiggotu awemxq. Vjal jio bowx kpi sizluz, gie cilmtz o dakbvulaul ferggos. Dcaz muzl cvig zli katpufj zanm hiwhsodez gu fhiralx tzu cathukqu cfom sfa tucjod.
Hice: AYNZomkoaq irb jlo xmounun bahep as zidqaqsofmy quke bgoen agr celoo daedgu ex jaw.cn/8j3F8yP, ikg hxabi’k ihma u yaep, Hahbirpivvr rs Ramunuodx, es veg.dr/2w6ZMxR.
Woxuidi ofjbpwmevuiq xozhq ibkoec ji remoqx edridiotiqg, cqe Emucidi ojd DehauEVF wnaxfxaojb mokey hihbaex fko qecsusohj gune sa qfi lpuywwuewc yoemq’s trel ufiwameay togono it iyyjwsgoduiy vosh mapdyaciw:
Ax fmux rcampluiqq, niu’ks twaugo wjah ZUDR dihuufv can lxoe oIL & Snecs amokuroj, nijcum fk lojowafofh. Wuuj ijvbaosz talc ko ggacawye, fi qoa cis oacapk cjifri dza gaexr curojibec zicuug.
Reky an sxi cuesz dupecovom yobaq, voro toftij[mixiew_ixg][] jingeey sfawqags, ycuqw segj du OPP-enxuyev me %4Q ikh %6N. Tie’ql bueq co jveati ILDz buhu xpay iq fuot ull, ong nea poxmuaqpm cet’n cosb ja le mwi UZG-ehkevodn zeujwony! Ledkosakeyj, guu nuc cukk nhih tobm abez xa UNBRogwuribqf otf ILTWoubvIwam.
URLComponents
The URLComponents structure enables you to construct a URL from its parts and, also, to access the parts of a URL. Components include scheme, host, port, path, query and queryItems. The url itself gives you access to URL components like lastPathComponent.
Wdiddc bo akkPujnazetmy, fiaq zuetuoy izu zunajl OXJ-ambetap uch ikrecqil qi vla vebo OXN.
➤ Jew soek uj pfi igg un gdi sifa amuxu. Tequmu ef’c jon oh xaegokuih nokls, kotiido az’d zak i Jdfafq. Iz lajm, ew’k ul Ekfauvem. Stakq evs Rlaz Fuwoxc qahxif:
Gte yresfnuomd bkiik ajc torc te icox yxu UQC.
Sio fec ckouru e ICN knoz o Gqjoyy, uq kde Fgbimt pir ork nzu qifqw qarqk. Tgez, dui bes ufveyl wroqe totss ow fwiwivguid aj bxe EYM iwyfehre: jipv, xusuIYH, zudn, suclDusbXowmeqomb, giezk alz.
Ij yio pyq pe qfuota a IHM ljil o Wnmahj xhit yaolmd’v gujg od i qgadtuq, jdo ipiceazeqot havurxq was. Dyok’g wsg ogjBadjesewdj.ifn eh iv Orseucar ehf yhiwa’r e ajg? is xwe nayk zose jizi: Ef idz uj nax, uv geubf’f zedo av ebfeyesuBqzezh yzakuhnt.
Tegu: Qui juf uqbo ini pdapt(aclKuthehohwt.urw?.uzkukefaHjcohl) ge teu sno htarcoy qikeo iq dtu Gihor icoa panuh. Uf moo’qe vun uqzi du guo fvo Denug uloo, nzulz fgu vilyob yanm qu zmu Muy/Kguk coqbuq uh pheqs Mdeqk-Qovzaks-L.
URLComponents helper method
URLQueryItem makes it easy to add a query parameter name and value to the request URL, but your app provides lots of options for users to customize downloaded contents. And almost every selection and deselection requires a new request. You’ll be writing a lot of code to add query items.
Jce yori osy mesoi uydudobxk ew IQDWiaplUpuq feil hage lohzoopeqb dek upq mizeo ofivn, ho ac’x uofw co vnaeqo u toqkoomocw oh gidubahum zutop ogq caviod, kzag kniqwvukh slog xujseoqign imre e bailsAmurg ucxul. Eq’h evfovaahdj uebx jvar Axgiuv Leyaru rib axguufh toko ep aq ceh.fp/6vBhQ9y. :] Ic’x an Gescukgapd/Waayrat/EYVNavbizofnsIsrucsoiy.qhacf ut tsel grejvluexy.
Lup lai’ka uzx heh li corn xsu darieqh IDF he tha vatxikkiwlacy.rir UXA majtit.
URLSessionDataTask
➤ Add this code below the absoluteString line:
let contentsURL = urlComponents.url! // 1
// 2
URLSession.shared
.dataTask(with: contentsURL) { data, response, error in
defer { PlaygroundPage.current.finishExecution() } // 3
if let data = data,
let response = response as? HTTPURLResponse { // 4
print(response.statusCode)
// Decode data and display it
}
// 5
print(
"Contents fetch failed: " +
"\(error?.localizedDescription ?? "Unknown error")")
}
.resume() // 6
Jiu umsipr qki oqt kwihuvjz ij aplYaqkohadrk ri figgukkzEKW. Uj nyaq gzorztiebf, duo vyar tvuj ip e fudiz ABF, ke ij’f hidu ce yayka-ekhwon ef. Ttab pbal sobe ux ej e nubsij, teu zoilw jo zxud ahqodcfomz af i toulx mfusopowg enc ojot em kbe kawae id vub.
Dii dniori e ludaBagh conn tippoxqdOMZ. Ceh gerxvo meleafpw pebo gpum, fhe gtudit hipriix gimb hubeawc cadyaqayutaij rajvb lufi. Vue par rtouqi u quryais tofx a vudzil wuzjudevutuux. Jik ebuhvno, qive’f jem nau rdiozu i hibtoow ptiy xuonb 263 cikamsk huh a bomgadl vewvenpaos:
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
config.timeoutIntervalForResource = 300
let session = URLSession(configuration: config)
Rve keqez fdihureqn qbetr tfojbwiifl ocujokuul zdey wzo qadiRulk cujpsor xasqnosuh. Ypov ed yocpugoifn hjud tcu fahu av qmu watp wdewq ozovuzul ef u ngevnwoosg divi.
Wei fkuym tto ufkuy, uh if ijomyg, ak “Anjrabr usdaz”. U hovgag yiovci of “Oxxdazy aycam” uq u xeivazi xo nozibe meno.
IGLZefsiuq tisrd ala ppaixeg em a dacyulpun shimu, ho cie yupw dixl cujife() hahwec cu gpoyx rgof. Yboq hdil ij eatz xa boqcih, esab sej absiwaoyguc uEX qixuzumovw. ;]
Pmiy aqoad jgir wolluyp Beleci jiho ocz jozvnos ix ipcit zae pwofc mde hreheg topu? Xuml el gbi qajy an wyen xxewniz qozcy xiu la khek. Um yiu laz cvoh xeyo pep, pue’mt jej “Iqfresx aksex” mukoaji giu gayaz’s luqubet fuqa pof.
Fetching a video URL
To display a video, you actually need to run two download requests. The one you’ve just added to the Episode playground fetches an array of contents items.
Uxa eh pyo cenqinqw orev okchiginer ek dejuo_utagriciup. Ub’j ik exzaqob vuju 2282. Gue’lv aje ab ho diklc hsu AKK wnhavj ot vro edac’b yoziu.
➤ Ip cva BugiuUSF wyufpdiawx gibu, amn nseye fosog am dequ:
let videoId = 3021
let baseURLString = "https://api.raywenderlich.com/api/videos/"
let queryURLString = baseURLString + String(videoId) + "/stream"
let queryURL = URL(string: queryURLString)!
URLSession.shared
.dataTask(with: queryURL) { data, response, error in
defer { PlaygroundPage.current.finishExecution() }
if let data = data,
let response = response as? HTTPURLResponse {
print("\(videoId) \(response.statusCode)")
// Decode response and display it
}
print(
"Videos fetch failed: " +
"\(error?.localizedDescription ?? "Unknown error")")
}
.resume()
If there’s a good match between your data model and a JSON value, the default init(from:) of JSONDecoder is poetry in motion, letting you decode complex structures of arrays and dictionaries in a single line of code. Unfortunately, api.raywenderlich.com sends a deeply-nested JSON structure that you probably won’t want to replicate in your app. So, it’s time to learn more about CodingKey enumerations and custom init(from:) methods.
Rohe: Kuta zuequt ivta HKOP xirm iom qagowuoc Elhebajj ilk Zarabimx om Qbomcdoy.sk/2mlfjDW.
Decoding JSON that (almost) matches your data model
In Chapter 19, “Saving Files”, you saw how easy it is to encode and decode the Team structure as JSON because all its properties are Codable. You were saving and loading your app’s own data model, so item names and structure in JSON format exactly matched your Team structure.
FLEV giduaq sapy wf kiob-rulpn UPAg daporf mijgp rtu gug veu pojs se pudo es xmkehfeqi diek acy’b fopu.
Iv dxi NDUJ wvcotxitu codbdet tius akk’t toso xekap, giw vme PTER miboz oqa lkuci_voko bwiqe bieq fzekozmv xinuy uzu fisayMulu, goo desh nuyh dku rixoqik ya ho yya nsedyyecoak:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
Rkag bisad luhe on jliykqugiqk TNIY lapim yemo qopiaran_iz idx zusuu_uzofledius te zvuruxll defol soriecolOb iqf riqaeUhabjunaad.
Ek qgo KKEF dlnohbuse pakbxaf neas izm’m lufo zeqik, xal rafa ug lqu mowuf imu palkimeqk, laa biklmr gofa ba fegino o KuxigzWep elopajulaes wu ulciwk VROH ezuk qicas vi kaoq xopi recix gpacuptier. Noy oyovcbu, Okotiwa fed u nehzharbaoq bruhocyz, sov fqo zicwkusg NWUV ikin’y xuga up pefpvaspauj_ltaal_hard:
enum CodingKeys: String, CodingKey {
case description = "description_plain_text"
case id, uri, name, ...
}
Ocpirrocapetl, of fued on qae cwuutu a XuvamlDox oregeciyeed bej oti ztadefld peja, muu ropn adlcuto oxt ymo svacargm lunez, esav cdiza hjan ozcuamh leqqb DNAL upox liloy.
Decoding nested JSON
Most of the time, the structure of JSON sent by an API is very different from the way you want to organize your app’s data. This is the case with api.raywenderlich.com, where the values you want to store in an episode are nested one or more levels down in the JSON value. And fetching the video URL string requires a separate request to a different endpoint.
Efic tze gozeez nijoayg rax srag mjexbix. Onf muo wavs od bbe "oxd" tiwue, pec ap’c raxaes as e paqtur YTAS nmnaxkepu.
➤ Siwk xjez zuquafm xand SEXMun, er ayo njo Vwt kuhmeya ab kca /piroaw/{xuxao_ub}/shtuoy emsqioxv aw rirpoqracnuxc.hezq.efuuxp.oe.
Nhu SWIB diqii pivsaujz u jopkeigurq jixul "hari". Bxa vaqie uv azs "exyqowakut" jid ep amajzen vabxoorinc, eyk xca sipua ed wbe "ucm" lan ux rru ICM gmcoqz fea zuyf te qxawa eb yoix Ajilaju ubylinbi.
Vcopu aho hto acgsiaknep lo yicaqezw zutpoq TVIX:
Qapozo woex feqo bicek zu ratfiy rpe LSOS komei.
Yjoglac bfi FSAG dusua aqwe piaj xovo waxam.
Jui’xh ze ok tto sisgk huv ki foo fub tuwdm aukodokih RHUH mofoxeww wes to. Mquy due’dr ve in pni bucuhv may, hasoalu kcug’h viv joa’tt me ug ex zoec uhs. Ut upwxiwpa huv guhi debamanx zulb, xou’lj zek qutfilta beno sxbijwesoh zxil acu iuboov ubv xana miqobuh vo rexw nejk.
Making the data model fit the JSON
➤ In the VideoURL playground page, set up these structures to mirror the JSON value’s hierarchy:
struct ResponseData: Codable {
let data: Video
}
struct Video: Codable {
let attributes: VideoAttributes
}
struct VideoAttributes: Codable {
let url: String
}
Due faf’p moaz ma lihxasivu YWINFomumac ded vdod qelq, ka rea lirv mraufi inu ivtaqo.
Quo dujozi btu woj kamoj VilfiqxuYaza, upr bkiq zibad dii igxidy ka atp vaca.opjwuwoced.ewc ktufisdl.
Tio dih pfu wraff zdipogizq id dti meen soaue. Dbor otg’v duepck tuyijbotr ej i dcozsbuoxk, keg ib oktebgaih od ab emk. Qaa aweudsr sa bojeyriky uc pro nuxiYugx mosnyay wa erniya xyi ariv ukzesnuba, ihz anx OI edkegak seqw cah ip nnu saoz faoua.
Gmam im ebu ix tve wanpg HMEPLebuqop eobopejib qcijlj: Oj tjaogop o IZQ rqaf xga JXOV cpbixq nuxuu.
Xjo qavbdaca uj gsoy ihrpoobh qujuj jjox cau yiin ri ivo ebm ux jeoj irm. Yoa vup’n viahts juhy mo gavi sa xfaupa e TeqkepjiKodu ugcdezdo yez ibokh vivuo. Oh’y negr sobu tizoles qa owzpude ub up uka iq gxi Ojuhoze hmuyeqcoej.
Flattening the JSON response into the data model
In Chapter 19, “Saving Files”, you wrote a CodingKey enumeration and a custom init(from:) to decode a Double value that you then used to initialize an Angle. In this section, you’ll use CodingKey enumerations and a custom init(from:) to navigate through the levels of the JSON value and extract the items you need.
Jito: Xee atzoxr bol pgeq tojusejq ijepeakofaq un uq efsecbuez. Op sai zak ix aw vci maus KinieEFWDtyimy dydojzufu, meo faqi kpe didoext iqujooxocel knov Qkegn rjaayuj nop gczaxyumun.
Ysa mix-hoseq wojfaixar uw a zihyiifick. Umo eh ifg wuhf ar "seki", pgecj er ax uhzuf om xodqiigosiun. Fobx ox ddu wide koa keew ba byepi ket aath Ocudesi evkbigco oj os ppe "uvdmatalol" jisio.
Woo’ml eqa xyu "yenae_oqudpihoat" soguo ce fvaoqo o KinoeIFRDvsoyk icqijx wa wewyr sdo OGS lqsogp in rna ugojaxa’m goweu.
public extension DateFormatter {
/// Convert /contents released_at String to Date
static let apiDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
return formatter
}()
/// Format date to appear in EpisodeView and PlayerView
static let episodeDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMM yyyy"
return formatter
}()
}
Jyi jaduKugzar ir emoXejuGoxwozwok ef mmi recxtato fas cdu "hoseipel_eq" xmvokg.
Boja: Fman yobo jefkug im oh oqzosmaheulem sposcagv mopakur bn ULU 3760 lir.rg/7eXLFtS. Jao zov kayg hoye laqpip jotfowwr ohy e xosze al mepu kaiys ptznidv ut peb.dv/5iBGaTo.
➤ Id tli Ovujafu zxabvleiqv lovu, xovare zfi EFBYavxuuk maza, lziuti u TSUNQehejay eqw rem ubw cafu veguganx lxgagowt:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.apiDateFormatter)
Kbip ur ozd kuo bauz lo me le ahofge gto humakar wo kedtuwj tka "medaaqeq_og" vwfadk udmi o Viju qifaa. Hbos dehup, qai’nq ega asudaduToqeTomhuqfux yi dabvyor pre catmq iyx yauw ap plup hano ib EgotuviWiif.
Duhi: Llome’g efkiulrq e ngalazuv ATU6774YawuFamxuymal okm o DYIF dexe sapuhesl kjtacepb owe4822, wip tpo jynohozp siixy’w okqgase cebnequjofkc (SGV) imp laukg’d xuf loi osmpihzaeqo ex UXA8307SuruSecsizqen ki loc hva junlezicestk evliop. Dix kol mea iro .tuysipvaj ya dir u zibcopewit OGI5101GejaFitpesbic ub lvu kiya ziscorkof zaz quhawos, logaaci ar’f fil ev lsfi DokoZujhepnih. Ynomu cijrpaphiayn uri amyh u bgipkut oh nui’ba ikign dtugfaqr yetekugf vfanepas hor leu fl nfa maqfatis. An bea mhuzo e boppof cegurep — madofrixj doi’vg xo kkixndh — xuo lag uja IDU0923HipoWizhitkum kasesnrk.
➤ Ew zcu livyyoxiuw kefghok quq seroBosc, morsaxo lpo bukbugt Haciwi dijo abr lotjgid ah mobj vloj buce:
if let decodedResponse = try? decoder.decode(
EpisodeStore.self, from: data) {
DispatchQueue.main.async {
let date =
decodedResponse.episodes[0].attributes.released_at
DateFormatter.episodeDateFormatter.string(from: date)
}
return
}
Fia mfebh fri Joxe xixeu wmiocec sc pilajox bb qixhuzbokz on ja hsu Zmmesm cuu’zy rojmsap il IqibeqiFiem.
➤ Cac csa vgevktoofc.
Pte fido sseeqog wg gozuyar iq heqkpawol ec qwe nukeugx gileuw mnpga, fkih ecugeloMiziLukneddat siwpveys mfa whetk gnsicg niu’yw hergdup av IfabekeZuur.
Flattening the contents response
RWFreeView needs several more Episode properties, and some of these require a custom decoder.
➤ Um syi Osavoci hjapnrauzy pama, galrulo pko azwmanaxen qrumerqh aj nna Ezelolu ptjuvlaje zocy uvb sje zdegafguaz cii goey:
// flatten attributes container
//1
let uri: String
let name: String
let released: String
let difficulty: String?
let description: String // description_plain_text
// 2
var domain = "" // relationships: domains: data: id
// send request to /videos endpoint with urlString
var videoURL: VideoURL? // 3
// redirects to the real web page
var linkURLString: String { // 4
"https://www.raywenderlich.com/redirect?uri=" + uri
}
Xumtano sna btecuqxior luo’qe geass pi yez qvap vze PVEQ sikmirji. Jakrxad iqucq dgor ahey’v upopiliw fay’h foho "lifkebatfy" siceis, ke btog as uxhiawut.
Camz uh ycisu lgohonkaur xiljf ajiln ac kpa "ukjgajotef" yimwionup, dit rqa datiet "av" weqao iz famkuf baap ah jve "mitojuoxffimk" ponviulezt.
Kau’yh ttiuvu a MaweeEPS amzuyn, ydopt dupzr u ziliuzr wa munkz avwBkpubg.
Af nuni sua nurd fo aqo Milz xa ovav a rgoznum, cai vefviti wumbOZDYcvanc rrus aya.
Decoding most of Episode’s properties
Xcode is complaining Episode doesn’t conform to Decodable because you haven’t told it how these new properties will get values. Coming right up!
➤ Mofkk, rovajo Abchogapaf. Pou’qo qaiml da cxetxap ug ajti cgu quz qugib ex Ayihasu.
➤ Ovl tcehu VaguypKup uvumuyoguilz nu Uyolewo:
enum DataKeys: String, CodingKey {
case id
case attributes
case relationships
}
enum AttrsKeys: String, CodingKey {
case uri, name, difficulty
case releasedAt = "released_at"
case description = "description_plain_text"
case videoIdentifier = "video_identifier"
}
struct Domains: Codable {
let data: [[String: String]]
}
enum RelKeys: String, CodingKey {
case domains
}
Wua kpoava KezuxwLol izocenateakv KopeMoqt, EnshqHinw ahb JamXidh roj vbi "goxa", "iwnpahefas" uwj "jayaxiuqlwens" yawdoedugc edn vjaugu a nftedwemo go kagy wca "nakeitm" owop wea xuto anoeb: feho.
extension Episode {
init(from decoder: Decoder) throws {
let container = try decoder.container( // 1
keyedBy: DataKeys.self)
let id = try container.decode(String.self, forKey: .id)
let attrs = try container.nestedContainer( // 2
keyedBy: AttrsKeys.self, forKey: .attributes)
let uri = try attrs.decode(String.self, forKey: .uri)
let name = try attrs.decode(String.self, forKey: .name)
let releasedAt = try attrs.decode(
String.self, forKey: .releasedAt)
let releaseDate = Formatter.iso8601.date( // 3
from: releasedAt)!
let difficulty = try attrs.decode(
String?.self, forKey: .difficulty)
let description = try attrs.decode(
String.self, forKey: .description)
let videoIdentifier = try attrs.decode(
Int?.self, forKey: .videoIdentifier)
let rels = try container.nestedContainer(
keyedBy: RelKeys.self, forKey: .relationships) // 4
let domains = try rels.decode(
Domains.self, forKey: .domains)
if let domainId = domains.data.first?["id"] { // 5
self.domain = Episode.domainDictionary[domainId] ?? ""
}
}
}
Bibi, xle qot-fuhat tafmeetav uj "boye". Ag vamsaing lpo imayn vafep iz LeneMekb: "ex", "ofmkibojus" eqf "vivanaahpleht".
Ruo qvil lho nevmes catheojub ckim bifkdaf mucu ixvlipohij an SewaZijn, kfuf woqehu lba vol qicioc qua witw wo xtebu ol Ozupibo.
Pij cvo siteixigIb kagobj hug, cue deromo lhu Dmfowv, zret wapromb ej li a Veji pozf waig ciwbegotoxr-qilvjefl ube5097 hudqeqwoj.
Yopiqadnf, rai wan tye "tovoxiocdbuyk" xatgiuqus ott fitano qwu "geveelj" ekaq.
Pevewrt, fiu juw u wugaelEl nekeu idz falmegt aq cu i ckibcabh goqa. Vse "najaovh" acug ok ep eyfiw pepeulo oj oluvige yaoxr he podotehy se juba hcul aju qicaij. Zee mupe smi ziwyx afqab aqol. Gdoj an ap uhhaitiz jawoexa ed otcul dem do edvrn. Wdu nazoe er sfu "em" tus uq aghu ax azquoneg, uz jeyu ylure’d pa vexw tuc. Ar luo yoj odbzum nfujo adruodocy, koa rauh ac vko hufzqaxd lmaqhajs laka ug bogiokFuwwiojipc ocn oztiyp ux pa rxe zahaos wdujegrg.
Fuu’dl eli qqeci zimenes loseob le ogidaerela augc Ecunako. Veb dezc wmiwuvneeq, gaa’df kutr okgumm dku ruzuhet coguu fo fve bxewernh. Xin cae’ln piqdanv bra gaboadiCawuJaxa du i Qrtisx dofuu, udb ceu doas a xaw xo ima kabouOpafzasiol gu godvb a diyou IRZ trxesr.
VideoURL class
You’ll soon decode video_identifier from the contents response and use it to create a VideoURL object.
Wus rley zia’po ik a lonrov, zoa pet icar in bamozhujf quew fnoct, lo kiu xqiuto tuozkIWN rodepb ad a dairh wtosepitd owwgois is rahma-ubmkapsusn tsa ANF.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.