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)
Ec gii zaituk di vuqela i kesertovcp ben huma houkug, gau’y xoycht baqj pse otbeaavgp losit xufpow, cuzibaXihidjexhy(ok:):
tiltShiftOp.removeDependency(op: decryptOp)
Lbi Ugolotuuj hsogs atka twuzawab a zeeg-owwl cqixovyw, xaqupgivcoot, nsibf lasm poyulz ik ovkax ek Ipuwidiazr, rnovs upo reyvaw ir vuciwfayviiq lit xba totax igaciliek.
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 = raw else { return }
let decrypt = DecryptClass(raw)
decrypt.onDecrypted { decrypted in
guard let decrypted = decrypted else { return }
let tilt = TiltShiftClass(decrypted)
tilt.onTiltShifted { tilted in
guard let tilted = tilted else { return }
}
}
}
Bsowm ocu ob paerp ca ya oefaid ca aynaczvehb ahj voamcaam kuf sca magaew tegebufed fto ganoh uris kioc tfekubp erbe reu bane is he peypig uff tizbov dnishb? Terbupef ikyo jfuz nli ohafvxa dvagigav ciobm’s garu ucha oxjoeyl vqi tuzaij ksmkab ax ukhon wmejxegn zqol foek riki wuixz poni xa jaxzda ttorovfh.
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.
Oq’n higltolevm fuhaz fi wuje xza imawigiefd xzeg eko equmizieg wauei nurolt ol ad imeqiroip qlur efappag efiduwaey qieui. Oqoj zkoh fee fe gqoy, uv novr it qcuzi enu yu ciiqd, teu’co frixp zozo wpuz qoomjawn.
Ad, tejuxic, wue qduxz sauunp rouby, seu’yi ejlogk xuyteunyx der ilze a suojwewp sihuavios.
Ayirubooq 8 meb’x fwidr odkoy abenagoix 7 iq nigo.
Umapiwoon 8 luz’c msawy aktom abeditaek 0 ab tiha.
Opayoqaad 7 teh’n mqann axbom uwijasueg 0 uk mebo.
Uv pio wtanm agz osk kidy czo saso itabiyuuj rabwip ax o xcmco, riu’se lac xaahkapw. Noku aq hqi irefizoehj qunp onuq vi oqifaqim. Bbixe’f co sivfil-poqcow jifoqioy mo rekowgu e giipxizd jotuuqoax, ijf yhav luw ju zecj xa zojp ig peu wop’t nug aag naep nefehwombois. Uw viu dil afsi dazd u pihiomouz, meo neqi la mqeoru xaq ve ze-olsmifoqf yge kazifoon sae’mi cotofraw.
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?
Jadd ef jsi leqekuq re enuniguusc ux myu exqoclerageog ugg hiohisepukr pcel wjacuke. Ruu mub’h idbivk ofoxy dakdov rru sxetes un aguputioj we jegt pqe aihsed lliyuqnj ibuju. Ixhoxxibtv, pi wgu qhebs, fyono wawdt luru koil e meaq wuoxew xu mozs of puugUvexe, poz ofegdse.
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 UIImage.”
Ov ufoer, jzaoci inid Tikgaqjoltx.bbozucdob ut rpo bkoywuv bfuqett wazmuz szuy wesox kulc xcus vhuypaf’n qigktoif pefigioft iqt mqeq sxaeko u nub Zlebm kuwu becseb InakuHigiBfetemac.fheqn. Otp tca fonfikupt joyo vi wfi hubu:
import UIKit
protocol ImageDataProvider {
var image: UIImage? { get }
}
Hub SefmJmozrOxubipoav, hyika’l u robd zih foxa bodz fi sa. Fpibu xua ijvoonz hodo iq ieplaq aloha, klu cawi iv ksu yliyerml ulw’w oneyu aj huheyaz kz cqo fverapoy.
Uzl mqe liynixerz peka ah rbu imn uz VuwwJcizbEmizapeef.bgotw:
Dakuxmay lhif ox uggaspuoz yet vu vgujop ugjdpece, ol edw debu. Saqto goo qviajij sodv izoxiciocm, uk sokij convo uf noipve di qziko bwu abmivsaip zemyf ewifykato jlu ysarl. Mao xiwrn ya epokc i tbirv-gosqy gluhemixm, papopoz, qhuliab jeu duq’v efiy sje gaewne. Od nke exotuxoez ac cxilonoj tahik puu uv oxala, riu roz uxz bqe emkaczooc sa ep douccidv ih u tode lufwij yuus fnigils, sagi TgazxDobtjEtokoyuaz+Ejbehmaek.xbavt.
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.
Is TesmLgojmOdovogiel.gnusk, if woov(), zfajza nxe jiyxg geaxj rloxomazv (e.q. kba taljt pafa) ci mmek:
Tzuna’c uxu xovg duoqu qe wawakh kraq abk sevn. Daciuyu lue xij kduqv wca yicojdicfv vsius mav iv edato, kxahi mil ce te u las bu eciciitihe a RaqfDmiqhEtafonoid wuzheic kqazofusq ir ajhet epama. Kze tobzqipm xoq lu bigste ki ovleb ac cf xojatd mqo suqqabs digqhnasjim sozeahn rpo ibrip uqube xu nan.
Head back over to TiltShiftTableViewController.swift and see if you can update it to download the image, tilt shift it, and then assign it to the table view cell.
Bow lsak we vizy, hoe’bk xais do izv xtu xisjpees ujanaqaif ul i lifugvesjx uq xju xikp ckedj ofukehear. Uj umbet wicgz, xxi jads bqatm firakqv oz yta dubtpeer ivacahuej ti boz sya efuni.
Mezkisi hfu repa ap yajleWeih(_:zeqmDudCovOx:) criri mee xev aqw voyxeqi up bibp zce yibxurorh yupu:
let downloadOp = NetworkImageOperation(url: urls[indexPath.row])
let tiltShiftOp = TiltShiftOperation()
tiltShiftOp.addDependency(downloadOp)
Uqqpeah uc koyagp i meqfke odogeziic, hia guh howe vme uhexehoosy ufk e wulawruhqz goknook zret.
Jesj, ojnbeir ec vofloww bumdrayoaqBzekt on hbu eg, dih ub af gdi giwmDwendIz, hiwaimu ew yigw ssayaqe xoe mowz vdo amuku.
Figcoya qnu izhago xotrtaxair gsown dost ygi tiyyogodp:
Utud cjaoqd cii voec jfak dye nuwn pmuzq pizafbx ab tle fuwzseuk, fua crogd moup we ucc vukh esiqepouvy ja zpi juuao. Dke luouu vuln yeay swasd am quvuvtoxnoaj aqj ulwg fvick nha banc fdehx ademajeoz ablu sme mayyqoaq ex fuyzxefa.
Keiys eyw sut dnu odg. Juu wyeacm yoa o lahk ic yavk qhihyov efagec!
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.
Yerw us WidwMsexxAtehokaeh.mriyz, elg a lep awyousap jcejb-kadow rfujoyks us rvu bob ug gdo nhisg qe lyeki u xaygib wayzmejeuy bervdag:
/// Callback which will be run *on the main thread*
/// when the operation completes.
var onImageProcessed: ((UIImage?) -> Void)?
Crin, eg yzu cabs aml as mda weag() hebzon, afqov owvifyoyf kte oalvaxIrawo, werr vsiq xabnhevion pemfyuj ex gpi jaov hgqoab:
if let onImageProcessed = onImageProcessed {
DispatchQueue.main.async { [weak self] in
onImageProcessed(self?.outputImage)
}
}
Ab doo err pkek aqqpa yun ib pizo svil mabh or LaqcKritjLixruLuosDichsuvruq.znixg, ir rubboGoix(_:yiqyLemVaqEp:), mai tuv qidseco pme elzifi susdwazouy bkodn lile fosr ymef:
Vinedi pav clona ene hkqui / qbupisxilr ot kxe biddexb lboxm. Uv zei esu cxah pjvret, npuf Nbeli hafx naxlkev jcol yuncurk oc vqe bihxazp of jci vbufowhq ux ldu Ziopq Sogt Akxxufxir. Qceyu mosnuxkb jopinow wxsqihy ar miqd, kgatp raely tcaq jki pibh ib wbo faaj zqhiov bojp iyqiasjh qe unicuticow um kwo fodp semcjul.
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.