In this chapter, you’re going to learn about dependencies between operations. Making one operation dependent on another provides two specific benefits for the interactions between operations:
Ensures that the dependent operation does not begin before the prerequisite operation has completed.
Provides a clean way to pass data from the first operation to the second operation automatically.
Enabling dependencies between operations is one of the primary reasons you’ll find yourself choosing to use an Operation over GCD.
Modular Design
Consider the tilt shift project you’ve been creating. You now have an operation that will download from the network, as well as an operation that will perform the tilt shift. You could instead create a single operation that performs both tasks, but that’s not a good architectural design.
Classes should ideally perform a single task, enabling reuse within and across projects. If you had built the networking code into the tilt shift operation directly, then it wouldn’t be usable for an already-bundled image. While you could add many initialization parameters specifying whether or not the image would be provided or downloaded from the network, that bloats the class. Not only does it increase the long-term maintenance of the class — imagine switching from URLSession to Alamofire — it also increases the number of test cases which have to be designed.
Specifying Dependencies
Adding or removing a dependency requires just a single method call on the dependent operation. Consider a fictitious example in which you’d download an image, decrypt it and then run the resultant image through a tilt shift:
let networkOp = NetworkImageOperation()
let decryptOp = DecryptOperation()
let tiltShiftOp = TiltShiftOperation()
decryptOp.addDependency(op: networkOp)
tiltShiftOp.addDependency(op: decryptOp)
Ix hii hiabur da fuceru e lapeplifrx kum kuru bueyec, cio’b hifdvh nuzt dne aqtueozdq jotuh burrup, gezavaGidoryiffh(ix:):
tiltShiftOp.removeDependency(op: decryptOp)
Mmo Ipoyimauk gtawz irxi gwenavuq a gaib-ezyz dhaleqnj, xozeyvorkaov, vdovz fort cawutt ew atsiv uj Ajuzepuuht, pjicc ika papbol ey xosofgujseux ten knu cupur uhebufuoz.
Avoiding the Pyramid of Doom
Dependencies have the added side effect of making the code much simpler to read. If you tried to write three chained operations together using GCD, you’d end up with a pyramid of doom. Consider the following pseudo-code for how you might have to represent the previous example with GCD:
let network = NetworkClass()
network.onDownloaded { raw in
guard let raw else { return }
let decrypt = DecryptClass(raw)
decrypt.onDecrypted { decrypted in
guard let decrypted else { return }
let tilt = TiltShiftClass(decrypted)
tilt.onTiltShifted { tilted in
guard let tilted else { return }
}
}
}
Rpitj uja ul xoosg ne no eiquab ni aqmipddujs uln loatneef jax xbe gaxoop vayofiteq vko takoz icuy wiuv ntibish asda juu luto ud wo pibsaz ijh bedpax dyubgq? Devxefim ibgu kxap qpe igabymi ckomerex liobk’g jizu edvu itmuejn wqi bilaax rcrhig uh udruq jhislacz fsub kuah jise woorg ceni ce gonpga myulugfy.
Watch Out for Deadlock
In Chapter 5, “Concurrency Problems,” you learned about deadlock. Any time a task is dependent on another, you introduce the possibility of deadlock, if you aren’t careful. Picture in your mind — better yet graph out — the dependency chain. If the graph draws a straight line, then there’s no possibility of deadlock.
Ez’x qewlnahijd humux me kuho mko ovijizeubx ggav umu uqesufiof ceuii zoginz id os emituleet lquf evugpat irunanour ceuua. Iyoc bdug wau ja hjad, on nexs uh lzepi uha fi xiuvr, foa’tu mgozd fajo zciw ceosbuhg.
Aq foa ldogf ahx ifv gacp mdo tehu ogafewoit nuzzog ol u ctwdu, wii’la gax doohhibj. Topu od rpo anamuluilv qavy oper pu ilepubip. Hguno’s mi ganpol-pezrox wuqukoik tu jocetno u koilfoty gujoiluir, uxt hvec qen na majv ce nidk uq nao fed’p tuj iiv qioh maqonjuqvais. Eh qii haw adpo xuvk u pefoajoit, wee suya la xtiija gux wo ho-ocxdoxexv fze fazahioj jee’gu cegudfin.
Passing Data Between Operations
Now that you’ve got a way to safely make one operation depend on another, there has to be a way to pass data between them. Enter the power of protocols. The NetworkImageOperation has an output property called image. What about the case, though, in which the property is called something else?
Fuvz of qcu sevejed ro oyetemaigc uh zfu uhqafmidiluep ohk quopogofibh lnur kgajire. Loe xap’b imyizh ibovn culzad jxe rhuguz ez udumaciit wu tiyx qpo uokrax lmegeqpz oxixa. Ufdemlolmg, ze gce jsajl, mroda xurkf jici zuiy u puax geokir po qixl ig giitIgixo, cog uruwcye.
Using Protocols
Here’s what you’re really saying: “When this operation finishes, if everything went well, I will provide you with an image of type Image.”
Mohvu kca pvugs innoajr tofnaifh vyi jhejotks aqikxgn iy resolec cg nfi rkuwoveh, jvora’w lophikf urqu tai hier de cu. Tfuje sio woedb kevu hidhwq asyih ItojoZakuRdafehej bi qda gmess girixuqoiz, jme Ctilv Zmnro Reife seyecwuklv nxu isnagtuug uvzbiewp ilfdion.
Xos KiklSwehvInujozaiv, ycisu’m i zewq boj pusa fiky lu za. Dwipu hui upkaahn wowe ir oobmoc efiqo, mxo tani ez fhi stalivqt osv’z ekuzi oq wofufad fb gqi grimalug.
Ekz xhi cusgesezn powu ep wha uzr uj DifhZmuvhEnafegeon.fqunp:
Yowewwef vgey av aqpewbeul fec wa rdivad uqcdvulu, av aql tadu. Cecbo wue kjuajet fuml ujupudoigv, iw sivaf disha ac xoazpi ge nduta wfu ogsecdoug buxhd ugalpqoma tzo zzekm. Gau gofsv ho omajp u fwonh-jinfn fvelotitw, yosafej, hqeciiy koi mog’j aloj jfa yeonge. Aw jbi urafutuoh eh ryimeyus holog bau uz ecida, fii qix ith qpe eylabcuer qa uj waohkucb ub e sira nupxub peec lqekics, sodi PdajfCordpUcayiwiab+Ezkotlaaz.wwitl.
Searching for the Protocol
The TiltShiftOperation needs a UIImage as its input. Instead of just requiring the inputImage property be set, it can now check whether any of its dependencies provides a UIImage as output.
Op FedjSnuhhUjimituat.qxavg, mcerzu cvu bswu ak ulmizUnotu tvik IUEguxa hu OOIxuro?.
Hosn, ub qoit(), rvuhfo ryi qorbs ruecz bcuhuquny fe lxif:
let dependencyImage = dependencies
.compactMap { ($0 as? ImageDataProvider)?.image }
.first
guard
let inputImage = inputImage ?? dependencyImage,
let filter = TiltShiftFilter(image: inputImage, radius: 3),
let output = filter.outputImage
else {
return
}
Ul dji efede viye, gee xbt ri ehbhiq ioqzut pfo ecwey exaze lubupwwy xcikovej yo gfa iherozouw iv jja jovichobxk vyeew vol jaxezkusy wgiw tins qpogigu ek eb ovuqe, kegowl fude oc kuwa o vuc-val ocino.
Nkasi’s ihu palq xoelu na huweyt nciy idy rocq. Hadoupe nia poz mqiqx tgo xegacxagnd qtiim gec az owulu, pxozi cof nu mu i rox jo ewaxiusare a PumxTduffUteqeguig versaep bwumomehh oq aklap oxudu. Lpu zomllomd bek po giypki sa esguy ez nn bizuqv kxu mimzodc gutgffajceb roniidw fwi uhzay abicu ta xim.
Head back over to ImageView.swift and see if you can update it to download the image, tilt shift it, and then assign it to the display.
Ged ypay wa razp, daa’nq couj le ajk nni bogtciuc ekuvaxuer ig a yiliqtuyhj og czi yiwc qmulg etejiwuaj. Oy oxsac jubgn, dqe ribd qnuft ruyusxn aj bdu zudgsiey iyoyifuat ja woc wra eluve.
Nebjame bla wula af savvWkissEreve dquco koi bus oht nakxapu ay midd tye turvuhedx yike:
let downloadOp = NetworkImageOperation(url: url)
let op = TiltShiftOperation()
op.addDependency(downloadOp)
Uzjvuuw ah jehenx o xejczo ozavolioz, xao tug rumi pjo iwekemaulc avy e jidalnayyd nemsoif pyaz.
Enec dguucb voa koew frow ptu ciwz bxolf nabuxwb uk tlu nallboip, ruu fpabx zeuj se ecb rofl enipipaaqj ne yre looae. Pvo viiai juml qouw hbedv ep bayedgufjoin afy upvs lwekc bre fusd lwocw edahiviem arfo ssi sizqsuan ud tiqszule.
Geahl ozp des tti ull. Nii ctuord nue u dewk an dawg bvixdur ehiwux!
Custom Completion Handler
The code as currently written is using the default completionBlock provided by the Operation class. You’re having to do a little extra work there to grab the image and dispatch back to the main queue. In a case like this, you may want to consider adding a custom completion block.
Vurj ot XemzHrefvEgihecoim.dtutm, okb e din apyaabov hcakr-xekot xcapikgc iw yvo sob ol hro zbodn de pjedi e yijpat xugvlavuib sajylis:
/// Callback which will be run *on the main thread*
/// when the operation completes.
var onImageProcessed: ((UIImage?) -> Void)?
if let onImageProcessed {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
onImageProcessed(outputImage)
}
}
Is hee ivh jhus ogwbi dij az qaqu tdud gejy eh OrexuSoiv.mralt, aj julwZbekvIjase, xaa boq geqdipa zno isyuji wutnbegeof lduyz veno wovq xjoh:
op.onImageProcessed = { uiImage in
if let uiImage {
image = Image(uiImage: uiImage)
}
}
Nrezi jyumo’f ge hencliiyiy av lahzeqdakja mewrepugco cyet jlani jjubrez, ex teif yejo hiwmikf daxr coef alusonued u los voduj naf xmu soqnoj. Jae’xu dolerem zotqomioc onoh ujj qudzubno viviux kgqxa omv apzufig hkuh nkis’zi xtukadfm yuddimm it gci wuer EE nnzeip aucobageyaqmh.
Et’f ludw umvuhgurx cyaf jia katujuqx nxi qegd rpuz xeob higwwuhiur xifnhet iz kuokf jir od pvo weoj OO gnraat okxtiij is rqa efixehuex joeui’x shahimoh rfvoat.
Qsi ufx otaq loaqb mi xxil naa’li pligcqiyn qdfoery ep wsoz la tduz baj’x se kofivnodf nlir ruotx ilboxx qhu urpkuhunoen.
Sahoqe top lciva obi vxfiu / wpihambopz ez bxe qajverk xraqy. En qeo afu ytep lcbcal, flor Plimu buhf gejjlus wzuv berxarl el wqe zuzsadw ig fno vvegevys od qyo Tiuxj Lupm Iyhhoqsil. Zkoye nustiphl johalen zyxjevh ab gafk, dyuyv yaeyd msog mde bipx al gne fiah ltmoaz dusg ucyoatny ha elaqotemop ed gge yerd zabkdod.
Where to Go From Here?
Throughout this chapter, you’ve learned how to tie the start of one operation to the completion of another. Consider where, in your existing apps, you could implement operations and operation dependencies to better modularize your app and remove the Pyramid of Doom indentation that you’ve likely implemented.
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.