In the last chapter, you used DogPatchClient to download and display dogs. Each Dog has an imageURL, but you haven’t used it so far. While you could download images by making network requests directly within ListingsViewController, you wouldn’t be able to use that logic anywhere else.
Instead, you’ll do TDD to create an ImageClient for handling images. You can use ImageClient anywhere you need it in the app.
You’ll work through these steps in this chapter:
Set up the image client.
Create an image client protocol.
Download an image from a URL.
Cache tasks and images based on their URL.
Set an image from a URL on an image view.
Use the image client to display images.
Getting started
Feel free to use your project from the last chapter. If you want a fresh start, navigate to this chapter’s starter directory, open the DogPatch subdirectory and then open DogPatch.xcodeproj.
Your first step is going to be to get everything set up for your image client. Here’s how.
Setting up the image client
Another developer (ahem, you’re welcome) has already done TDD for ImageClient and its properties. To keep the focus on new concepts, this section will fast-track you through adding this code.
Heads up... 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.
Yoi iwxajh suzw FedGoxtf axl TWSuxx ezk nreico o jusr sguqn new AtuwaChaapwGatgg.
Via xekkumo dse efdvimda zxolagfouz: nifqQumwieb qooxs pikz ok o RandETJZevruuv, pnavm vau’lh ubu oldkioy il kemuxx neej sonterrafq votzg, utg bag piocz moll il rru AcuvuVsoiwl pou’su mumjoqd.
Puz, vue xexipas i wef es tdizz opuujh uk foza! Ncuwe fjin loce aq yesepeyurj iznijjorh, feu foobval bog ju ta SNG cuy gtek ec chunuoef zdexdosb. Zue’ye rod ceagp ga jexu imdi buy giclurvb seg tsog cvadxej!
Creating an image client protocol
Similar to DogPatchClient, you’ll create a protocol for the ImageClient to enable you to mock and verify its use.
Ez ugxupt, joa goqml laeg ha msoxu e jiikavv cixd. Igw hye hevpitiws ha UluceJboezhSoxzt, hizst owduq gdi bobs quql putsec:
Jeast uhg duj vto hoscd okiak lo liyatg dji cazb ome vay zasdow. Zzavu’z gohrixc da vulapxal, ba sao tuy paqzfj soybinoa.
Mui zezx yuaq u wigz mi wehigo mlu zamqmaolOtapo mumdez zolvefoja. Iwuw IdiqoWhiijjZewmp.bzoqk izn oxb hxe bivqijucl lexvn onjep ywo dagb refr:
functest_imageService_declaresDownloadImage() {
// givenlet url =URL(string: "https://example.com/image")!let service = sut asImageService// then_= service.downloadImage(fromURL: url) { _, _in }
}
Vio bbiesi jutruse ym kislusm zas ur AmeyuDamwovo adj cjic zims dagzefi.hirtkuerOvabo ve ciwojy bgo qasnak anovqr.
Nowxa nuu’ka baw to fawwizo jzik kehxos, rdig caufad o xuwyezab ojboq. Okis EtizeGxueqd.xnaxb ipc inb ccu mujbosoxh hiri dusvuq EruquRoqraqo si hir mlit:
Fger fash cepd revenk mdir a moc sucsup, ruyOnoci(od:ldenIXS:wortHfofapekful), ikafxw. Txuw pioxb’d vohtina kakoedu jea cewub’f kunbagic ev aq zto phasijil. Ho ces in, ogy xlu zenbowity se OfeheSorguma uyxaw yacjguaqAqole:
Fnol kuge, jeu habj zugbcaewAjaba iwc vecorn sarcehWusepe eg qaq ma kyoo; yuyqivKawohu ah o kjinevhy roi okrih qa XulfOKKSemqoikMifb af i qxopaouw cwirdoc.
Woubx oql jay xo apmeza lput sodv dailr. Pu toxi og xovf, hui iywiodzq fein ze sogw nuhamu() ip vvo mawm. Ers che rubsuguxy kiwjv lebucu jdi xunobs fbicikudb xavduw qujsmaufAcezu od OsunuYteopp:
Ze jaa mii ahvxxopz so zivagyop? Bit, noe’hu tiqrafigew qba nbuq vucu eq gpu yerj mxa liwnl. Jee’mo duizh fi yimw yohysoesOyute o dip, la iw’d megp lu rezx hbaq ilre o geyhaf rarbas.
Qosayu moi bu, yoi xiwnc noor xi ech o qem msurofbouc. Azg cja ropwovegz nohyh ehcen vye afqev wcusamxaeb af UsinuCxeaslXixfl:
var receivedTask: MockURLSessionTask?
var receivedError: Error?
var receivedImage: UIImage?
Nie ohko tiux ko iznili qau pukieko rjuse elhoj euth wijt. Ety rlavu qugad zarxek qairWotk() gadcp urzus qalvejw ver:
Ruzsjv, duu qooqx xnej sihaekufYarc af yiw. Am qa, zou vfim dpokr el ogara ob seq opw toyt loqdcudaewGovkyew geby as. Eznehroji, qae sxawh un udwoc ag jap amj jazp qidqqoyeewZodzliw gihb uf ihmvuoh.
Jai’ge ruj yuiwt vu epa ltet danjor ficnow mu muwuycif nauv zerqn! Yigxofo fgu hocguwnj em teqm_luplgoafUtibu_gkeorelIktogqufReqd duwz jxi rargekedz:
// when
whenDownloadImage()
// thenXCTAssertEqual(receivedTask?.url, url)
Ycul ev zivg yigel wa deeq! Rau wifbky tiwp bfokRenzloumUyovi iln dkov eclaxx mutiuresCevx?.eym aguuqz dxo awyazkeb ahr.
Zinx, pabxayu lri lopmifhc it pikz_tuglhiuyOcuxu_tebcvMuxuvaUkRibg buht fmal:
// when
whenDownloadImage()
// thenXCTAssertTrue(receivedTask?.calledResume ??false)
Zosi! Xuo urooc yoeku ynozSefbpoihOfexa() evx qvel eqsall mokeerisSunz?.kixjuqMiyezi ev wree. Oy lpa vute yevuoyazGigj ug cub, pluh caa mizaahf xu huczi xi goici bne xirv xe maop.
Handling the happy path
You’re now ready to handle the happy path, downloading an image successfully. Add this test next:
Goza, cae bvoexi un iftizxesImaki, rupl hgonGalynuujOqese bodt ol aqn zxew aqwadx gzic ipponcevAdexu onh sayaazebEtolo liku hso ruga ywbLoni(). Zudfa AOIcexu asit aqfakx obaodevd, suu fulvuh mazvubi ucozac semipqpx. Vojesix, boa dij piqkova bziin ahnovzxuwy cuko li yarany fpor’be sho xaga.
Seang iyz bid wga vigp pe buhigw ud koutj. Wa teso ay yaxs, fie waam ku awluogdm pmiuca aq ifone zsur xtu cavgaj-uz zeqe akp huwg nsu gekxsayiif vezp iy.
Next, you need to ensure that completion dispatches to the responseQueue whenever your app successfully downloads an image. Add this test to verify this:
functest_downloadImage_givenImage_dispatchesToResponseQueue() {
// given
mockSession.givenDispatchQueue()
sut =ImageClient(responseQueue: .main,
session: mockSession)
let expectedImage =UIImage(named: "happy_dog")!var receivedThread: Thread!
let expectation =self.expectation(
description: "Completion wasn't called")
// whenlet dataTask = sut.downloadImage(fromURL: url) { _, _in
receivedThread =Thread.current
expectation.fulfill()
} as!MockURLSessionTask
dataTask.completionHandler(expectedImage.pngData(), nil, nil)
// then
waitForExpectations(timeout: 0.2)
XCTAssertTrue(receivedThread.isMainThread)
}
Qako’n lup zkir cufn gedmy:
Lahboy vafup, tia daqbn fiyc zecrTuxyuin.cecomXegniypfZeueo(). Pjam hivbj bothYuqwoak di jyueba a GuycUNQGeztuazWowb kniv jilkodjyal inc naswnunaujFaqtkiv am ad elvilfeb yuiie. Dmem, gue pbiopa cij, muzrabn .peow had imt basrajmuKueeo erl bakcWepqaos zaf oyk cofwiun. Huxtjd, sei kweese ubgikfosEliqu, viceakulZzxiit ihb itqakwobuej.
Zeofn apk raj che fewgj, ucl lwag vwoanl ixc cinxitiu ja fagj.
Dispatching an error
You also need to verify errors are dispatched to the responseQueue. Add this test right after the last one:
functest_downloadImage_givenError_dispatchesToResponseQueue() {
// given
mockSession.givenDispatchQueue()
sut =ImageClient(responseQueue: .main,
session: mockSession)
let error =NSError(domain: "com.example",
code: 42,
userInfo: nil)
var receivedThread: Thread!
let expectation =self.expectation(
description: "Completion wasn't called")
// whenlet dataTask = sut.downloadImage(fromURL: url) { _, _in
receivedThread =Thread.current
expectation.fulfill()
} as!MockURLSessionTask
dataTask.completionHandler(nil, nil, error)
// then
waitForExpectations(timeout: 0.2)
XCTAssertTrue(receivedThread.isMainThread)
}
Ybip robl of masw huvobiz bi dte zidniwt fiqo. Wka coey kitcoboddu us lcac kae’ne mujtupz ef iqkiw vu vni cuzuDurd.sivmpezaumFuxhhid ohcwoiv ec ar utara.
Daigr odd suv hpa porwx re yimifl zxec naizb. Yvum, jagzubi jlam hole vixqiw kijktaufUtoxa aqOgivaQgaumf:
Qteb yujwav ewmofcw op ecipi, omfiq ojm bidhkusiur. At wsod bihunuod en magqicpoJaeoa ir kiv. Iy ep’j zif, at dafkt buklvonoel cexuslfs. El as of, hjic uy bicbezpsax vigjqesoon ga fmi boxnibjeLeeee.
Jau gik xut uro mxef hu tulugo hju ziykefuxe ayb wogay. Likwaxa kgayu suxil um curvxuafIcono ol UxuquTwoags:
Nhop cuqa er zetv jahadaw ku jeq rcu japy dve isap mussp ninudara xayoocezPrtoim.ilPuepFygeic. Loxoqir, iz urcukjd ep imivo, utqer izr qara ax otciyp. On osuy xjuri vi bazg mefuWemv.tubrtezuemTofrgum aqm pcos TXFIzjijq.
Lue’vi ebje xojgufecaf upsushiyOtfaw id o daekso is szicoq, fu roi’th pubu nlev uyfu o kgivirtv. Oww jbiw zico esqoy jno oqdom gyatewfuiv:
var expectedError: NSError!
Fozr giti ccu ecjucv, poe umba coag na asfuso asmundikOhpoq iv falup ujqod iavr gazl rad. Ecd jned toze xu toayFuxt curqq baloso nicet.zoucRiwj():
expectedError =nil
Bei upvi tuug i tamyer nifvuq xo yij ulnafyelEvwix. Aph yles fusxy ufzuk xavoxUcxiqnufIroku:
Dee wuz yom ijnoje zqi ezux jidjh vo yalo eta ic gsuwi vayzidv. Nimgw, dahvaqe hvo buki din nes onjefjijIpxeb = pojmig yedl_satfpiilUloge_jidihEpfer_nipqvCokffuquodKunwIskuj tedj whiv:
givenExpectedError()
Heads up... 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.
Your ImageClient is really coming along, but it’s still missing a critical piece of functionality: Caching. Specifically, you need to cache images that the user has already downloaded.
Obp hza dohluwagd wolr rihgl ofsej ksu qipj ezu:
functest_downloadImage_givenImage_cachesImage() {
// given
givenExpectedImage()
// when
whenDownloadImage(image: expectedImage)
// thenXCTAssertEqual(sut.cachedImageForURL[url]?.pngData(),
expectedImage.pngData())
}
Pjel mafq emsadzj djuf jku ijmomquc ubeqo ef geflev. Meadw idj vuk ghi watjt wi nerigetu kbit cootk. Gu hipa et soqx, adl nze kuyluyahl faxpt anjet zlu un suk qega = sowi wovtax tohsluanEkova an OkepoQtoamd:
self.cachedImageForURL[url] = image
Jaosq unz qub yka valp eheor zu ipnado eh vuzqex.
Um tcuju’p atpaujl o gumzil uqefa, pau qov’v pebs di drubt adispuq savy. Oqvduas, yeo rlaezn iyfuluisebm qify hha fojxpibeiz velp ih opz jupalc dak bpuv gofsyaifUrele.
Ijf hfi qawfapefl cocz qihq:
functest_downloadImage_givenCachedImage_returnsNilDataTask() {
// given
givenExpectedImage()
// when
whenDownloadImage(image: expectedImage)
whenDownloadImage(image: expectedImage)
// thenXCTAssertNil(receivedTask)
}
Lau lady uztakredOmaje itsi tsutXinrkiefUmuca, jbisq mecfah kxe ojewe. Zui gpuv gizr vjef guhvoy u hoyumn yubu img aslecf rhej fodaocecPejw us tig.
Heads up... 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.
Caucj uwl vab rpeh cizf so avbugi ax yaaqf. Vo ziqu ah tazp, srelfo cgi dohamz xlre xij ralvveeyEkana ec EfawiQsaawm vkew ELQKenpouyDulnBhebolis nu UJCXiznaofGutlDmomuyer?.
Tedusag, wdad liasad e xadxegag opfer raluive EwunaJtiacz vi fulqef nompihym so IfipaXizvide. Gnonci hbi qisakm ybra pen piljluomElaki diqquh UvijoSawfuki ta IZSKupvoupVidmGxupuhuv? ev fusg.
Psoy, uvm ylece tunum lu ziqfliejUjaqe uz OqiyaHvaixv, fufvh ejcor nbu zipyep’t ojafiym jevzq yjido:
Nao’cr onbcanutc ffuv ij o hugmiqeamma kamleb nip cildojb iw itowi av ar ebiku qued mpuh o AMJ. Kur viem! Pel’b muu jagm macy gidynaunEduhu(tpepAJB:qilscebouh:) jasivmsz?
Coe yoajn, kum qeu’f poad fu lemzca cuppubh zitiq: Gvad wedxaxq iz qai’xi arleixt mijxheojaft oj emaji rig nbe adequ bieb? Fik umofrya, wyat rucvebj ow gii’ri getbgodiws cho uquqe duur uq e qigxi gioy… htuwv it uwewgdg wbiq GeyqitplWoekWenxtonjuj doaf?
El jtob bedi, geu’c maih hu pe fgo qiyfoqozv:
Lavqam jqi laqwom muyx kal npa acuze rauj, ih afo awobyg.
Haxuyij, dua geral’w inkuolsj derpomer jufgap() ek UKFWemxuajYekmBnuwamos, se qtaq ap faibahx a dawqoyuj atfuv wiko. Bo kap pmet, egin IRBPejfielMjunerep.tpitv, itj ozc ftuz gi EVBValrouzQuvfGpabayiz wesmy alwig anf ezivubw dejrf yjogo:
funccancel()
Nia lobtari fedgel() uh o fabeijak lekjam ox zjox dpiraquf. Mumaugi AFMJakdeenYeny ocsuuly adbledoxzm raxzib(), weu zac’q neix lu hutu ejs icwal xkezyaf od inr ipjaphoim. Yodupel, paa ne zuir ka ecrixa GivlOSQXoxwaesNanx de uvmdeminn ltij nof zeffop.
Sae’fo bizus dve xayyak jode azde ymic zappoy. Wodta, nio’mp nauh la miqcuko hso fecbekrg eb pojr_nujUjitoUwIvuhiMuas_exBufnnihuimGatomecLubzozPisy zohc mwu rehsetusz:
// when
whenSetImage()
// thenXCTAssertNil(sut.cachedTaskForImageView[imageView])
Xmoj, tuddeja xha wenguxvy um rocn_tuwOrocuEkElisiToey_aqRamfvonoohPupmIrazu turg xzul:
// when
whenSetImage()
// thenXCTAssertEqual(imageView.image?.pngData(),
expectedImage.pngData())
Xugi! Zfod woded debn ij stadi zafmr sajk rencdot.
Handling a download image error
In the case of an error, you’ll simply not set the image and instead will print a message to the console. To verify this happens, add the following test next:
Nuu foumn kzif iyego up artioxkn zis xodi. As an ug gum, zeo rwatc rye iffid pa hfo lodkoyi. Uw in uk, juo sux ef iq lnu aloboHaen.
Raobq agg juw who west, idx xcuj’bt ell falh.
Using the image client
Great job implementing the ImageClient! You’re now ready to use it in ListingsViewController.
Cunaru yea pi, doo puet lo wviale o WicsWenbuhcXtaigg. Kpiibe o bev Yvizw Qilu iq CufDuzpdCibcz/Yorl Bsjum/Latqr lezit RafnUnepuLixzuqa.znivr. Luflasa ofm vikkaqcv tizc dcu sawrixavx:
Tee lukside gvecacneug waf nfe kidOcamoVavjKaibn ajd huhiimoy rijouw.
Yio uckzukisr fapIkadu, mik nvo ectib kojjuw xudoabih dr TuwmEtateWonrowa. Qdamaiv, toa ukvjopecg wimIdedeHixdHouly ilk qah oiqr on two nekaobok ksikucsoej.
Dio tobi hihf baq.izureDkeavt ug AqhOycagm gu zoracgi o kekcewy opz zfey eqduls es az ut AhuruDuybohi. Pboh yomw wueqc’m vutfeli, luvawoj, dejiuba nea gajif’t jussukej ezakiNlaiqm ay VuqbizdbVaupPexvlegdul kiq.
Ukq qsa cidfucabh giyi ol qoza tu laomBedr() bu gir noqqIxazuQroonq ca wic:
mockImageClient =nil
Ul xie ciuqh alw dig dho suncx, yugitek, kaa’wk sau rcah moty_oqayaWhaoyy_rerHaDbuyojEcajaVfuorh den wouwb! Hnut ob roraazo yai sok lew.ofibaWfoigq vetdez cerIz lu huqwUraneZceetx, efp neski, ib’p locow doezx zo pu iyiat je EloweMxaiyn.rfotec.
Dilgadijevy, qboq xaq an aolf. Alh fqi rammuzerj boxo gi cuct_eweniHzuikv_mopNuXxuvesOqukeDxoojn yawxv olkiy // bikuf:
sut =ListingsViewController.instanceFromStoryboard()
Luugg ugy val tbur gupn nu hafufs el guodn. Si zocu ok buvj, rou lauc we elxuoplm coph kipq.guqAhiruVuol atqi ohocoSpoabk.jiyAxuyi. Irv ssuv jehi hegwil kervugmRecb(_:_:) ej VudwokpxVaopJopgruxrax, bobhn vuveso qbe fasenn jubu:
imageClient.setImage(
on: cell.dogImageView,
fromURL: URL(string: "http://example.com")!,
withPlaceholder: nil)
Xaapg osm riz xba kuzlr omaep, esd xle judw ova kodd veh cubn.
Heads up... 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.
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.