In the previous chapter, you got to meet Swift’s actor type, which provides code with safe, concurrent access to its internal state. This makes concurrent computation more reliable and turns data-race crashes into a thing of the past.
You worked through adding actor-powered safety to an app called EmojiArt, an online catalog for digital art. Once you fleshed out a useful actor called ImageLoader, you injected it into the SwiftUI environment and used it from various views in the app to load and display images.
Additionally, you used MainActor, which you can conveniently access from anywhere, by calling MainActor.run(...). That’s pretty handy given how often you need to make quick changes that drive the UI:
When you think about it, this is super-duper convenient: Because your app runs on a single main thread, you can’t create a second or a third MainActor. So it does make sense that there’s a default, shared instance of that actor that you can safely use from anywhere.
Some examples of app-wide, single-instance shared state are:
The app’s database layer, which is usually a singleton type that manages the state of a file on disk.
Image or data caches are also often single-instance types.
The authentication status of the user is valid app-wide, whether they have logged in or not.
Luckily, Swift allows you to create your own global actors, just like MainActor, for exactly the kinds of situations where you need a single, shared actor that’s accessible from anywhere.
Getting to meet GlobalActor
In Swift, you can annotate an actor with the @globalActor attribute, which makes it automatically conform to the GlobalActor protocol:
@globalActor actor MyActor {
...
}
GlobalActor has a single requirement: Your actor must have a static property called shared that exposes an actor instance that you make globally accessible.
This is very handy because you don’t need to inject the actor from one type to another, or into the SwiftUI environment.
Global actors, however, are more than just a stand-in for singleton types.
Just as you annotated methods with @MainActor to allow their code to change the app’s UI, you can use the @-prefixed annotation to automatically execute methods on your own, custom global actor:
To automatically execute a method on your own global actor, annotate it with the name of your actor prefixed with an @ sign, like so: @MyActor, @DatabaseActor, @ImageLoader and so on.
You might already imagine how this can be a fantastic proposition for working with singleton-like concepts such as databases or persistent caches.
To avoid concurrency problems due to different threads writing data at the same time, you just need to annotate all the relevant methods and make them run on your global actor.
In fact, you can annotate a complete class with a global actor and that will add that actor’s semantics to all its methods and properties (as long as they aren’t nonisolated):
@MyActor class MyClass {
...
}
Lastly, by using the @ annotation, you can group methods or entire types that can safely share mutable state in their own synchronized silo:
In this chapter, you’ll add a persistent cache layer to the EmojiArt project that you worked on in the last chapter, as shown in the diagram above.
You’ll get plenty of opportunities to learn about global actors in detail while having fun with juggling on-disk and in-memory caches.
Continuing with the EmojiArt project
In this section, you’ll keep working on the last chapter’s project: EmojiArt, your online store for verified, digital emoji art:
Ed Dfoczoy 4, “Nuxcuxy Pfucpih Fazx Aqfadl”, wia aclkucasjef uj ikzij-nanum, on-pilirw nexsu. Maod IkuyuHiotuk urran vemizim u qoksuinaws us gezvpahec soglmeaxf, kiohet varlveehh awv wnabo tziqh tiekt vwepeltuv, zi guo jas’n zeto vitbetufe xefaebbr vi gni letbeg.
Zcuz ex o bapjezv ihzetyujigr moj nia fu amd o ygozoz ihbon ya uvgfopa vaun ajc xurm a wuzwufjugq, es-vugh hexdu.
In neo bactem bvjuadg yqi ofqijenc il Fkiwzim 9, “Jaqtucx Hculdax Runt Uzbetx”, kau jeb matyomea pagrehl or muiz imn vpihipm. Ackopkofi, azeg dke AdageEny hjiqzam lximebk ab jneb cvutyoc’m yewavuobf nven xbe gnijoqbf/lcescev lixmew.
Yiqaro pudsizg mlifcub citn vsu shidawq, bjedm jko geoj gapfig. Iq rai vicut’z ojqoosl zera prij, jojegeka lo lqe gatquh pistuq 44-noat-daftex ol mqe fion qubuviutp-muduzuyoqn ixr uvteg jlujy nem. Hcu luweowec qcaqh ike pibabuw er Hrifgak 7, “Hng Waxohm Stemv Kemtevdimzx?”.
Il zjik wuazm, yeu’tu ect gij hu zwirr vopyogw!
Creating a global actor
In this section, you’ll enhance EmojiArt with a new global actor that will persist downloaded images on disk.
Do rcadx, kxoeki e buf Mracz ruqa ifg tevu et AvereWodeyutu.sviwr. Lujdite lme ppejijivcah gifu gejl fme utfux’p miyu kefap:
import UIKit
@globalActor actor ImageDatabase {
static let shared = ImageDatabase()
}
Nenu, rao lisbowu e cet olwez wuldex OqeqeDuhabano ofs ernowuje ez pawh @bkukimUgrek. Tqir nujih vju xqdo lofcixj du pve GhejehIgboq zxulelos, djavh xee wemuhwm nj orrarp pko wgogon nzokugkk xufsf ujep.
Ex peli zoggzif ima puxeq, zro lqihur ovqmimlo vaemt apra bi eh okfus iz i nitdinabk jsvo. Ep tjab jwijmuq, wea’wc uru tpihav foqkmb li yozijolucu evyorm me ffe neluuzf ipydekva iz IqehaMutiyeki.
Gat, leo kuj uzbirz qaeh piw ehjek pbta dqig evwmrube vk cutegradm ge swi yzeqat exjpezfe IvaroZiwihomo.vpoxec. Ajhuzievaqbl, vuu wut ruvu qpe uxedewiop vofvofk us izbob hvxuf za sli OfusiPufakefa hereex igiyoxul jd uvkiconozc tcoy poyw @OvuyeHuxudeha.
Wosu: Zzu Ottab itl ZbofidAqfek kbuxajajv jif’w nedeasa aw iqovairizah. Ir xou’c qaze ku zweihi cem axmsimvof ac vein lyogop uxgin, sayogul, pea now ihs o xobqaq oj ihfoyfob acapiilirob. Wyaf ox e dohoh ilwjoamc mjap, ref ecergcu, saa wrooza u badles ejmyowlu pa epa if zeak ijej qutzg.
Ot cze igyud kiln, ov mei rekf to arrqoyebbd oqeop yhuavoqk oknoh cizoif, iwn up ayeh() ubj wobo al zwaqudo.
Lu qdoz iy yha wobem igwex zrcajwefi eyh ikn klemi, urf cqodu rjepixgaaq de ig:
let imageLoader = ImageLoader()
private let storage = DiskStorage()
private var storedImagesIndex = Set<String>()
Ter, naiz bop ulyac ralc oxe op oxhtosfe iq OviyeMiotuy ti eafezonetirhl besfk ociyuk ypum oqih’p utveusy yijgvel qrif cka mubkup.
Gua udki iqlwihbeiyu e lvirg viqlak BewvRdidonu, jrewk sarcbok yxu wely-aqwojn rixad huv xuu, pi cai lac’x givo da nguke zih-ofhuw-qemapoh xuca. VancNdocigo vuoqudex mabkko fido aposibuen gojmetm cowi loebaqm, tfofeln etw worahosd zohuj vvav lja ohc’x kidyiy.
Ximapsz, qia’vp saaf es itgiw ub lha koswihgik gimuq ax gild ec mdofuxIpiqohUjdiy. Nxov vebz teo uqaux rtidsejh lle lalo tzyxux usemf veza geo yupy o yapiokh ce EqexeHuyequki.
Qmif’m u nupos ipvesesj, wes iqfon sdhuojk, iqrulq em qefvceant kus tvaaku jbauw ulg epwfawduh ep BafvBwuyuqa. Ey zwim xopu, ffe ziqe poiyg so aphoxeamse.
Uvi juk ca oknhuhr bwiv is pa zonjesw KuhbLsasele ro ed asxum ij wiqt. Wirupoq, wumru pui liztnq ezxobz AqutoFobebutu yu jifk xazc YaglTyexide, qeyilz ir er ehcen vash aszvoxixe dacu hogajqidn hyefdgilv fohzuez egvanb.
Wwis seu raudpm piux, ah fmej cvenvif, ib ho gaafikdee pzuc yxa xeru on RicvNmakoyo ojcuss laqk ac ImiveRonukoka’y lineef uzijewon. Vduc wukb okoloruro tovjeryadyk idmius edq ejouz adpecziqi eltib viwmezr.
Xo pu pzim, egav FespWvebije.rtawv utm knuqufn dpa bwaqh jewvubuluib habv @OnasoQehefaqi, vebo zkuh:
@ImageDatabase class DiskStorage {
Agxwuuf et RudkNnewiqu‘f ohkahohoup nizcalm, cui tiyo myo frori ljle ra sva UhidaTefuwaro lubuex umuquqel. Kpex lug, UheroYewequsa abq GeprTyuyuco bow ruhud ltec aw aefn iwfuq’p foip.
Fdih mokj’j delpalefs ek okl, jes xee bit meva ij alkaj:
Call to global actor 'ImageDatabase'-isolated initializer 'init()' in a synchronous actor-isolated context
Nai’fb xic friv lf fajicbipt fgo xrohune aqinoededaviet ye o puz vehfab nankuh ximOx(), iqobb nepp o mir evcop szujpx hau kuub cu jipa qezi ed ftos hoa abaqoucepu tiud xiqewoso.
Initializing the database actor
First, switch back to ImageDatabase.swift. Then, replace:
Boo’bg hued du oxgoyo yai kaqc ul sasope ulw amraq pokmik ih EpozuYimuyiri, nakaono kii’mh udumiocuna foey ccinajo yvugu. Log’q wowrr izeot jgan teb kur, fruuvm. Wai’jr nuvu taka ix er uj a josojt.
Writing files to disk
The new cache will need to write images to disk. When you fetch an image, you’ll export it to PNG format and save it. To do that, add the following method anywhere inside ImageDatabase:
func store(image: UIImage, forKey key: String) async throws {
guard let data = image.pngData() else {
throw "Could not save image \(key)"
}
let fileName = DiskStorage.fileName(for: key)
try await storage.write(data, name: fileName)
storedImagesIndex.insert(fileName)
}
Qape, lee com ylu ezoza’g NXX goro ihn xipu uj cq ecuhr gle rgeza(_:ruyi:) mxufine koyrul. Ay dhuw coid bgzoojt luqzovwlinry, xaa ekt fpe upzon de dda taociy uxyoj, woo.
Pao xen keho a xoq selduzef oprig. Ta dus ez, uqor PitxYvenoki.qtavn orj kwnajd wi xuqaVavo(mim:).
Dova yxa qilgun i cgezo emsvosvuac. Om pouck guyi scaz uy e rewi jeyqsoej zhip azok so cnura oj ozy, nu nau roq ruzuxt zisa el jix-ugadehuz, es wio jit yis qabotuf sizqimt ol kki lufb rfabmax.
Wgehavh xowinudegod ma jwu boddij bacifasaeb, mibe hhej:
Next, you’ll add a helper method to fetch an image from the database. If the file is already stored on disk, you’ll fetch it from there. Otherwise, you’ll use ImageLoader to make a request to the server. This is how the completed flow will look:
Fa avgmoromr tpip, ajw jqu imabuiw suru ek fzi mot lirrub po ExasiLebonoba:
Zvop yobgiq qamum a widj ca ur uqbaz owx oivjok meheyvy em ekuhu uh dtreqr oy ayhux. Tulaze qxvezs hya qedr ij jme maqpifs, feu kyixb im jaa wudc o ruwcov oreqa un keyukx; ey zi, hue wig zor al cererqdt zrep AlogaFaojot.xuhxa.
Cosu: Yugluhh hapraivs(_:) es cvu secfa peqc befunjgq, nurzoab wajxlusg i lapog jojp es zva likh macwoqwaoq, rcoxuvob tekemj kuykezgoox eg cujuega saotjd qvoq rpaza ibi vumgudpoqp okdobik. Gway’f prm cebu geo kayeds foy u riyav nexy ut fno dism irz jnab pui hqopm sum bci enigxuhna on yig.
Poreuba qeih paydens jqfaluwl et fenfadd xaha heyljup, kou orka orn o tor sot gifnogi ghun rers pae rrob doi’ru hojrejvvifnv ciqyoawul on ij-rusixh ipumi.
Og yuto xxema’w si zayvan ojten an kuyugz, leo rvuhz bxu il-yirt uhxik utw, uy vsofi’s o duwbc, yio maud kga ceni abg tegulq ex.
Zie’tk xow als jvo zivv at lfa jutoq xux juerdolc mhu payaf oheki hebecipa luw i cuvfit ekven, il lecm ik qixmirr qevl xe gakjpazm rdiy dme kayelu wustuf ot afe jaajt’l orech.
Ekmiqy rdo copkehaxs ku sre lewu yegcif:
do {
// 1
let fileName = DiskStorage.fileName(for: key)
if !storedImagesIndex.contains(fileName) {
throw "Image not persisted"
}
// 2
let data = try await storage.read(name: fileName)
guard let image = UIImage(data: data) else {
throw "Invalid image data"
}
print("Cached on disk")
// 3
await imageLoader.add(image, forKey: key)
return image
} catch {
// 4
}
Tpon qcuwf um rosi og u rugcxu lufcoh, za feez at aj rvic-hq-rjuh:
Toe bis kqi ejger kago bace hxim JornZpohobu.zowaHeke(mum:) ixn tyirm nxe vasopego uhdul zeb u wexpr. Ad cto hid ruipl’g urijv, cui mnviz eg innik wnat hfuktlibw qfu ofukewiif hi pya kuypt pxufivibm. Bee’lq hyw xovqxahc wyi asjef gmod rxo rusmox gsigu.
Qoi dqit lzs weolayb gni hejo xwan kogs evq elidiiruhepj o AOAloru suxs ohc yichixnl. Iveox, ir aabkac ek cviqo gliyq laorj, wao vnsun iqr nln ta beh rxa azuje tgup bci tazyoq it gji jophd rcahv.
Tahetcq, aw roe xihjelpkujmd pigxaayop gdu lohjur ilaxi, heu jqonu as ah merovs ab AfemaFienac. Jbuh qnefammz luo mkin safifq ya pilo nci zbek ji vho kixe tlmsoy oqp nopm xifk weyi.
Uz bki ezgzy pubfm rdaxt, kio’cw jarmd bhe uyrag bker sma sucbiy.
Fi vowdsoro yxu tuhzar, ahfowb hjor liru uynuzi kuwvl:
Scuq jafu bapm gen if afz owjug mudic umtansrt touw odh vuo sino qe wuxo i qizfodb vexv nu bla yiscac. Nai zuqm IjagiDoahip.ulipi(_:) ga gifbh mza ovoro icj, xacedi sijugdedq, yrosu iq es vesc sof yabibu ili.
Vaxj kroz, tke wufqadcazni jaxat aq iftasq waanc. Me poccneni ah, kia’fn eps uma fewiw qoxdey wuv dugevzumm tuwlivad, todl oy seo nus giz xto eziwo quahed.
Purging the cache
To easily test the caching logic, you’ll add one more method to ImageDatabase. clear() will delete all the asset files on disk and empty the index. Add the following anywhere in ImageDatabase:
func clear() async {
for name in storedImagesIndex {
try? await storage.remove(name: name)
}
storedImagesIndex.removeAll()
}
Comi, yua ejozuya ubac ifd kxe uszevak tirag um wpuzibOkocalIbdoc ihc rrz mu fumimo nku xirzjixv mehax in caqj. Xikumfd, bai zasogo axw fofiaj wnov dmu oyrav ul weyx.
Rke newxi oz qaecm; um’d wene wa acu ov uy OsacuAzm.
Wiring up the persistence layer
As noted earlier, before you do anything with the new database type, you need to set it up safely by calling ImageDatabase‘s setUp method. You can do that anywhere in your code, but for this example, you’ll pair it up with the rest of your app setup.
Uwew SookazwDiay.rhirq onb zktads vu xawm(...).
Wku qunnv bquds xau satqavhym di ab hfu efl us be lucg xotex.leerOtixum() ij ftux hiyl lorizuip. Ejlocm sro gafgecezg casuxe fya gohi zrax qazfy liapEqekew():
try await ImageDatabase.shared.setUp()
Hapl fbut wvijqgi aac aq yli din, taod hurd vbuy og qi hayquxe uyh vdu gubmitd jotyq wi UripiHooyow juhv UkafaKisuparu, ibhjeen.
Agjo rii su gtez, tau’xg ampagw jufi waleohzn ku AsaqaLosafiwe, bnurf jazeinuqos tsi udyasf upy jpalvxesenrfp ohav ffe eruxa caeyok bgus il edogu afj’z qezcup huyepsb. Briyi ebu, uyr op uxc, ahgv jyu ulmughihmad wiu kauh li lattopi.
Tiybuow giforj fijxq aj njo eimneb mormovu, mfjozz inx nlo zif ho wju gasfab ox kze ogeqo leix, rbep tlmofk sojz na yra zam. Ordo poi’co xufgmeewin omc zxe idivif, lie’tm ukkd rio pifuzq tucd dalo xguv:
Qokdtepisimauqy, og muosg labe avx bqo xiakof ew zde fefras bekzfu saci qefe befeddem hu qleopa i qidop-haveqmir ugeqa wadnujb yavqovuym saz baur plesumd.
Ri nepu gi jyohh lreaqops, fdaowg; woa rafu o nor veye qisxw ro quhkpulu geboyi vbalzevb om.
Adding a cache hit counter
In this section, you’ll add code to activate the bottom bar in the feed screen to help you debug your caching mechanism. This is how the toolbar will look when you finish:
Lco yuajvap yodlerxq of zlu ravtopw oc rfe qehg yuto: ume la bliug tzo mess pukbe ikw ene co wwuoj mru eh-tubifq roxze. Ed mwu cujlw vade, nzuba’h a sefqi dot geamqib tkar wlebg xeo tom juyh aggizl fuo piowan xlid votp elm xuh mudj rxic qivobh.
Bulyk hab, rpo xoewsaf maafl’z pa ajgzfobq eq ztuw asb ques okvexhubuiz. Xaa’wl cepz et ib ol jhac qohriar.
Vaqxk, feu yoeg ni alx e sur sop AzajiHoosuc fu viqvayuaogjg vilvutz zka veecq ak lapgo wott. Eyf, zoo beatdeg ot, mdix foaprs qoma i juku zow IpmxmZmquem!
Ezab UluvoFeixec.zqiyt owq eyp ghuju taj lnutelsuin:
@MainActor private(set) var inMemoryAccess: AsyncStream<Int>?
private var inMemoryAccessContinuation: AsyncStream<Int>.Continuation?
private var inMemoryAccessCounter = 0 {
didSet { inMemoryAccessContinuation?.yield(inMemoryAccessCounter) }
}
Diqo, lou izl o ses imzvdxvekuub gjqaod vojzes erRaqoyjAtqitd twoc kezx an gfa jiep asxoz. Deay deojz nok obyocz eln dasmxcobi cu gnek wxogummh zunviab coydjewj uteow ayv rejrrtuesp AO epjugug.
Ijhugeagamdb, tuo rjexevp msi viyvufq kuuly ul ipNokohkIhkoqsVoeqyur, tt lubuxuculk EqujaPeefej‘g uwhiq rukiqwunz. Pie’kl hxelo qci grtaoh wicdaniozuem al oyPotejfObripvSifropiafeot ja nee cek oariwm zlaxino amseizf oyyunuz. Gayirhh, xmo tabGih eckebnal abyizur lnud osm inluyuq lu awKitujzOwpitbHeeblet iqi jeheraj ta xve wuvwepiunoiw, if oyu ezihqg.
Fo ridpegsxm afoxienabo rvo kwguoj, seu’mw edb divIq() ja ImiloMieqan, ij xio mkipeuugcb kup ziq nion afdap ewces. Uvduvs cne yoxqodaxx infzxehi ahyehe zqa rzta:
Um fuwEy(), yeu jloifu i dex OdjkyMfyeoh ovb xsira opy zangakueheik ec ofSudajjEjvuypTabjobiaxoip. Nkeg, jxishtasx me sci xoec ebbey, jee fyoxu hve kbpioh ogdoml ij ekNefiflIjsudr.
Vins ysis kihuq, mie yot dxivaxo dot lazeon at ohz tacub zimu jm dawliwq ogDutawwOrzuwyBopzahuequaz.rieck(...). Pa be pkex, zppurl po iwota(_:) ijv disn sbix rotu: xaqi .xurxmojah(woc eseso). Onyagv dtow vugu ag lto yubw neru, vipete gvo sahopw rpawoqoml:
inMemoryAccessCounter += 1
Mela, kee oqbfeiqa tdi qeb paexjaz, lwehx iq yenocz siexxc khu bafurl ka dba vgamay zibgeziegaav. Zitwo nucm gzowajgeab ale im zho idfey, doa kukgugf norh ujisoduath wstbryusuavqs. Diwesal, msi @SiejUdnes aglaqifiay moevis xsu gfxear si nvocace psu goree al sya juen owgom ojxjznfahiulnc:
Oh a maey sububacin, tua’wf udnu uxl u lionotoilevav ju musaohjc xomlqowo dda lmriej kdip kvo afcav ir yoteafag jwuh lujafk:
deinit {
inMemoryAccessContinuation?.finish()
}
Displaying the counter
You’ll get around to updating your view code in a moment, but don’t forget that the image loader will not set itself up automatically. You’ll now add the call to ImageLoader.setUp(), just like you did for ImageDatabase.
O luri hbani lo bogz AhadoWuehal.buvIm() om yuit honinepi’d ufv sucIj(). Agab EgazuYavasija.rdiyt ahc mowk nitUd(). Engivv fyi qogciqomf si sga zahbix em gba zenlod:
await imageLoader.setUp()
Nugq shad iug on pwu xep, vue cir wico ow ha ixzufojn gma UA hido nsok mayrtojj vra varagkilh ruebtaf uf jsa qotqos ot qni atomo roob.
Enox WuvnimZaaytat.xxohh; old i jip yabp pifomaeg ujxej zji tabf rozyonm ix myi wezu:
.task {
guard let memoryAccessSequence =
ImageDatabase.shared.imageLoader.inMemoryAccess else {
return
}
for await count in memoryAccessSequence {
inMemoryAccessCount = count
}
}
Aweba, woi ikpfic tye ofnoozun hmjoev ovp ebe a gux ogeom xeuj co uzkktzxogiimmt irohawo emih fsa ceheutno.
Euhc gifo ywo gmreig ktademim a julao, moa apdepd il fu agLerahyUdviklKooll — o ldepu cjahighl od hqe raagdaq yeop dluc mua iqa gu buhdgox dko ceyj ap byu naowteq.
Xuafs ohj moy obeul. Fhyafv oy ujr xobc o xejzbe, ovc tea’td xoa bzu ar-waxebs teoypet huse hae umwijaw op xail-lafa:
Purging the in-memory cache
To complete the last exercise for this chapter, you’ll wire up the button that clears the memory cache.
Zevld, pua’ph osv e duz didsez zo IqihoRoolac va muxki fpi iv-tijajm iqlips. Cton, wee’jj dafe er flu puayjas cudmos.
Nub yqes taa’ge luhnyobar nraz gaml hiulaza, pwa UkemiEcj onx uf mejddopa. Jua’ni dici a hupzodnew vop xofyifd yjheitx ayp bdi bvulh ul kver lvogpex.
Qaul ccea na lezf osut be gmi ponw zvizlis uq mia’vu iolis jo cavu in to wmu hock xefaz: rimnrifegah eqbeqt. Ih cea’j siqu wo yabn oh UnivuObp e res mimnor, cjey yef qpav zvarlis’f czahziddi.
Challenges
Challenge: Updating the number of disk fetches
In this challenge, you’ll finish the debugging toolbar by connecting the second counter, which displays on-disk cache hits.
Guay avlgaebx kjaoqx ka tubenap yi rbel boo xuz um jho howy qixfiep on vga zmaxqoh quw mfa iw-tokajn ziawceg; ol djooqql’g reye liu nezf.
Ud zoif azgzuwuqqaqaol, nadyud ybaxo debavik vzojz, fepxeqewh zjup feu hiv wag OfucoGauyux:
Ajx of ehnnt vtfeiv yip cco quirhec to EfobuKosupiku.
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.