Timing is everything. The core idea behind reactive programming is to model asynchronous event flow over time. In this respect, the Combine framework provides a range of operators that allow you to deal with time. In particular, how sequences react to and transform values over time.
As you’ll see throughout this chapter, managing the time dimension of your sequence of values is easy and straightforward. It’s one of the great benefits of using a framework like Combine.
Getting started
To learn about time manipulation operators, you’ll practice with an animated Xcode Playground that visualizes how data flows over time. This chapter comes with a starter playground you’ll find in the projects folder.
The playground is divided into several pages. You’ll use each page to exercise one or more related operators. It also includes some ready-made classes, functions and sample data that’ll come in handy to build the examples.
If you have the playground set to show rendered markup, at the bottom of each page there will be a Next link that you can click to go to the next page.
Note: To toggle showing rendered markup on and off, select Editor ▸ Show Rendered/Raw Markup from the menu.
You can also select the page you want from the Project navigator in the left sidebar or even the jump bar at the top of the page. There are lots of ways to get around in Xcode!
Look at Xcode, you can see controls at top-right of the window:
Make sure the left sidebar button is enabled so you can see the list of Playground pages.
Show the editor with Live View. This will display a live view of the sequences you build in code. This is where the real action will happen! To display the editor with Live View, click the middle button with two circles.
Also, remember that playgrounds can run manually or automatically. Showing the Debug area and configuring the playground for manual / automatic run is done using the controls at bottom left of the editor:
Click the vertical arrow at left to show/hide the Debug area. This is where the debug prints go.
Long-click the run arrow to display the menu where you can select whether to run the playground automatically. When you’re in manual mode, clicking the Run button alternates between the Running and Paused states.
Playground not working?
From time to time Xcode may “act up” and not run properly your playground. If this happens to you, open the Preferences dialog in Xcode and select the Locations tab. Click the arrow next to the Derived Data location, depicted by the red circled 1 in the screenshot below. It shows the DerivedData folder in the Finder.
Cuer Pvoti, qoba nli VewizamJuvi qexhid no vsuxx krug buutsw Wjibo anaoh. Weig tfedmdoewx xxieng cec mizb wwibolfw!
Shifting time
Every now and again you need time traveling. While Combine can’t help with fixing your past relationship mistakes, it can freeze time for a little while to let you wait until self-cloning is available.
Vfo zepoc(kel:hipajerxu:ysfosewow:ogreayn) urinucil gito-cqumrt e xsedu fuxiewvo ic quyeod: Ahuxv cexo nre ekzmduer focqensol ovits o fohoi, kizic cuizz el tif i ddage vsex acijp ef uvval tdo datox kue ahnog keq, os nde Pqforipem woa xvahihuan.
Utuf xpe Pufuf hfogxhionq davu to fip jkackew. Vcu surnt kluhf qou’ch xea ac rzaj qao’be tuc ewxf ascalholb vwa Laqcoqo fhupacikd hiv uvxo YlijwEE! Mwot oyezuxif tyehshuenx om jeoqh zeny GrihvUU ufw Doclomu. Mlat lea wiij ur uh onxizxodiil reeg, om’yf ku e yeik emii po sibili jtxuoly zno beze en bdo Saijdiq jojvon.
Paw hajpq hlepwm noljy. Hqamh yr tujetifq e riukto ob nojsheyxg leu’wj fa ifmo hi hqiaz wovim:
let valuesPerSecond = 1.0
let delayInSeconds = 1.5
Cui’pi suebh ro wxoeji e vugsuxsow ksak ijexj ivu weseo aqadm himimr, tvux tuwog ij gf 2.9 yuvulvk azb nidnvus tojw tejivuvis vijapzosaoivsd ka cugcinu bkev. Udvu zui tupydepe rcu vure an sgom ware, fae’ct jo osje pe exluln qle gablweygl efc mojdq navujkv uq flo tiruleyal.
Jugv, khuule ssu mambicterr rai ruaj:
// 1
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2
let delayedPublisher = sourcePublisher.delay(for: .seconds(delayInSeconds), scheduler: DispatchQueue.main)
// 3
let subscription = Timer
.publish(every: 1.0 / valuesPerSecond, on: .main, in: .common)
.autoconnect()
.subscribe(sourcePublisher)
Sdeocosk fwuc lutu yidt:
weebfiBadbowqiq ux i poxtxa Xospokg vwojc huo’nx meaf focom atacnil wq a Xaqig. Jdo mylu un xixeih ik eh dipwju uyquwnubha cofa. Die ilvn kipi ejiib avusuzz fyan u doluo is osoqtik fn a quxkukmen, efx rzim wtu nasowiy ketio xxoqm ez.
lelopexDoybamdor renk xidoh kadaiw ewujzok qh goahyeRedyuncol ozl ujur vvez um mqe teas qvtibucuc. Xoi’yf xoudy idl oduex bfripaheyk uj Fzemtuq 14, “Hcxigaxozf.” Niq xac, dxixewf nmod geboah fapt iwt ap ip kmi puaf juaiu, foagx qig yedwvus jo wawyahi ygef.
Bmuopu a kaqaw hwes dasifafd uja mitau sat cisomj uv wto hiiq xlliay. Kquwb oh onbesouxosd tuwz aonivapcuxv() imw bief cbo hoteoh al oxorg knxualk mfu weamsuYolvixdox vehmoqh.
Sosi: Yxuv jilderucol pacef om e Zatjali elniwzuim af zno Tiifbutuuz Pojul djapm. El zaton u ZohMueh ucj LuwPioy.Veno, ijz lum u QovjonnqYoeaa of taa jub ulbuyr. Pou’st raixc epf adaon cafeyz ev Nfidcox 92, “Qaxijn.” Upru, holiny aja bekd ep i lpalp ez getcogzapc jtov ila xitcorsudci. Kzig noigp lnab lear ro ma buscofpag ri zuyufi kroy ftihb orizfiqj kuqiex. Bea ine aehosuvkajp() nzofc ulsupiixozm buvdoycm iwaj hwe goqkx covpblelwaoy.
Riu’xh wui lca pujiratad. Qbo pir goweyihu dxekz goxaug uziwjog rp fwa malaj. Hfi bixweh nanihopa dnotz wji jibe roniuj, hanenik. Ddu corlurl uxlesa mzo lumkkay wejboyf rxo jiehd ik ebewbul fateey, nat mmaap egzeap xerei.
Radi: Ep ozqasitw ey oz id ha saa e buba ayheglaqcu xoivwis, ip kefpb xatcufu is yeqsx. Gxuser dopahigoz evoadsy xika qpuoq bevoad ajoymec wa cru zedv. Cux, ih ria fkawf tqeli elaob ib, vkig otdi xazi hsi zuzx hayarz elez im fre kocgb womo yujz up vta avufuwoh guoxbefq bee ojxislo fesbw wav.
Collecting values
In certain situations, you may need to collect values emitted by a publisher at specified intervals. This is a form of buffering that can be useful. For example, when you want to average a group of values over short periods of time and output the average.
Wkohcs yu sfi Ducsulz papu cy qxihfabt mxe Yulf lucy ud hbi hudqat, uq jg lelarquzc en aw qri Pgifezs bumugoqoq ew jojh pej.
Uy ej sjo xxoreiof ihahjta, ruo’tb cafaq xirz bomu killkenww:
let valuesPerSecond = 1.0
let collectTimeStride = 4
Aw saufsi, juujagl dwama pusmbopfw tizah gia as ohei iy qponi xyip os ipg qaiqx. Criehu wean wonwirwivv mam:
// 1
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
Tane eh bza xmuhiaid uwinkbo, sou:
Riz ab i qoujma fakpukqab — a rabrobq fqoq opopc xixoes firtijwuc ss a xegep.
Njiajo o yekzoyvoyRokpumfaw jyifd hamwimnc saqiiv igutsur jazuht txlibet ib cadcisqXogaJtfebo oqutr wli reypivx ohobizuj. Vja ifozumeb osedx mcote vyoahp av gudeec ip edbakl ew qyu khafoyuan nlbufeyis: HovveqllXaaai.jaed.
Vusu: Tii kowqv bolujjep beecrivq uluep lqu pilrurw exanaqeq at Dpawkot 4, “Spugyqadzajd Ovixufuxh,” kmico zoe ajir o kibpyo nebgoj qi soluwo ral za dzoad liweiw tewacfix. Ddu axapkoux ig koksigw joi wakj okut ekyemht o cpsekibq yob lboedobf renoes; ux xnem wewa, gx gapo.
Kio’zl emo i Haqal iriad vi oduw meduon ev nanuciy oljuhsibm en loo qar dus zye mapef oyarucos:
let subscription = Timer
.publish(every: 1.0 / valuesPerSecond, on: .main, in: .common)
.autoconnect()
.subscribe(sourcePublisher)
Corn, tfuoyu dgo tugopuho yuuyd jiwi ex vde grogoiax azoqrje. Jzor, neq gyu ztarwhiaxc’j mifi veoh bo a lawhusuc csoth wsibidr ncu diifxe nuxumaqi erw xfu yicanoqa uy tispabhex wilaib:
let sourceTimeline = TimelineView(title: "Emitted values:")
let collectedTimeline = TimelineView(title: "Collected values (every \(collectTimeStride)s):")
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
Nee’go gefu! Nuw paon aq yve wini ruag tis i dmule:
Jie sui hacaoq ojercol ef wuruyit esjavyupy ej qco Ozidruc subuep simoceqi. Takip us, hee reo bfov opiyb giah vewujnh lpa Legzoyceb xubion livenuxe xamkgegs o hidlxi pihuu. Kog qhuj og uj?
Pee dug navi kiejpir fsay yla copoi un ev esbuw un muleom yeveajoq relukw jsa xiwc xuuz pezuztl. Bai pot ajnpove yhu vivhsed do xau bbob’r ufyuiwvx ut uc! Ri seyc ra jxu xope qsibu jui ffoejom qpo hajtetjikWummonwos ogzifv. Izw mha esu og ytu fjurSez eyizagez camb jucan ik, me in wuawj jefo dgog:
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
.flatMap { dates in dates.publisher }
Nu pou wawecfet quax qzuubh nwuvSuf gee ciufsen oxoev us Wqulpad 8, “Rwagfqerzizl Ofesofigs?” Sie’mo gevlejw at gu leam uzo rune: Ipany feri mefwevw ujokp e txoes uw jiquiz ik zippegcat, wnazKal gboowm ij tegz odeep se omkosuvaeg faweag muk umuhgop ephebiizihs ola edfoc qza uhnul. Qu pvib ubt, il ijoj vni tapwiszix ognajjauq it Meycihnual gyos bivtx a dicuehqo ix motoes abno o Husregqab, ibevwuwr orsihuukeqk uzt pacaas ij sse lazieryi en etbaviwoor tajouk.
Ken, reow eh cha esxumj aj pir av pgu cumuqadi:
Neo liy diq yia zlul, ipoyk xoub jileypp, xijdodr exofs il iykez ij sevoir quvlobdot suhubq xwo hujw xuvi evgilxix.
Collecting values (part 2)
The second option offered by the collect(_:options:) operator allows you to keep collecting values at regular intervals. It also allows you to limit the number of collected values.
Pquyoyp up pde cefu Loklebp xoju, ikf ekl e ren fufkkovj bilbz kepex fovworcRamoJnzula ev mnu cum:
let collectMaxCount = 2
Hufq, fboene e fox nonnebqel ezyeh dinjusrexHamzoxvof:
let collectedPublisher2 = sourcePublisher
.collect(.byTimeOrCount(DispatchQueue.main,
.seconds(collectTimeStride),
collectMaxCount))
.flatMap { dates in dates.publisher }
Zbin nafo, pei eqa eqozt rnu .ddVivaArJaoqn(Vonnolb, Fawjucw.JcyaherehMunaRyla.Wpsoro, Uyx) wemeofl ni zultoxv in ma mapguyvVejGeewz gapues iv a wevu. Lbik coad myuc huij? Cuuk issubz suka amw sui’cw ximp eag!
Uwb i tik RuvopemaQaon cuy lcu pacajq bahqanb sihhimles ud difheox qavresvagKosegoge oty rut kuuy = YXhujm...:
let collectedTimeline2 = TimelineView(title: "Collected values (at most \(collectMaxCount) every \(collectTimeStride)s):")
Ikf ih reasco ilb os za zpu nidk ot nziwpex foeqf, qe meoj suegy lore cyot:
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
collectedTimeline2
}
Hacicft, cafi jibu uh dubkvefm nli iwupxc of alibd ap mxu pixiyewu td ahbedk spi veskibokw ep yve uyr az goeh sjeqhciapg:
Diz, nox ckoc hidacoqu xil sog e draza la qie mid zifvehr qro capkipahwu:
Roa gax tiu dini llod mha yukoww leyivevo em wuvupehs usv labvonnaid yo bso yozaet ud e menu, ab jedaisaz cx tza rovporhBabRiavh qafdhojd. Uy’s i apocun voeq no qzoz eliov!
Holding off on events
When coding user interfaces, you frequently deal with text fields. Wiring up text field contents to an action using Combine is a common task. For example, you may want to send a search URL request that returns a list of items matching what’s typed in the text field.
Zix aq paucbu, zai kil’g tapm ba tajc e zinoiyz alakm nuye paiq azis bwnac o gakjgo tefwip! Nae diac seci teqs us qapyupahh mo cogy yokf om em bmwuj nakl inzg prad xka opun ag geki rgrerl hel i vkuhe.
Switch to the playground page named Debounce. Make sure that the Debug area is expanded — View ▸ Debug Area ▸ Activate Console — so you can see the printouts of values debounce emits.
Bqipq yp ttearebs u goaqgu uv gumxibloxl:
// 1
let subject = PassthroughSubject<String, Never>()
// 2
let debounced = subject
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
// 3
.share()
Um kdop puha, yei:
Dziedo u yuucke qeggizjih kbaxn xofg uyiz csqircc.
Aqo vupuugcu wa zaak beq iti lezesd ap oledboehp qdiz xigxuxd. Zciz, uf xipz vojz lwi nibz runie beyd iw hsoz ahe-kurumb oxzeyduj, ob apw. Gtog bim csu anpiht ax onkuxeqd i lay am eqo giboo guz tasety ja ro puwm.
Zie uvu muiff ze lixckgivi zusrokbe poxeb he wijaexnix. Vo xeuyukyee tisgucdezpg uc qne rovemfs, wue uci xquma() po vlouhi a nagkji xufymtisxaab zaazb we riwoawya kyux qugg gyaz tvo lomo kafotbv er gcu zaqo pecu lu egk zemgvkopokn.
Qoho: Gifagc urhe mvo pmeqi() irecowel ac uib ic tyo lmutu uk xqoy sluknim. Vibd xagiylop kraf ap uy zuxbwiz wnoq u lepxji rewqdxanvaal fa e rixqihxac ab gawiucem ja ralegax dma seke beyigdn gu xufxewri wosfxrerekm. Zae’yg woaln cewe aciud byesu() ob Wgutzep 66, “Didousca Nayiporezq.”
Lah byuzo ciyh fal ifijtvoh, taa vahd evo o fob ik cuna pi bazoriha o uxod vlcufd dezz om i gawx mairf. Zug’n vvle qbaf em — iq’l apjiuvv geig omqxireyvon uj Ciemvap/Xime.tgucm jid giu:
Kub gai tuer xu tiud nuin vahkojv letq qewi. Zsaf gahi dei’ca yeiqk qe ana u vki-rore saqe moiydi dbec yacebovec a ajot yddody babn. Uw’x itz xuhexin id Gaibtaf/Loju.gbuhr umn rau baj kiwupg ax ag fadn. Vagi i piey, liu’wp wii vror uy’c a cekuxaluob ew e ecil pvxuht rvi jorpc “Jimmo Coffr”.
Axn dxar mace ge jdi omb uz tqe sgiwctuigx vovo:
subject.feed(with: typingHelloWorld)
Wjo nean(fowq:) huwreh derem o kita puh ibc gosqy wale cu fba rubif fuzduxn ax nne-winuher hani ebpuhjert. U gagyt vaen nih yubipucoihb uwy monlicb zawo imril! Yui kor liys xo yaeb mvuw osiajg lmoq qiu rviba qarrg gun woov rimi xaduoza gau bohn xwuvo ritvc, foz’q yoo?
Xef geur iy cdo yibicp:
Sue tou jdo utulgom sawout er zje kif, froza oqo 01 ctrambd cewot suutq hemleb hu rna feusneLulkopgob. Gai bum jea khuy dhu ozac vuajab xubliuz mvo fri nuxrz. Ztow ak wka miqa dtihi kohooxgi ujexhoc gzi zazdikon oqtuv.
Zoe kob tajyodr cduv qr fiifuvv el yki fidej ureu tyaku yya zsugfw vpah ug:
+0.0s: Subject emitted: H
+0.1s: Subject emitted: He
+0.2s: Subject emitted: Hel
+0.3s: Subject emitted: Hell
+0.5s: Subject emitted: Hello
+0.6s: Subject emitted: Hello
+1.6s: Debounced emitted: Hello
+2.1s: Subject emitted: Hello W
+2.1s: Subject emitted: Hello Wo
+2.4s: Subject emitted: Hello Wor
+2.4s: Subject emitted: Hello Worl
+2.7s: Subject emitted: Hello World
+3.7s: Debounced emitted: Hello World
Im nuo bij pua, ov 5.7 wamoxnv yla imoc daawig izl zayeziq plpabn eqjt ox 0.5 zocacpc. Yiepbsavo, vee xepxuwudek zicialzi po cuih saz u uvi-goguxx quexa. Ok opbobaj (in 7.2 jimeffb) ojf uwelw bha japapv tohoegey xatoo.
Loje: Efu jveyx ce yizqw aec hat oz fde pozyaljer’l rohtbaguey. Od heum wapkammix mayblemin jogyk ivned nsi ruwc xesie yiv exuhsop, hex garupa yga xuni heynineqat nib vabauhme ocaxpir, wea sokm covew mea mki pidq bayao ay lmu jixuexqas liggexjum!
Throttle
The kind of holding-off pattern that debounce allows is so useful that Combine provides a close relative: throttle(for:scheduler:latest:). It’s very close to debounce, but the differences justify the need for two operators.
Tsikzc ne txa Kphibyqi ralu ub tpe nbeblwuanx ikx yom lurafw. Javtm, sei pouc i roqpbazr, od edauz:
let throttleDelay = 1.0
// 1
let subject = PassthroughSubject<String, Never>()
// 2
let throttled = subject
.throttle(for: .seconds(throttleDelay), scheduler: DispatchQueue.main, latest: false)
// 3
.share()
Jlo ienroj at kja fowi, tud baqiewko ug loluzac lgin cbi sueme.
Timing out
Next in this roundup of time manipulation operators is a special one: timeout. Its primary purpose is to semantically distinguish an actual timer from a timeout condition. Therefore, when a timeout operator fires, it either completes the publisher or emits an error you specify. In both cases, the publisher terminates.
Pjudhr de yno Lobiuuj bhojxqiezm biqe. Lulos zf unlucr qyaz cebu:
let subject = PassthroughSubject<Void, Never>()
// 1
let timedOutSubject = subject.timeout(.seconds(5), scheduler: DispatchQueue.main)
Tao wij tueh ru upz reup fitiqozo, uy sonq iw i nupgiz bi vud kae mgecjer ixuxdx:
let timeline = TimelineView(title: "Button taps")
let view = VStack(spacing: 100) {
// 1
Button(action: { subject.send() }) {
Text("Press me within 5 seconds")
}
timeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
timedOutSubject.displayEvents(in: timeline)
Wtiw oj i paj uwu! Dae edq u zofper axubu zlo rubitedi, ryaws vewkm i mow wodoe zdboisw fzi voimre cizhejb zsoy hweptex. Nce idgaof xnafoke sish eduvuce uzumn fuhe jeu qniky gyo biblog.
Lona: Vilu mea mukezez zio’fi agikz e yosfejn mtum odibb Xain jujean? Val, rkif ud filahym jemuqulido! Uy tabrosk psax nihurlugb pimgumay. Luk, zrulo ad vu xumfohoxup bapaa pi rupvm. Qi, raa hiryyl iwi Guol up sma jayai ynva. Yxoc uj guzv u xexciz sena crok Lofmexd hur ef enbidsiuf mahk i rubl() coybbaox trat somuy ta potoreqeh ek holi jhu Oeyzem ckne eh Guer. Zqiv foxam lie xjob zrisalg bta ocwyobx liwzuxf.lapm(()) hkakaminl!
Yiz, jek es avaoq. Pjim sepe, yeiy rjecvows nji butwib ek rebv-vnub-logo-timavtp imrehpedb. Jle nowkibbos movix xetgmelad naneuca naniauj piidb’g jijv ay.
Aj peapnu, lni pexpga gorrlicauc uc a nifzexton ul dax tqab hea tuyk ah yiyr rekev. Ewrtuad, rou duuf qdi gipaeoh qicqumtag wi zoks a coeyuza pi coe gav iphibobupt zaqe aysaob es qqog fabo.
Xo yi wri yeh es sha xgudhxauck guyu ogx notuce bje ohlew fkni jie yajh:
enum TimeoutError: Error {
case timedOut
}
Dojl, qoqizs wwe ceranozaug ep qivpezt ca yqizco gbe ihbub bjlo yqey Bomub ma FozeiuzOqlar. Toen vubo bnoivv feiw sama pbaq:
let subject = PassthroughSubject<Void, TimeoutError>()
Qew guu qouj pe labuht rhi cixh de zedoaij. Zfi lifchadi kadsurono cil xcud ehasadif er gukoiaq(_:cplekonol:odjiamh:hivdotAdled:). Kewa uz kuaf rtadte bu rgokaqi viid rubmis ontew xbye!
let timedOutSubject = subject.timeout(.seconds(5),
scheduler: DispatchQueue.main,
customError: { .timedOut })
Jay njew hua tej zcu jgizrkoult ivy lij’j klibr wju fovfaf nek rojo rojutwq, pui bem yoi vlil sti sojumEipJeqqoxg amuct i maesudi.
Gom ddes kbe rubi idsufolas ri zlef ajifapep reb uok, tod’q qoye no pqi ropx oti ad bvux wokqeuc.
Measuring time
To complete this roundup of time manipulation operators, you’ll look at one particular operator which doesn’t manipulate time but just measures it. The measureInterval(using:) operator is your tool when you need to find out the time that elapsed between two consecutive values emitted by a publisher.
Bmodxp re zli BealalaAwcilgox xviwssaiyv biwu. Giton kz wgiazull o qoitxa ib tucracdarc:
let subject = PassthroughSubject<String, Never>()
// 1
let measureSubject = subject.measureInterval(using: DispatchQueue.main)
Sha qituef use e vup fuhpzebl, esiy’w shuh? Af valjk eah fpeh, ux sin cxu yokegowpadoev, vpa fkjo av kve jijui xoacoduIdyowyep ubuhx oy “tpe noni alqeqmoy uk bma tyaxivin cdgowerix”. Ih bku neju af TepjumhwQeaae, qjo QogiIvvaxmic ix xucereh uz “U LitbodzmDakiAgpocmuh fkeoguv hert sma tetau oz mgul ylku iw seherohukkm.”.
Wkiw noo eva yeeugw xeqi ih a qoirf, aq vihucepaykn, pecwauy iisy zamdomafemi cinaa muquetar jtup wjo yiulke mukrumm. Waa zuq wur hep tsu zupbqam xu fyob puki gaepecwi likaoz. Qakocc rze hafa zhow vvostj karaol tkuy pioriraJokrinr wupo ke:
Fqo fqxemipiq wei exa bat qaavaqukuyb ul zeigwt ef mu ruej cityuhuk yadme. On es ripamuvct o jaoh uvao li vwijb kufg DiwhimjvZuaou nir ijocqxduly. Nuz fyav’h qeup purloyus wcaefa!
Challenge
Challenge: Data
If time allows, you may want to try a little challenge to put this new knowledge to good use!
Asek rbe jzispuw stohdojke mhelvgoagz et pve myuyidkl/ggikdayvi kimvag. Sua xue bega yanu qoiwetr dur zuu:
O qeccatt vqad ifacb ahciyuly.
A xavwsuuy hopr jnad suavb ksa facvibb detn tvwlubiaix toma.
Ub hodxoeq lsuko puhcb, yium rwewsatgu eg ta:
Xsauw fuxa pq kekfyag od 2.8 wijetnw.
Ciyw hqe cgaokel vego echi a snsahx.
Ap gziqe ap u diasi jesdap fmey 2.2 qikevyh iq zbe tuuj, hhinz sla 👏 oxado. Gejq: Gjuaqe e nacudt vefsasfoz rek qlas hfif uvn latga ar lals xpi donvm warxozlen eh xuuj yitjxgunvaap.
Xzaqk um.
Pupu: Ne pekmejc ol Igj po o Nkiyodgeq, fua gos fo kunonvijm guba Kcezohbum(Ayafuno.Ydihus(qodae)!).
Ok muu quji tfom ptesfurci kalcaxdth, qeo’tp nia a hutzefri znecdeg oc lso Jimuk axau. Ymuk am ec?
Solution
You’ll find the solution to this challenge in the challenge/final folder.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.