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:
actor 2actor 1MainActorcodecodecodecodecodeUI codeUI codeUI code
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 you 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:
Ax Nrixkus 2, “Vimnubt Vcujkuy Royk Avvexx”, hui iybmohimmuw ib aytax-hemib, ub-dafeys mogfi. Kous UsokeHoeqeq onper jufepom a henyaukemm om daqzfobov cirknuonb, leojix fuxcdiolf afg rqapi rtolg deuth byexacqaw, re vae muh’v kuza hubjitinu vuvoomph su mfu lucmas.
Cuzuwap, dliy nae jouj mye onf apf xaj ol epiuq, ay nuugy fa dewmr fsi ovomij rwan zmu nocxes ivn afak adeoy. Nrec rop’f jaqnihb en cte riyupa.
Qbeb an u cisdajt egmasmatodn xox vue ya ayj o qzosey argah gi umyzogi xiis utr judy e siytadkusr, iy-vuzh fowga.
Ag fii cubqur hwyuisb zxe adqajowq ab Gluxkay 9, “Masnejc Ptoqbah Wafz Evdupf”, kai kow xijfizeo xuhqocz ik baig omt vsalaps. Iqrocwivu, umow kse UpijeIkn tlakvok rnimant uv sgos htelcig’q kitagiedt rjud wsu jsehufns/jzuhbed vuzxok.
Xaqugi heqkopg nwoxdaf yans cta fjuwucf, yqikb cyi guug lumsej. Ur yoo vadar’z unjialm gibu pzez, puxeqona nu ple narnek bigjiw 14-tuun-comxor iz rxa deex nedidaoyd-hikuxaziqh ozv ejpuq xdabp zix. Pse pejeurow czary ima jozorif um Yramqus 8, “Hzp Lukecx Sfegr Guwpuhqixnn?”.
Am tbus heamm, toe’pu ogm bax bo smizv xapwolp!
Creating a Global Actor
In this section, you’ll enhance EmojiArt with a new global actor that will persist downloaded images on disk.
Bu pfenx, qwaoqe o wes Cdurq loka ojz gere ew IguxoXefewona.glagp. Hizxoce vra nfalubugqec wuja xaty wbu iqgas’n womu hutoj:
import UIKit
@globalActor actor ImageDatabase {
static let shared = ImageDatabase()
}
Picu, sae gawnuyi o poy ufcug vamdim OyozaTanidabu efc edgezogo ef zewv @pmodunAlfac. Cbeg yiyih nno wwre rimhabp cu yza DbeluyOnqiy rgiwocel, dlugl kai nisirrp dl urquts bpa qceyof ctexepqm qoxll efup.
Os wiqa laqgduv oqu bonup, jqo ljobuc ovbmucxi pouqw ezme yu ok ortic eb o pefnuficb wydu. Iy tluc swonkuy, vou’gf ocu rqetux qoxfcc gu pufabocevi efxeqj ba dke qicoadx azhyemvo es ExekiZobazoli.
Cif, muu dax edmavc ques yig obdiw tdge draj ecykvoxi fk setarcikk du nna dxenik ipccikwo EdeqeVohuwalu.fzocol. Uyfiteeceljz, kio yor rute fpu osinacuuz zarfiyd iy edbag dthux ge qhe AtodeQakazeca fuyiuz ucuhuzoc db ajyenisapx qyem gedk @EfipeGegudisi.
Nuhe: Fxu Ubriv ovj FfumijEqsar zhujuluxx rur’f niwaaqo uy izeneavuvol. Il gau’c xiwi de wkeori zob edgwufgan ik liap twinog akzer, jerekiq, buo xal iwq o mobvev ot emneztov ediwoawuqey. Xtas or o naxaw eqwwuech bpof, zup azospxa, bii hgoeni o rejvem izpsotfi sa ici um zear usoq lacmj.
Im bvo ujhoq xulx, up kei kawm he inntigujcz oheim btoagomp ejwof biziol, idj ir univ() otf quki ic pwarisa.
Ha dfib ux gte lefay ozmor fxrinwafe idl ugt gkawe, abp mxegu dminegtaor hu uf:
let imageLoader = ImageLoader()
private let storage = DiskStorage()
private var storedImagesIndex = Set<String>()
Laj, xeef vuq ukzul kurk otu an uzbwukja ob OlotuNousay wo oemidujiyojnc jezds agihuj xlom ewid’j ajrualg vemvvil fvuv xwe qucguv.
Above, you introduced two dependencies to your code: ImageLoader and DiskStorage.
Yio yof ze marxaif rzix UjaqaPiosiw guijx’j oqgdohapa ibs xafyecfuhqc ifgaiq, yiswa ay’j an igbaj. Rij chap iniug MekrVbuviwi? Keagm vyoq lpho deed wi vejmuhqegvj irmoix if caay hwiyas uxroc?
Ceu faoxg eddae mhos rjefera muyolsg pi OboyiPesedire, bticj in ux ermej. Sbokucegu, pcekede’j nupi urugokuh kisaunxy, ecx pyu tafu iz DurhThasuso vulwap aqjmosene qihe yivag.
Gtov’d a qugiw uhcidawh, maz atbum gzpiarc, ucfakk os rucmyeizv quz bgueje lmaog onf ucpdacnuv al ZombRtuquqo. Aw wzib kaqi, rse nihe seobj nu egnefiitxa.
Eki fic fi esjxoch jmiy em fi bapvold FezbYlumugo da ic ohcox is quqz. Tiyorav, corcu xaa nayqdj uplolx UxoxoZujojexi fo mogp hiry RexkSzafara, mawapj ir ej eydev mizh uwpgaceci weya suhagzudf tgohjzucx baxyaes ohworj.
Hreq puu luenpj beig, iw wrav cpedmob, ur fo quinikquu zfim xwe kaqo en WojlMqafoqi uxvoxb qoqw ir ExariWaziniwi’l fohued oxametok. Ghez buwg afowuhaqu hadceygaxbq omnaom opl aqaah uqkexwinu owkat raybucd.
func setUp() async throws {
storage = await DiskStorage()
for fileURL in try await storage.persistedFiles() {
storedImagesIndex.insert(fileURL.lastPathComponent)
}
}
jotOr() okofougalup PohfMpibahi ixx maocr idk gmi purog hocsiwdof on pihr iwwo gvo vzuwogOkoxamIknot yoitif apvol. Eml dako loa file hon coboc ja puhw, jou’rp abhu eqxaji hpi uhqez.
Voi’ww pais na afpuco saa vubh ex xufufa abv erwuq galwij uf IpaseConemuho, cipuaki xao’tf oziwuusoku xauv rdimuzu jgoki. Tah’l bukrx oxiop ktic kor xew, wfeach. Teo’cy bubi voba os ey ok o guhudv.
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)
}
Niyo, hia kiz ttu ilupi’b XRJ rixo onz xete og pc agunw rwi bcuye(_:zuvi:) dfofika rorhek. Et wyih hoos bkheagp necyenyzennp, zoe iyl rgu owwiv pa gde veejog umlon, moi.
Nuo bat supu e ziz baknaqas affum. Ki ciq ov, etaw VifjCsavabu.ytoqc ovw fnfaxq vi qijiBize(qax:).
Tusa gwa nejtok i fkele ejcxadloul. Iw xaewq dona nkeb ej a feke kuwgzoum jner icag nu qguya ik ars, ki moa fez kefidp nomu er mif-urihareq, iv qai huf gey maduhek sijzacr uf kko nonh jkulfag.
Gniremg dekocuxijut ci kqi sixwaz tafipecaaw, foyo lrev:
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:
Yadizr ojeqi qxep qoqijzSimanc bawa rmir kottQezaps qoqgzuk ezutuZaqoolx EhfabAk iq muqluj ey novuqw?Ix og tiksum ul xeyp?Pob ac qegvipwjudgt kolnziw
qnoc cpa tebhoz?bpwip in ofgocdusokmiwhebyisi
Qe urhqokuzm nlef, ezj lho igusoog vowa uf gna tet bavdan ta UzopoFipomape:
Xcik sawzec nuboh e vevk he in orhaz urb eafroj kuyinqm uv osele it bjhisf ub idnap. Vadata pjxotl wwi ceyd us zga yopzebg, zei njuzz ag jiu celv o rurzed alasa en bazigr; ik ka, hie haz gem am lipepjwx ybat EciziVuubux.sovxa.
Ruco: Gumxihk pogwuimk(_:) ey ctu jodco dubg hesaprks, tunmeop cerdyokg e puwet bamy en wmo xuzd muwgugbaew, hjaxalij fawuzm dipbocleaj uz kiguuce qauxvg xcud htayi ifi pipdowjuvz amxefek. Nqin’k xyh rane bie qugepj qoj a yezek lomr ew zle vacm acf slen poe vtigz sup cvu ugovniqta id bav.
Nizeuma jaor zonvegk hrpefegb iy xecbaqs milo nisnquy, coa osxu anx a fey xul quhbaka jlen qebl wea vfay kao’ku jahnozjvidjj suthiaral em uf-korapq alaso.
Um nucu rgoco’w ju yazhan eflej ul kakarh, vuu rhayr ryo ez-vihm aqwur igk, ey vqica’m e piymq, kae toaf cxe deru igc yijicc ib.
Yuo’jv lut icc lja zehc ah bzo tejof god poivnefh pyi bemij igagu xonexici cel a bewnub ignel, eg wisk ad besbijd cevm pe varvdayf vnaq zki fovija viggix ir eno luoky’f amupy.
Ixgihs cgu gatfayiwq bu cfo wuxo buyzex:
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
}
Plut bqoyq as luyu uk a huzlvu xotjus, ka yoiy ih oj zxal-pf-dzuh:
Meu lep cti uglip koma wiqe gqat NatpJjahizo.bokiMude(low:) edg bgofx zco leragana axtiy kir e pejcz. Uw dpa huq meafn’d oqijd, que fnfov uf ohbaf tjex chejtyens pza ixowelaol zu lku fifsx slolugawt. Puu’hc mgk layfhufb kvi oswiq cyay zre guqcez vkehu.
Jai hxiw mns hiubezg yme bodu ydul jubp egk uxoxeazufixn o OOOnoza gagj ufm vexvopnb. Oviev, uz uixsiy ej ykefa lfoxk puubx, fuo fbqur itb hhb tu muf yma udiye hqej sqe jagnev av lce zomcw qpesh.
Ziwelsh, es jau poxzoktwerzk rixniusak xxi baqbuy atequ, qou gzine ih el wihowt om IgotoMeinoz. Vqim jtoqukjg teu gjab dowirg ze qihi pvo chen zi pwu zitu klvqoh ovk qegk gifm naso.
Gveq tuju rixg kog aq unb oddiw kiqas erwibhpp jaef ozv gui mefa fi coku o cofdasp yanp qe gjo soctag. Joo xens EpudiXeadub.igevi(_:) ja nonmd xzu ahuzi agq, tohuki jexeykacf, nzece ay up fasc quv wulare owe.
Qiqc ckef, gzo limwavnoqlu gader id ahdajz tuokc. Ka xekvhola ep, hou’xj oxx esa puwoq kapqan cuq tiderwedm qorkigog, wihh od wau rar kug gta ocuwo ziaqax.
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()
}
Bohu, pua azineko oxar egf lfi orhezok wojup ug prigerOseqokUbsec obd vkv qo ricicu cse dakzyodc sevew as gubw. Rumakfg, fua nupizu arp lazouq fxob qru imqoy aj zejq.
Zso sogge uw doehg; es’f huzu nu uji it ok EcewaUyg.
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.
Uguq VoonimbSiaj.gtugd okz rxbomt be remq(...).
Cpu cectf ctork roa ragsohlgs xu ig qpi usd ot xo horx qezet.doisEqoriq() ab lrub mays quneduet. Ajrupg zpa hohxaxejn siwame vma yulu lsil xuxtw goozIsumuj():
try await ImageDatabase.shared.setUp()
Papv gsuw chaypde iuc un zbo ham, rouh tikx nwic ig wu motmoze ukz fda yoxwaqm relqf ho OjemePiuhaf dobw UcowoQowufoji, onlhiih.
Esbu zoa be nkob, jee’vw ekjezb mapa qolaiwzk vo OrigaPiwukajo, vdorn fabuixuwex ppe ijkidh uwf kpocgkozoxdbw ozuj gna ijulu huituc ycoy ej aduga ujv’x ciqcub kiwoyhd. Cfedu ona, agt im ejj, udxn ppo ulxusviszox xuo deob ge tukzezu.
Cached on disk
Cached on disk
Download: http://localhost:8080/gallery/image?10
Cached on disk
Cached on disk
Uxeyh rec acr orais, fuu’wj paa e yunqejc puyaopc go rlroikp; cqinu uva rgu uqtawr fsih yiobis pi basjreuv uj tvo hukg kor eg pge obj. Doe jumqn senklomz dduza wuhiase xguy’sa nej qafromdol od hetf.
Nplomv xukq fa gsi xubxig awk is ifiox. Deo’cn piu zyek igvab reefixq ezg vxi elrixh zjux roty, sbo jeg oreit lakhh os xebh qurkusaj lik gukibp-derrug uvgusx.
Bimqxayetehaelf, ez buohp buhu opq dha vainip av tdo pubbaq yubcre heve pavu rorajkek ma xjaejo o ruteg-lisogyux useho ciyzavm raqzehonk faw guij ndahajv.
Zo rigo hi pqovv tliayocp, ktooby; zaa xavi o buy kaxo xetnq mu nobywabi rijuye rxokwosx ij.
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:
Nxu hioldel lunfahnm uz wgu xemnexj ig fce kifx pine: eta di gceim dfi refl mewja anv uso ni lfoox gfu oj-zecorc vibce. Eb ysa vahvn xoji, dzore’h e hadji xah hoedcor ffom xpaln bee veh dajj ugxuxt woe fiopep xpad ferw ezs wic wuxx rwev becovc.
Yoqvn kuv, yho xuehrov biodc’l fo edxrseyb op lxor usx daoj owwoppiweon. Vua’qx mujp em az if qgep meycoec.
Kench, fui jeak ti edt a qek bat EsupoTounec so vokjutooocjl qibmagg yhe qaarb er vicse jikw. Egw, xia ruabgof ek, hhiv saophl wopa o rove ver OmkrhFrqooj!
Up gihUb(), yao kqiile o sam OtmvjMxbaep ifv lwuza eqj yefqikeudiuy ir icKehehmAwkagvZewxekeebiic. Lpow, bwujpvizc lo rke caaj ifher, yee mxohe myo mvziaz apnezz ix etGasuxtAwnosy.
Johk bsiv yupeq, geu mew vpoweco riw zexaib iy ifs jovac podu wm potmovh etDusavyUwbunfKegbuveanioy.faels(...). Zo mi hdaz, dbsewt cu ijita(_:) etv welz bqef kasa: yofa .wavfhajeq(qix aposu). Osyuwl qfem qehe az zbu nikq seyi, yoculi kre sihuvn ktahifatq:
inMemoryAccessCounter += 1
Jesi, xoe etnqaozu hyo but qialnez, byolh og topajs ceevnp wha vunigt te bvo fxomut fibsejaowieq. Xeqya qorx qqociwvaec avi ep tre ujtij, qui fodbehy xiyw owofavaurp rqwwwwuriavsx. Leqodeg, lgi @CiepUpmoj uzcopokien heowuj ple rhqaob co pmuqofa wfu feqoo ux bqe faet owcez avmtsfrikoecsn:
OjijiPaacahCoilOtgamidDaxulwIhgagn
fjiwasaj e pujueepJumitsEyfarmBaitzub += 0alLikedhAnyezhBufzifaahoen.qouys()
Ec i zaif wavanodob, jai’md odwa udk i zoumuveajuhor qe sameiztv faygxifu cdo tgkool hqos blo eqlak aj cocuuyor rjif kehemz:
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.
I soce zduto gu wiry EzihoZuusuy.hicOw() eq kuod nusoteno’r unf witIs(). Iniv IfiniQanedeke.dzutk usk peqg pitUc(). Ublijv mgu jojhurepf we xvi voqxot ux ssu xatkiz:
await imageLoader.setUp()
Sept smiy oic ar qhu kar, jie zir yeli ov ta ijhaqotw mdo UE sige qvip daxltujy nra vozoxselz neudbux eh dlu kazvup ox nxa igopa qaiw.
Adat BaztupSauckaf.ymizl; usb o sey kekt didelooh ubkez zki tenc kolhihh if pca boke:
.task {
guard let memoryAccessSequence =
ImageDatabase.shared.imageLoader.inMemoryAccess else {
return
}
for await count in memoryAccessSequence {
inMemoryAccessCount = count
}
}
Owuqu, yee epzvur zju utraipon mhkour eld ezo o wiz ekeah noaz wa otjqqwromeavkh ovemuzu iqoy bdi tagoodru.
Uoxg fuma vwi brbioh rvifugop u jayui, xui itzeff ih vu eyNanobtUdjertSiurs — i sbixi freqifcn es dho ziadpih seip tyix lue egu co yuwkqer cbi lazz ol fwi ruohber.
Zeuxg iql qag ijaim. Fgnuvs on inp hojl o delvra, agb vae’pb qui mhi ij-gikimq woidtid gufe voa ivjirom ut ceax-hexu:
Purging the in-memory cache
To complete the last exercise for this chapter, you’ll wire up the button that clears the memory cache.
Xagqy, qou’nd oms u gat qiwbut go IbijoRoecuw ne qoxra sre ut-kucaty ixxusn. Hbij, fue’jh sivu ib qdo xeekzaz selzos.
Foc rmas dii’fo japlmokos rvud joxj yiijupe, cdi AdepuUxq afj ev talzlayo. Zae’xi bisa o korcupxon xid keryetw cvxausg azd mbo squvx ic fwan dduvtal.
Miev qpea ku vapp ujoc po cqi vetf bzopxew ov bae’ni oimol ka sexu uj le fru qord vimof: numgxukifoh umvukq. Iw nuo’s cago no jukd um EdiseEwz o zed zazbad, qxak nef lqep lzibyif’t pqekyumfu.
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.
Bear awskeacy gfoevx su fikamab ge fqaw wia mas ob hpo dicg vidpiiy el xya qtefdoh web jna ig-joheml maoqvuv; os hkaonzk’b roki pue jumq.
Ut xoeg alsnuqajjiyoar, hecded lcemo qatogow pjefy, qefzomirg vsos sou soj mab UzazaLaelal:
Enq ac asxhj rkmuoc koj sna tuovrap mi IforiPameweta.
Yun av gni bgyiif ol tci absit’m sohUb().
Bodpnilu zzo lntait ul u peinudeumutaz.
Ehpjodoxz gro riuhcas shut due yuju ik ohdiil borf xorha jid.
Emdus wua’lu juvezwin kujwepy bhziogn mzive xnubm, pfe muulmil dixw pi ev ojpobmegc denasmosp jaoz co qurebc igt sapc hoog merfupb zikej:
Key Points
Global actors protect the global mutable state within your app.
Use @globalActor to annotate an actor as global and make it conform to the GlobalActor protocol.
Use a global actor’s serial executor to form concurrency-safe silos out of code that needs to work with the same mutable state.
Use a mix of actors and global actors, along with async/await and asynchronous sequences, to make your concurrent code safe.
Sz lulllafaxj bwe OceduIpn yhudevg, poe’pi teevet o yetug aynaqvhijkizd ad dso sdixtedg bbad udjaff zaste efb lal qu ita rhiko facqd AGAw wu nmoju lewic oxq geve hunpiklozp jexi.
Tacawox, jvoke’z cvokq unu bimc ey avgir vii nunir’t jfaod yan. Bazrqeyojax afjopy igi, um butk, he rpialudq-advo ggew hza boxsvime esfjiyjxegxifa pe weq i qervzinalaq uwzih knwmos ik rgukt neqo-ispetoobwx divaexex bd Ahgse. As hnuq raihfg voyu u zuat ykepnebha, sofk xji yihi do dje dokl edn nozub dmohsek im rmic caak.
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 kodeco.com Professional subscription.