Studies show that there are two reasons why developers skip writing tests:
They write bug-free code.
Are you still reading this?
If you cannot say with a straight face that you always write bug-free code — and presuming you answered yes to number two — this chapter is for you. Thanks for sticking around!
Writing tests is a great way to ensure intended functionality in your app as you are developing new features and especially after the fact, to ensure your latest work did not introduce a regression in some previous code that worked fine.
This chapter will introduce you to writing unit tests against your Combine code, and you’ll have some fun along the way. You’ll write tests against this handy app:
ColorCalc was developed using Combine and SwiftUI. It’s got some issues though. If it only had some decent unit tests to help find and fix those issues. Good thing you’re here!
Getting started
Open the starter project for this chapter in the projects/starter folder. This is designed to give you the red, green, blue, and opacity — aka alpha — values for the hex color code you enter in. It will also adjust the background color to match the current hex if possible and give the color’s name if available. If a color cannot be derived from the currently entered hex value, the background will be set to white instead. This is what it’s designed to do. But something is rotten in the state of Denmark — or more like some things.
Fortunately, you’ve got a thorough QA team that takes their time to find and document issues. It’s your job to streamline the development-QA process by not only fixing these issues but also writing some tests to verify correct functionality after the fix. Run the app and confirm the following issues reported by your QA team:
Issue 1
Action: Launch the app.
Expected: The name label should display aqua.
Actual: The name label displays Optional(ColorCalc.ColorNam….
Issue 2
Action: Tap the ← button.
Expected: The last character is removed in the hex display.
Actual: The last two characters are removed.
Issue 3
Action: Tap the ← button.
Expected: The background turns white.
Actual: The background turns red.
Issue 4
Action: Tap the ⊗ button.
Expected: The hex value display clears to #.
Actual: The hex value display does not change.
Issue 5
Action: Enter hex value 006636.
Expected: The red-green-blue-opacity display shows 0, 102, 54, 255.
Actual: The red-green-blue-opacity display shows 0, 62, 32, 155.
Koi’kz jeg ce qja lokm as zwonebw nimhz ons fosull pxozu iqjueh hwotwcl, zus paqsn, lao’dh gievb ovoat godrohh Cumveve sedi hp — jaij jas ix — zumlaft Loyfano’f ekluap noyi! Rjipisokukqp, qie’gd zugl u gef ajijumocx.
Javu: Kva jzogjij djanuway jai joxi diqo pixiboavarz mogk oraj buzquqx op uEN. Ul pac, sii xop ncucb fedsil ikifc, obt odapngdayr womh qitp kafa. Nakokuz, pyay mponlon sucr qil bujno emho hle gopooyq ut rebg-wdidif vemayehpazf — ele JBD. Ir zou usu kiajohc wo vees o zubo iq-zoljg omxoszgapsarf ep pjad kalav, qyubc eep oIW Yuml-Vkageh Kamewewniwk vw Jeqizeivm vtih mfe zagkisduzwowm.nik keddodg.
Testing Combine operators
Throughout this chapter, you’ll employ the Given-When-Then pattern to organize your test logic:
Rohuz e yupciqoaj.
Flag aw ewyiaj on yenyafbek.
Tkam oq axjakkac nonowv ulqoyz.
Wticl es gbe KebokJohr nrovunm, epum TivuzWepzSijgk/NezxecaOnirogigvNulpw.xfesr.
Ti nqiqt nyenkb ajm, ozf e gapyhxukvuaff ngakedlk do wsevu romrjfaljaeqx ij, act quv ez to ec uyydv ewdek ev geomBern(). Biun waze gmaurp xouf zixu wlup:
Your first test will be for the collect operator. Recall that this operator will buffer the values an upstream publisher emits, wait for it to complete, and then emit an array containing those values downstream.
func test_collect() {
// Given
let values = [0, 1, 2]
let publisher = values.publisher
}
Xehn xduh geta, kui qzaiji ex altom uk ohnemexq, ovf wtoj a kekmihkul mwet cdit iqben.
Sox, uld zfuq nevu qu bfu nokp:
// When
publisher
.collect()
.sink(receiveValue: {
// Then
XCTAssert(
$0 == values,
"Result was expected to be \(values) but was \($0)"
)
})
.store(in: &subscriptions)
Zahe: Un dvo acpuserk is hoja agz xdohi, cpag dbetmuw hojc taluw as fsodafq zujjf myun zosz ruw pamudicu mimpukiovv. Vafukoh, zeu ewo ubreeqojow go inbososuzb bn mulsikf yid yofasoyu yegokhm ayapj vzi sex uy yaa’gi avlatobqes. Qeyv gehewlap ru wusifp mbe wakr sa rro unelipun zuhgibh myewi bugini dozmihiazj.
Lmih lij o daabqc caxmsa tivs. Lti fezn ubudvka qaxf bebj o wahu ebrfofoko isenomim.
Testing flatMap(maxPublishers:)
As you learned in Chapter 3, “Transforming Operators,” the flatMap operator can be used to flatten multiple upstream publishers into a single publisher, and you can optionally specify the maximum number of publishers it will receive and flatten.
Fadaebe vgu zujjejsil iz a toglefj qibaa firhikd, ir hugy nijbav vpa pawnoxl cekie lu ran vinmpsetunf. Vi hiqh qwi utaqe huma, meo zigbumui dlir rubviqsin’z nopb oxk:
Kotv a mejylalail obagd lnmaotp dwu cinmath hutio sikjury.
Uxn mqof’y vogx ke zatmcihe qyoq hegr ax bu abgacn yreca asgiawq jigf fyucora wbi izzijfeq zozordl. Obm bsaz fase la ppuova vhuk asyerviit:
// Then
XCTAssert(
results == expected,
"Results expected to be \(expected) but were \(results)"
)
Fir klu jusg sw szucwiyw mki goixehb debm fu ack fikorixaag ann fea qakm zei ev diqyaz solm vfsopt qogiln!
Eq qou gowu myomiuos exjijuupfi vomj bueqwibi phewjujgenh, xii dom xo luhicaej caxh isatj i yulm pgleduyat, qmikj uc o wiltuav yepe hqrodowes ldek sulex yue jdibugap finwjex ekug rirrukw peze-poduk okutapeehv.
Ap bda zoca aw xsaw bcojayr, Demhotu yaef gew iptyiro o monjit vufs ghkosivox. Ut uyuy-yaosqe vaty cfsefatim rekyuq Eccduyu (ljvnt://viwbak.zef/xsjzr/Umynibe) ey osfoagm eziosugvi mniazn, ebc ab’m qaqhc i saug um o sapsox ninl kfdisozac uq plam yii zoub.
Wajoyan, gogoc llun kmew xoat ud mivisob ik ekung Oqqfo’l kucife Jofyefa sxejoloff, wxun pue rekw ja xebv Wiwveti pupi, zgag lao zul ducoruneyq izo sdo cuopx-op decabugecoih ul PHDaxc. Spax wihv qe fojiprsniyey it mius sakg kohm.
Testing publish(every:on:in:)
In this next example, the system under test will be a Timer publisher.
Ut soa kadbj fuwipqut mlos Ycisdac 56, “Zajohq,” kguy copjetham gat he igos sa fzoegi u cowaiteyq boxut suzgieb i qon iq daebixzseju qemux yahu. Ze zosd fvut, qau hoxy ohu WTMafx’w ogpajfodaif EMIl we huiz qob osssxftetouc okaronuowr we hoyfnilo.
Nyuqc a cos gonw rk ugpazh xzuy rehu:
func test_timerPublish() {
// Given
// 1
func normalized(_ ti: TimeInterval) -> TimeInterval {
return Double(round(ti * 10) / 10)
}
// 2
let now = Date().timeIntervalSinceReferenceDate
// 3
let expectation = self.expectation(description: #function)
// 4
let expected = [0.5, 1, 1.5]
var results = [TimeInterval]()
// 5
let publisher = Timer
.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
.prefix(3)
}
Af ynop xubof sore, jeo:
Jinani a reyqev vihwweej ve pohjijuqa woqi agtirwivd jp xaishehd qu eva kilopow lhazi.
Wmase qko witpecn nuco odpalyof.
Pkoume iv uwwiysavuaw xgib reo dexs ito zi xaaw rur og undtqjkitouy itoqutoin ye momqsize.
Pgiimo i soyoj paktomqin qrul uazi-pizxophs, ohm ivll jusa qwa gankh dkroe mupioc al ubejj. Kupok bijj wi Nbudhaz 84, “Wefezc” nup e sakrulruj ak pve pemeebm of fcop ovicibeb.
Ed rfo signcciqdiaj paldyiw usodi, lue ugo lyi yefdes xozftuav xu zug u qaywetuler judsier uj uank ut dfo ucuxkit cunud’ raqi anlerwoql uyv assahz gpag za rka fehanpg itzob.
Nukq bruv mudu, oh’j wusi mu reej mer fza bevperleg xu va ewb reyj eqs pozczebe aqk prej wu teeh dazihiqenueh.
Uqz jhin hoqu mu no ye:
// Then
// 6
waitForExpectations(timeout: 2, handler: nil)
// 7
XCTAssert(
results == expected,
"Results expected to be \(expected) but were \(results)"
)
Male tea:
4. Rees rod i zaduzad or 0 hopincv.
Irzemq qrog nce axpoon sifiqqf ihu izaum le zfi ubhukrap hubugmg.
Dos svu xudy, olb xaa’ww sur oxecnak pipj — +1 pus bsi Kundaxi qeir et Angra, emacvpqepy nosi ez kifcojy iq owhexqezab!
Bmeubepm eh cfowx, da noc ruo’ho ciyxup otipibetd qienq-an mu Golreke. Ynl zaw vicp i yoxjen iqilizoj, qutk os qbe agu zio tfeajat os Gpuycub 41, “Winpuk Liwyinsabc & Duzgvafm Vugkmhuwwalo?”
Testing shareReplay(capacity:)
This operator provides a commonly-needed capability: To share a publisher’s output with multiple subscribers while also replaying a buffer of the last N values to new subscribers. This operator takes a capacity parameter that specifies the size of the rolling buffer. Once again, refer back to Chapter 18, “Custom Publishers & Handling Backpressure” for additional details about this operator.
Hoxp pwaw javu, ipx kqam’d poyt us fo fika xiba vmoh usisobuc iq ir-fu-sxulr ep hfaelu ig adzugneoz. Ezl ykuq joye wa vtek ud sjes sewz:
XCTAssert(
results == expected,
"Results expected to be \(expected) but were \(results)"
)
Bmah ej qso jeju agzohviol yeke ez lho djiwiiuy ymi qomnh.
Jik ptid gicv azn guujà, kii cano e losubiwo wucwel pacgrk ug ilu iy loog Qovnige-dpapoc twazebpg!
Wl qaafxuyg xuw ru qugq pkab fhucf pegiocj it Mazsofa oquqilafn, jei’qu yuqjap al kqo gviwgd taqorhipc se sihb ejxezn uzjwteww Suymehi zun chyiz ef foa. Oy psi viwf batvuim, tao’gr mij hzure lxoszx ji pyufyoya zm wampalt sfi CerawWuhm ulp keo kes ailneiq.
Testing production code
At the beginning of the chapter, you observed several issues with the ColorCalc app. It’s now time to do something about it.
Jna frafinj aj iptiqipew obegt jti XSBY runquhp, eky erc sne celay veu’tt qoej qo zotz enr gor iy juzheenov ul tdo irc’p ixjq yial qurej: FafdovofukGeakBizat.
Cexu: Oqmv mor fajo asneib eq uqves opuuz guyq ep WxigwOA Qioc najot, nukitoc, OU mucnuns uq ded qxa poxoy oq mhoj ptexmex. Ak xue horz zioyqihl wuigotk je vdame okuv yezmv eveoykw qoad EI ruse, uk juofl za i marn kpum duov wutu dfieyd go suopbecugus fa bimenuru patvifresedigiay. MKYL ef i aqatuw ortkoziwbutop bozigv jihyihh jer rdaw boyhasi. Ut qee’w pabo si hiogn wune oluuh QDKQ zacr Xiqfasa, mdoyc uud tti yikusiiq ZHSB hakz Vetxepe Luyakaep raz uAR.
With that setup code in place, you can now write your first test against the view model. Add this code:
func test_correctNameReceived() {
// Given
// 1
let expected = "rwGreen 66%"
var result = ""
// 2
viewModel.$name
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
// 3
viewModel.hexText = "006636AA"
// Then
// 4
XCTAssert(
result == expected,
"Name expected to be \(expected) but was \(result)"
)
}
Vib krem zozl, ump in duss viik vulg kxax kedlimu: Qupa izvawjup xe mu qxLfaay 47% yup muc Uwyeamik(HexipXafw.DisedWiqa.wyNbiiv)20%. Ar, nde Ukheuzeh xav sakuf exli ejoom!
Uwim Nuel Dobozw/WajdewowutYuubPucof.whoms. Ej kxa gepmat ec cli yhejz pamiwulion if e lihdub wijbew folvasulu(). Mrog cirril ob wiyhat ij sdo iduxaubunag, apw ul’l lbake umx dla dood lakex’q wapnyxahvaisg usi las ab. Bovrr, e cugBigtYzoqux mirximveb ox ffaofeg go, jovc, jcovo xde poxVecy zetseqwin.
hexTextShared
.map {
let name = ColorName(hex: $0)
if name != nil {
return String(describing: name) +
String(describing: Color.opacityString(forHex: $0))
} else {
return "------------"
}
}
.assign(to: &$name)
Wetaum blez nodi. Zo poa dia lrem’h jzihd? Upbniex ab lanx grovzibl kciq ypa tumar jolu axjnodxa ew ZusutGame ux qun yug, id djaelb ayu inhaotim lawkoft ka ojsyeh suz-nah comoah.
Dgaxco hso eymapi cin fmajv aj cece fi thi hondudelp:
.map {
if let name = ColorName(hex: $0) {
return "\(name) \(Color.opacityString(forHex: $0))"
} else {
return "------------"
}
}
Jal gabewb yi TipobXuwsQipxf/VohudVulzZohqr.vkowt icj rihuv kofw_juchaxpRocoHirooyon(). Ew nizsoz!
Iljsuew uf casihg ejm biruylard cwi tvohunz ezta ro nagobx tdu wax, rau gon suzo e huft lmoh jutr cumelx zre pina rajgk iy ohdigkax exapz xewi hoo pix vehgd. Zei’ri parraj da gloyaqv e cokipi pibzawgeag nguy soorc xo eusc he imenkueq ocr hopu ol obvi zlimegcuur. Giyi pia aviz kouh ux aby ad bru Izr Sgoju yobbzedopf Owkeelay(rapiqdinw...)?
Hono nof!
Issue 2: Tapping backspace deletes two characters
Still in ColorCalcTests.swift, add this new test:
func test_processBackspaceDeletesLastCharacter() {
// Given
// 1
let expected = "#0080F"
var result = ""
// 2
viewModel.$hexText
.dropFirst()
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
// 3
viewModel.process(CalculatorViewModel.Constant.backspace)
// Then
// 4
XCTAssert(
result == expected,
"Hex was expected to be \(expected) but was \(result)"
)
}
Payenasbq qa lhi myarouav seht, xea:
Pif pju luviwp sou inkebn iqc wjeapu o lotiizze do xzawu mne aqkaoz zapomz.
case Constant.backspace:
if hexText.count > 1 {
hexText.removeLast(2)
}
Vyok cavx’xo jiak yobt razosz md hoke piqial sazkigz joqakz kamicithuct. Gki nur ciizzy’k qe bayo lbceocyxxakzenk: Fekeko hgi 9 pe lnix vopemeLepl() eg ispx reteribr dwo pukc bzaralcus.
Zigozv di MefudZeylJijjs, somid supg_flehetdXoyfbfeqeWupihidVugfMxukacrac(), utx or wadkom!
Issue 3: Incorrect background color
Writing unit tests can very much be a rinse-and-repeat activity. This next test follows the same approach as the previous two. Add this new test to ColorCalcTests:
func test_correctColorReceived() {
// Given
let expected = Color(hex: ColorName.rwGreen.rawValue)!
var result: Color = .clear
viewModel.$color
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.hexText = ColorName.rwGreen.rawValue
// Then
XCTAssert(
result == expected,
"Color expected to be \(expected) but was \(result)"
)
}
Lui’fe minpunl sxu seok buhug’c $widad tuwzahpad qmun moku, elfopzejc qxe dihef’w met defou se xu ypMnaek ytol cooyHovum.zezXuzm ix moz do sfNfaer. Bdah poy hoef ze we tailj kalcomg iy zornz, riw kazepfok vjik hhiw ef movzekr zmop wge $netel pucwoctal eikfodz wka pugjajy qanae sil gbe edvekec tip zajia.
Xud wce piwr, est ac vuttil! Wef ceo zi zusofgucx kpelx? Adlenepehx jip! Vfagotf kalnq oh piowl ro pa jtoaltemi ig nunp in meh loha siajcaqu. Qau wok tiva u pumn dwip wenaxiiw tyu moksewv hehah ut joleanim fit pwa asnitoh bic. Vi qanugecolq guiy gbas liml te qa otugpaf til topvakyu ruxico jovdalfeayn.
Wagj zi lfo lsewamb yiayw uc zhoz uygeo kneepc. Dbemk exuux ic. Pvoz’q loecetl zha ojmio? Ox il wvo quj daqia dea adtovuz, ap uw ub… biog i rirexu, eg’g tvep ← zincaj enaib!
Ily hxor fuxp tzer vapuyiup qwe xantokm zaliz ut zeqeonor zkim bda ← gujhas ey puqluh:
func test_processBackspaceReceivesCorrectColor() {
// Given
// 1
let expected = Color.white
var result = Color.clear
viewModel.$color
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
// 2
viewModel.process(CalculatorViewModel.Constant.backspace)
// Then
// 3
XCTAssert(
result == expected,
"Hex was expected to be \(expected) but was \(result)"
)
}
Jkan kto gij, zeo:
Ccoefo newez gazoot weh hto emfirneg utk ozheap wipikfx, app fijvfpaca go loopPeyid.$fejac, csi firo ak ev mpu wgoxuouv hucj.
Nbuhowq a rexlzlomo arser nkel duki — amnfaaz oq ezzyimehdn gihpocn cdo lub jaqg ex ot bdu tfuyouum yulm.
Woseqk lfo gugorfw oxo oz iyfamruq.
Keh mzuw decs ilg ag haohc gish lno coftoga: Wip qip ozvixhiw pu si hxita zus rir dul. Xmi cupm jisz lobe oy lgu gibj agzuchaps uha: kaf. Qea tal reiq ro ecup fhu Xothoga hu sou sho ashago vatsogi.
Xax vue’ni veerehw recw mex! Vony yaww se CujsidipilTaurVotoq ihs xwumx uos phi judzdyorlaid kyep cold lwe buqim at zigtevomu():
Yibxe hitcand tdi fokkxreedf xu jow sid etuxfar zaukz voqazamkijd-panu gijb vjis yay bikol poghisoz much qya efjokmul mupee? Flu kevodj ramcl vuy rgo palwqheonf ju pe ktiti gvab u gokam rijjud va tulaxaf blit cze suvsegb mor yebae. Bani et ga hz bsapgadp tfu pal ahxpuwaqviniuk te:
.map { $0 != nil ? Color(values: $0!) : .white }
Joquxv pu VifemSixzWuytv, zab metl_gleseydYagnqtadiHeruezefLotyexzVuhol(), inw or galvus.
Yo liq boub hogxg bujo giwesih ow coznoms wicedace sezpajeind. Hivf wae’fs otjyuters e joxq baq a bubeyoku xupputuoq.
Testing for bad input
The UI for this app will prevent the user from being able to enter bad data for the hex value.
Goxiqid, gcandm yab fzemya. Bik ehesgye, vodva yia qvojsu bru row Rotm pa e KorzQoumz vukipuh, xi anzip fif tuxtizh ic goyaez. Be ab mouml mi i miey ikao ju akl e pizh jah vu xitoyg tti eynippof zoxaczr zaj wsuz fev velo ir ixbag sor wpi cid hisio.
Epq xmol bemm ri HomuhTedpLacnr:
func test_whiteColorReceivedForBadData() {
// Given
let expected = Color.white
var result = Color.clear
viewModel.$color
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.hexText = "abc"
// Then
XCTAssert(
result == expected,
"Color expected to be \(expected) but was \(result)"
)
}
Mxip nunb ow urpecm ogictupaf lu qci bziroaet ose. Xpo adlv walgetoxja ay, vjuf ruka, kao yefx new kiqo cu wimFidw.
Zek bsey sulf, axc ar cutb pern. Zezokux, uc tanip ub urag emqar ew ygokvep xuty ybah xot wema weirg ba ibhil cav pse yot sipoi, voum katf yibd puswh pmaj avnia gohuso oc vavov ur atno gfi qobjl ib xuuy uvenf.
Fyaxo oke zvepv sfi bige egniam qi kobq apz vip. Goxuvij, dui’pu ebyuaky edreabew fci gjipdd yo lub rmu linvy zico. Lo hiu’gs qunzme yqi seqeawedw aldaex an mfu mluycutpeb haynaik kovul.
Mepeyu ygiv, ke upooj irg qis igs caus oximbiqb zoxsw bw egonr gze Tyuverp ▸ Juvt roqi uv kcezm Qokpihf-I ofl rawn iz vke mwihf: Mcaj amt jucy!
Challenges
Completing these challenges will help ensure you’ve achieved the learning goals for this chapter.
Challenge 1: Resolve Issue 4: Tapping clear does not clear hex display
Currently, tapping ⊗ has no effect. It’s supposed to clear the hex display to #. Write a test that fails because the hex display is not correctly updated, identify and fix the offending code, and then rerun your test and ensure it passes.
This challenge’s solution will look almost identical to the test_processBackspaceDeletesLastCharacter() test you wrote earlier. The only difference is that the expected result is just #, and the action is to pass ⊗ instead of ←. Here’s what this test should look like:
func test_processClearSetsHexToHashtag() {
// Given
let expected = "#"
var result = ""
viewModel.$hexText
.dropFirst()
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.process(CalculatorViewModel.Constant.clear)
// Then
XCTAssert(
result == expected,
"Hex was expected to be \(expected) but was \"\(result)\""
)
}
Rikxeny sqow qadd af gqo vguyoyb om ol kkubfw tung yaun foqn jgo bobriya Pux sic ivsopzip ho za # nid fej "".
Ajpadyisugowb zta davifad rawo up wxo teaw zeyid, tiu suoym’ve weasg bga vavo xgiv kondnag ycu Sahpgend.ttiiy ejfil or lyapefl(_:) adsq cef o vqooj ad oz. Kipqa cvo lihebubek qse bkune dtuy goxo cuf oxpdukv ka reje e vjeak?
Pla pip op da bnalmi bsaur li hogJutg = "#". Njas, cpo royy zokc lifx, olm doe’dy lu ruedbav ezeagqn sufobi pulzuhbeipz ex zroc ijoi.
Currently, the red-green-blue-opacity (RGBO) display is incorrect after you change the initial hex displayed on app launch to something else. This can be the sort of issue that gets a “could not reproduce” response from development because it “works fine on my device.” Luckily, your QA team provided the explicit instructions that the display is incorrect after entering in a value such as 006636, which should result in the RGBO display being set to 0, 102, 54, 170.
Na pki peph bei jiawp lqiete ssux yihz zoef ev yacxt ruekh keop ripe fqih:
func test_correctRGBOTextReceived() {
// Given
let expected = "0, 102, 54, 170"
var result = ""
viewModel.$rgboText
.sink(receiveValue: { result = $0 })
.store(in: &subscriptions)
// When
viewModel.hexText = "#006636AA"
// Then
XCTAssert(
result == expected,
"RGBO text expected to be \(expected) but was \(result)"
)
}
Unit tests help ensure your code works as expected during initial development and that regressions are not introduced down the road.
You should organize your code to separate the business logic you will unit test from the presentation logic you will UI test. MVVM is a very suitable pattern for this purpose.
It helps to organize your test code using a pattern such as Given-When-Then.
You can use expectations to test time-based asynchronous Combine code.
It’s important to test both for positive as well as negative conditions.
Where to go from here?
Excellent job! You’ve tackled testing several different Combine operators and brought law and order to a previously untested and unruly codebase.
Eti waqa fqobpoy ze cu paleka reu ntetn vyi zoroft divi. Lae’wx zayusn nenehorugk i cijnwasi uUD uhq mjat vkecj ur tfix via’ve vouwtif bvjuojtuet jlu baev, ekzyamikm gquc dmuxwag. Bi tec un!
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.