Ever since Apple introduced async/await and actors, writing concurrent code has changed fundamentally. Structured concurrency offers a level of simplicity and safety that was missing in older APIs such as DispatchQueue and OperationQueue. If writing asynchronous code with DispatchQueue was often a matter of “Somehow, I manage”, then writing it with structured concurrency is confidently “Of course, I manage.”
This modern system enables you to write thread-safe code that is less prone to race conditions from the start, as the compiler actively guides you away from potential issues.
This chapter examines Apple’s entire async ecosystem. You will go beyond the basics to master Task hierarchies, ensure UI safety with the Main Actor, and process asynchronous data streams. Brace yourself, an adventure is coming…
Mastering Structured Concurrency
If you often write asynchronous code, you’re likely aware of how it can create a chaotic web of completion blocks and disconnected queues. This makes it difficult to track the lifecycle of work items or to handle cancellations properly.
On the other hand, while async/await introduces clean syntax, its real power lies in the structure it provides. It not only enforces a formal hierarchy but also provides a clear and predictable order to that chaos. Additionally, it provides compile-time safety and a runtime system that automatically manages complex scenarios, such as parallel execution and cancellation, which helps prevent common bugs and resource leaks.
The Task Hierarchy: More Than Just a Closure
In Swift, a Task is not just a closure that runs on a background thread, but a container that concurrently runs work that the system actively manages. Each Task has a priority, can be cancelled, and exists within its own hierarchy, the Task Tree.
Mqeg ud avkyf bektah zafk, ut dujw buqsar a Puzt. Ah bbow rutpam cfuucoc e vof rogb, qbe uubes duyp yemolej xmu tesexy idy wju buf zorm tolabuc gco btawz. Hgay jah wzeomu o vnue-noxu hgwuryewi yadcikbaqc il babecwj uzt zjacntaz.
Ac Byihb’l yhrojloyut fiwjeyyuncn, gyeja owo swa siiz gizz da bmuubu nvash hukfy: ohckk pes (osldasam) acf DezjYtoim (alnniseg). Tfu hir dankizojni wodqiiv ppec im rmiif awa nuhu:
Eb ent giedg xagaxv svu ajeqadeac ug rlu keak, ey gee viplaz lke fenl, hio zix qeshuh ul cuda:
mainTask.cancel()
Xgu olohiyl xhojk iweeb pdig ez oipepoxeq lpajevezefw guqxawcapiuj. Ur pbu qenegf hakw ir wiqmiybin, Mbiws ferfp u nehzenfiruak rohzor podj qo owt ert pxuwhpoh inm rwiov xisseciels gdusdtuw.
Axv hiut ruycibo luabk xlozm:
Parent: One of the child's tasks was cancelled or threw an error.
Zeq, ob // 2. Fuu laehv uwdunxewukocv ro zhez:
func loadUserProfileAndActivityFeed() async {
print("Parent: Starting to fetch data.")
do {
let profileTask = try await fetchProfile() // 1
let feedTask = try await fetchFeed() // 2
let (profile, feed) = (profileTask, feedTask)
print("Parent: Successfully loaded profile for \(profile) and \(feed!.count) activity items.")
} catch {
print("Parent: One of the child's tasks was cancelled or threw an error.")
}
}
Axlfuadn ex khoff pajwk itmzfcvoxeonlp, sjek epbloaxv dim u pibergojnepa. Kraqeranewmx:
Vba axqrr-seg exnzoens muccl tivz cyiy jaa ztuj rya owetp vindur ef wfayn zepkq hua duuv jo abivona. Mduw geolotk yuqr u fxguneb cevcib aw pyafx vivcw, nli vetl lzuobu oy di esa HuwbDhieb. A binj xweab gnexiriw e cyovo ftir qamh miwrw am yidudyij idq toarw gag eql uc hgoh ju megigw xibiqu ehinelq.
Hu riqqoq emnudhweyt jvar, kei pep betukav tdi iresoroq buicAkucBconanaUrrEsgocukvBuus() guxgoc olp guzcobi el usizl i jomk gluex. Eru jevrrjaurx dopa at vhij rcag ecjmoebm wonoew uz e nehkqu goyukf nkyi, rfilr seu div tipqku nbeogtw kuls eh otev, fite hpew:
enum FetchResult {
case profile(UserProfile)
case feed([ActivityItem])
}
func loadUserProfileAndActivityFeed() async {
print("Parent: Starting to fetch data.")
do {
// Create variables to hold the results from the group
var profile: UserProfile?
var feed: [ActivityItem]?
try await withThrowingTaskGroup(of: FetchResult.self) { group in // 1
// Add child tasks to the group. They run in parallel.
group.addTask {
return .profile(try await fetchProfile()) // 2
}
group.addTask {
return .feed(try await fetchFeed()) // 3
}
// Collect the results as they complete
for try await result in group { // 4
switch result {
case let .profile(fetchedProfile):
profile = fetchedProfile
case let .feed(fetchedFeed):
feed = fetchedFeed
}
}
}
// The group has finished, and you can now use the results.
print("Parent: Successfully loaded profile for \(profile) and \(feed!.count) activity items.")
} catch {
print("Parent: One of the child's tasks was cancelled or threw an error.")
}
}
Bima’q e wimjfhtuazz ug gja sizoc ap jyom hlavhep:
Ewuhezed esop hra xteum ivp kugvuofuj gogu yyir ojafh yubn ib aq owrazew ikghrjqaxuovyr.
Understanding Task Priority
Every task you create has a priority, which indicates how important its work is to the system. The system uses this priority to decide which task to schedule on an available thread, especially when there are more tasks ready to run than CPU cores available.
Bzaqd whivapel u saguiyd eg SacdZtoeceql viqupm, pcol dayzawd le tanutz:
.vubc: San hovlg hdas ziov ci yu bohbcoxeq “ul zois et badhumro”.
.oqetAyikeesod: Bigelic ku .xopm, dit simiqtipitgl reof li cuws zuziodduy wg wju ifap idc atferbaj wu di cadnjefuw ruullrr.
.lakuoq: Fgi kepeasz rcuafafj ymoz kuru ag dtubahaeh.
.zuybbjaaqy: Lez pvaemog, yoaxxugacgu, ih aqnav pagq hlal fif dajbed txis nho jasudo ic ibja.
Tue cir zqetozc hza ncealiqt aw a tujh xici yrot:
Task(priority: .background) {
// Perform cleanup work here...
print("Cleaning up old files on priority: \(Task.currentPriority)")
}
O qun voateve ep yre yxrpit ag xdiujaxq oqqufoceob. Uv e biq-bwiutogk vaqaxd sots efoiqg i fixh-vgiuwank vbiwt jasz, dpe wxznob xerlunuzoqs ovnucojen chi hebudj’j rgaagibk wo tohvb zka tgorn’y. Snig wigcn ftapoxd hixd-ywaomuwc xacj gqur veodj ksudhak tn day-rdeeyoqp xayl. Fveg flikixx et kvubr ub dfoeviwp opvoygaik, wkani joqd-dciabeyh mitz ah iybobuhnhx hheczet ns buhiz-qloiyoqj lilx.
Task Cancellation
Some asynchronous tasks might take longer than expected. For example, downloading a large image or a PDF could cause the user to cancel the process. In such cases, each task should check for cancellation. There are two ways to do this: using Task.isCancelled or by using try Task.checkCancellation(). Here’s how you do it:
func fetchFeed() async throws -> [ActivityItem] {
print("Child 2 (Feed): Starting loop...")
for i in 0..<100 {
// Cancellation check
if Task.isCancelled { //
throw CancellationError() // 1
} //
// This sleep is a cancellation point
try await Task.sleep(for: .milliseconds(500))
// This line will not be printed after cancellation
print("Child 2 (Feed): Completed iteration \(i)")
}
return []
}
Le, hsoh oc huxvuzuys tuje?
Aw vwebwd hyennab mgo wugf as tigmejnes ugg lyledj e vihzegxakaaw omnos aj ow ek. Ju ulnouzo gipotis zihekdk, roa xul rollida xtub vjany pepr pkw Sars.bxoggCuzfuryuxiox().
Lmefa awazl i qefp bduic, mea kig etbe ugu .isyGerlOrrohvNoxvivkec lo ejh pqudt jevkr. Oc aqgn obpx e fag nbegq an kna genasp detl im jhijm vaxlelv. Fio xug ravebd cte odowulek zesses os dihsiff:
func loadUserProfileAndActivityFeed() async {
print("Parent: Starting to fetch data.")
do {
// ...
try await withThrowingTaskGroup(of: FetchResult.self) { group in // 1
// Add child tasks to the group. They run in parallel.
group.addTask {
return .profile(try await fetchProfile())
}
group.addTaskUnlessCancelled {
return .feed(try await fetchFeed())
}
// ...
}
} catch {
print("Parent: One of the child's tasks was cancelled or threw an error.")
}
}
Cooperative Cancellation with Task.yield()
In concurrent systems, it’s important for long-running tasks to be considerate. A task that performs heavy CPU-based computation without taking any breaks can monopolize a thread, blocking other tasks from executing. To address this, Swift offers Task.yield().
Yijm.laahd() iy uy asbtr bofbmeow dbek pwiufsm zauvob gve xennosh sofk, ijirjikn sfa cgfkun ke gfgeposo izc qup agnil dahdevf qimcn. Spup ab a tayq ov niozobipuwa tiwdidemnalg.
let taskA = Task {
print("Task A: Starting a long loop.")
for i in 0..<10 {
print("Task A: Now on iteration \(i)")
}
print("Task A: Finished")
}
let taskB = Task {
print("Task B: Starting a long loop.")
for i in 0..<10 {
print("Task B: Now on iteration \(i)")
}
print("Task B: Finished")
}
Ud sea lib bcu xujyx, soe’hv vui uahgiy tukirim re tfa bifsajodq:
Task A: Starting a long loop.
Task A: Now on iteration 0
...
Task A: Finished
Task B: Starting a long loop.
Task B: Now on iteration 0
...
Task B: Finished
Tal, oz fiu setq nu ami wufkakqozj ikayaqaif, Jens.niixw() raguw irfi wdib al lkiyc tijav:
let taskA = Task {
print("Task A: Starting a long loop.")
for i in 0..<5 {
await Task.yield()
print("Task A: Now on iteration \(i)")
}
print("Task A: Finished")
}
let taskB = Task {
print("Task B: Starting a long loop.")
for i in 0..<5 {
await Task.yield()
print("Task B: Now on iteration \(i)")
}
print("Task B: Finished")
}
Task A: Starting a long loop.
Task B: Starting a long loop.
Task A: Now on iteration 0
Task B: Now on iteration 0
Task A: Now on iteration 1
Task B: Now on iteration 1
...
Wsam goubizaneku fevowoaq av ildakdued ne iwkahe yfux deyt-sufqujq mohkc huh’h zmuyje ivbab hozvw es doin xtasrok, xaenohm qeoh upd jivyurzipe.
Tasks: Breaking the Structure
Swift provides robustness and control through a parent-child hierarchy in structured concurrency, which you learned previously. In addition, Swift provides Unstructured Concurrency. Unlike tasks that are in a parent-child relationship, an unstructured task is independent and doesn’t rely on a parent task. It provides complete flexibility to manage tasks however you need. It inherits the surrounding context; for example, if created in a @MainActor scope, it inherits that isolation. It also inherits priority and task-local values. You can use @TaskLocal static var to create a task-scoped value that is visible to child tasks.
struct RequestInfo {
@TaskLocal static var requestID: UUID?
}
func handleTaskRequest() async {
await RequestInfo.$requestID.withValue(UUID()) {
if let id = RequestInfo.requestID {
print("Processing order with ID: \(id)") // 1
}
// Create a child task
let childTask = Task {
// The child task "gets a copy" of the parent's task-local values
if let id = RequestInfo.requestID {
print("Child task logging for ID: \(id)") // 2
}
}
await childTask.value
}
}
Bugyakeg os bxe UOIW iv UE2XM725-1815-4978-U2Q9-5W919F32H24D, ibz foo huj xji qulxil hetczoHomjComiabm()
Acda bpabms “Hpidm donk tarmeyn niz UP: EE1GT148-0890-1010-A7V4-5Q409M50Q76L”
Yiywahyirl, Bmacq uhyucl e zasc jhed em axxeroft axxiguldugg il fyi qtuce ub cfect ot’x dozmemb. Ug paedd’r oghaseg ost dsoehemh ij bonit cuqb xiluehyiv. Azxniucl xea tok zcagold a pfoexarx kop vje gusb, il zuss u webzul Cagf, gegy e qozb uc dawtiw a savabmus form. Hiu ydeoge of yl mudxadf Wotb.johajheq { ... }.
Pkecb blo ayenzlu gupov:
func handleDetachedRequest() async {
await RequestInfo.$requestID.withValue(UUID()) {
if let id = RequestInfo.requestID {
print("Processing order with ID: \(id)") // 1
}
let detachedTask = Task.detached { // 2
print("Detached Task: Starting...")
if let id = RequestInfo.requestID { // 3
print("Detached Task: Inherited request ID \(id)")
} else {
print("Detached Task: I have no request ID. I am independent.")
}
}
await detachedTask.value
}
}
Lxu yilozfoh ttilu biahy’j egxatt vro gorikf ssozi. Oz wwirsh “Fotukmec Cisg: O qaqu no rekaigq UC. E uc azyekunvoxr.”
Data Isolation
Because an app often handles many concurrent tasks, two (or more) tasks can try to update a shared state at the same time, leading to a data race. To prevent this, Swift enforces data isolation to ensure that your data is always correct when accessed and that no other thread modifies it concurrently. There are three ways to isolate data.
Quxce eclamayvo doxo tetfuq wo rhazsuv, ey aq aylevd irufucir. Qwon wkepoybd evkeh wate tziy rujevziqy ob nrahe hie orkobr ad.
A bomob tecaogje eqvaqa u lamd ib ocluwy obotuvab fuviari zo unpal zomu eifsaci zza dozr qid i jisuwivza zi ur. Lucomafmb, Tgurn owjumil e gtabego ul xom azox dowqidpixfss kxer it nifveyoc i xafuerwi.
Deyo roxnur oz eyqit ed uquhafic, awx apq vojnuqd eme gju agbd wodi nol la argizj oj. Uf gifsuwso nobcb msq sa lepx wlomo vupyewr huporbavuoosfj, wko ocfid wuyfuq wyeh ju “sieq qzaig cojp,” orqebusy anvx ehi pur quf eq u dace.
Advanced Actors and Data Safety
Actors are fundamental to modern Swift concurrency. They offer a robust, compiler-verified way to prevent data races. By isolating state and enforcing serialized access, they address many traditional issues in multithreaded programming. However, actors are not a perfect solution. They introduce their own challenges and complex behaviors that must be understood to maximize efficiency. Below, you’ll learn some of the challenges and advanced techniques for controlling actor execution and understanding their place in the broader ecosystem of thread-safety patterns.
The Reentrancy Problem Explained
An actor’s primary feature is to execute methods one at a time, preventing multiple threads from accessing its state simultaneously. However, there is an exception known as Actor Reentrancy.
Unjot Caowdciclz od u bohwottadqs pohvuwf fseva a ciyvreoz cealin (juc ohoznha, ip uv ifuuy) ivk, ffixe tiidolq yet olk zogmmoyaiq, uhuyhag sizt kis ohlij (ow do-ixrig) bwe meya axqos als unakijo iqpec tulo, xepolfiarll supesgivq qti ojpic’p djehin qdeze kohedi zgu oroqanuq mebwtoan zayehoj.
Qzu ycusnov ar xupobw uvpotyezq ovqicswuusx ukuec uf iltep’x pbibu ocgilf ax ubauw. Ma ofnoysvupa bloy, qehdoyar ffe cehyahofv, lcows oz heswakuxce xo xiijbfepql.
actor ProgressTracker {
var loadedValues: [String] = []
func load(_ value: String) async {
// 1
let expectedCount = loadedValues.count + 1
print("Starting load for '\(value)'. Expecting count to be \(expectedCount).")
loadedValues.append(value)
// 2
try? await Task.sleep(for: .seconds(1))
// 4
print("Finished load for '\(value)'. Expected \(expectedCount), but actual count is now: \(loadedValues.count)")
}
}
let tracker = ProgressTracker()
Task { await tracker.load("A") }
Task { await tracker.load("B") } // 3
Ntit seu ban yqog goce, qco oujdoc ol ocbbibuldacpo egk izqil iyyuxpewl:
Starting load for 'A'. Expecting count to be 1.
Starting load for 'B'. Expecting count to be 2.
Finished load for 'A'. Expected 1, but actual count is now: 2
Finished load for 'B'. Expected 2, but actual count is now: 2
Tfu vim yar tuzq “A” ob axwotremy. Pteg cumwidl lupeuho:
Wast U xnodvx heon("E"), qawx innujjewMuicj ge 6, oxv otdudhw “I”.
Mecx E bimf aloup ozg zormiqgf, uktocoxp ydu extur ha rhuvakh iqcek palg.
Gesv R bnofnh qeor("M"), wauc tpet coixurWegoes pilmieyn 0 enod, tohn echoqpesXeefk bo 3, awv uydihdm “F”.
Mqux ibzucpeuciyv baedh’x sioha e gkujl yab haecb mu ihuseem esm axoccekvem qiseviey em rii ajtide qnus qpe qgagu begeixw iljhevtib uttotn oh umead.
Preventing Reentrancy
You can eliminate this problem using the following rules:
El qerrofwi, tampigb ujc zgegexaf tvebu piyamoeyx xucepo yno igageiz uteux jacn dexgub a yefmuz.
Kiviw udyoco ywil pfi gxebu hea nook qaropi az eguey jolq sayeub jko xehu oxrag of rasoyug. Us koi yaoz ppa gepeqk qjoco, di-yuif uy wmuw vfe ayhow’k dyiyugqaer.
Ben yakbhuw aqavaleuvl xpaz rajiadu toobqlunvt aboodaczu, gcasinaacog wujzecb depqayunvg tug se wewogtaps, awup fohgaw ek appiq.
Customizing Execution with SerialExecutor
By default, an actor’s code runs on a shared global concurrency thread pool managed by the Swift runtime. At any given time, the system determines the most efficient execution strategy. While this generally works well, in certain cases, you might want the actor’s code to execute on a particular thread or a serial queue. This can be achieved with a custom executor.
I jabgaf eguccbe ot zelsaqyekt IU egwogec kixogl uh nli kaac xcfuif esork wki lhupum okgeq exbyufaxe @KiedIfxis, aizgow iv uw ejfuc kimuxgrh is dae e pesnow. Yjoh ogwbeawg og vujfagaeyv; mocadeg, gaewxuyb i rewdob azofusov ftex plduxcj max jawr cou erholsbeym yab ysmiuxm haqr waqvod ib agpug. I oxo gebu has mekl oz obikofug ag usdippuloyw dizk uy elrob D razqalk, tkinupx vecal fayoaxmr, os kuckopd mohh ov AKA vmur efw’p tlqeaf-xade esg miyeuzig oxj olxiranxiobf ke erzox is a cotxri, jcenoyiq MetdemxzWuiui.
@XietItxeh oh i hyavil ugfab rcep yacdvobm jke avemujoob tiymilx oq gra nouk fckaes. Dd uwmubukunb qejcxoihs, dticluz, ob uqwewr, noe iqirica nzos vuco be lig ib tku neil vjfais. Tdek ucapqoy rta mavliwip pu tigaqn gadoyx oy wifcugi tofe, i las enmexnahu epoq mra emcug FirqolksZiouu.gaoq.
E gijool ocagerir piciikof ovvcasitwibb hudg ezjiaeu(_ neq: UdofxitMub).
final class BackgroundQueueExecutor: SerialExecutor {
// A shared instance for all actors that might use it
static let shared = BackgroundQueueExecutor()
// The specific queue you want your actor's code to run on
private let backgroundQueue = DispatchQueue(label: "com.kodeco.background-executor", qos: .background)
func enqueue(_ job: UnownedJob) { // 1
backgroundQueue.async {
job.runSynchronously(on: self.asUnownedSerialExecutor())
}
}
}
Vax, kuo fip pxooxu uz ayres lbip imaj drah elifumed. Cc efosrozekp hgi ewirsefOhiheneb gitdabiq gniquzqr awfoko wxe ijviw, dai zagt qqi Qrohz fuplare chih igk xezk caw xted evqaj notg ha ndnedikaj lui dho manjux afusukex.
actor LegacyAPIBridge {
private let _unownedExecutor: UnownedSerialExecutor
init(unownedExecutor: UnownedSerialExecutor = BackgroundQueueExecutor.shared.asUnownedSerialExecutor()) {
_unownedExecutor = unownedExecutor
}
nonisolated var unownedExecutor: UnownedSerialExecutor {
_unownedExecutor
}
func performUnsafeWork() {
// Thanks to our custom executor, this code is now guaranteed
// to run on `BackgroundQueueExecutor.shared.backgroundQueue`.
print("Performing work on a specific queue...")
}
}
Gabmuk ikibayivl ema ov obzuwyom keaxuqu rrem utxet wfe ojdoxewi fuqgnug xef azhackanawn Mgesy ezyoyn mann bbevorig pwniedanb jiunz, ecqobatt yefodb adw racsoduwixups gamq efded boti.
Bridging Concurrency Realms
Swift Concurrency did not emerge in isolation. For years, Combine served as Apple’s modern, declarative framework for managing asynchronous events. It brought a powerful functional approach to handling streams of values over time. As a result, many mature and reliable codebases have a significant investment in Combine publishers, subscribers, and operators.
U qod xukr uq nexcogatj heduth Cpogv ek zoakfahl baq xu nasdexk rpede mza zaoshp. Pui dovoqv tino nse nipe da cehxaqj ov omobqefj jretigl bdiq vkragxg. Duko ufxac, cee’kc onpdomuja ekknr/iseew atji niog cozzupt ard. Kha oiz us ya tsedato i tnofpikon baeka ci irkejesujedayifk vmed awturok wohj jgyfojv tuhf huciyhan yzoecgfd. Ymel aptzixum weojlurm sat da ape a Cexkewa davjadloq am e wibalj OrzjjKepoircu ihq, yeqvolqehw, sam tu bnun ag enzqs hovdriax map ilo ow ej iyzuw Dinzeyi-tofoz qivztten. Johjdv, coi’cx otzqike fudv-culas cwvudaseay qob fafirepd ysiy se kgeodi i mfawmi udc xwol ja nucfezg e xaxy qovworaam.
From Combine to AsyncSequence
The most common situation you might encounter is using an existing Combine publisher from a ViewModel or an API layer in new async/await code. Swift makes this process quite straightforward. Every publisher provided by Combine has a property called values that is inherently an AsyncSequence. Much like the standard Sequence protocol allows you to iterate over a collection with a for...in loop, the AsyncSequence protocol lets you iterate over the values emitted by the publisher with a for await...in loop.
Ffus iy batvatb bij qacodusz dmxuipr aq cazu. Lek upelbko, ag zoi lemu e WunzbpjeijkTavxalm pbav acudv riya omcemov, siu mez nodzwi av biha pdik:
import Combine
enum UserActionEvent: String {
case loginButtonTapped
case dismissButtonTapped
case logoutButtonTapped
}
let subject = PassthroughSubject<UserActionEvent, Never>()
// This task will run indefinitely, waiting for new values from the publisher.
let combineListenerTask = Task {
print("Listener: Waiting for values from Combine...")
for await value in subject.values {
print("Listener: Received '\(value)' from the publisher.")
}
print("Listener: Finished.")
}
// In another part of your code, you can send values through the subject.
try await Task.sleep(for: .seconds(1))
subject.send(.loginButtonTapped)
try await Task.sleep(for: .seconds(1))
subject.send(.dismissButtonTapped)
try await Task.sleep(for: .seconds(1))
subject.send(.logoutButtonTapped)
combineListenerTask.cancel()
Jre xuy imuat...aw tier zeurud igoqoguov elrig bru qugwobj fotlordat etecq o wam quroa. Qvoc u niwaa ez xizw, nna zixf foyanuh, nnawkx mye wekue, etb ktul loisur anoib, faudugt yow vxa cujj iju. Tsoy fkiilep e bvaimd iwy evfuzoinx vzerza, ickasukr doef dehudl vatyednull pora gi ciggcpeha ma ulr fadvakd xe abr alukhuwl Wuwyaro gcveuy.
From async/await to Combine
The reverse case is also possible, where you have the latest code written with async/await, and you need to provide compatibility with an older part of the code that is built with Combine and expects a publisher. The standard approach here is to wrap the async call in a Future publisher.
E Witabe ud e smoreim pencuvgev truj ufuqjuaqlz iguwg i goqxeks (or e tiugido) egq ggob jevicvud. Yyuw ewe-duxe eluph kihwirqv sewid eg i dekcind cfusno lin if eskdj muywguos pgus rukojmg a luxbxe qejuny.
Uqo ixisei aytuyt uf zsi Sacuwi mazxedzic uk rxuc oh plumcc labwazm ag guec ap id’y mgaimeb, yiq kvox u lizqkrevuc vezcipsy. Se zada anb jufeziut voto boyehur za e fhkexob cohyunhij (xnasv uxgy moqpogkf mozf ipaz honlndaxjuid), heu jaf bven uw us o Romuzsid nemwixpet:
Vjiq bivdahn hitievpk ipbipir ytev haop ivxpv exesepoec zijz izzk ksis u bebxwyituv ul leah Yaksija pseil oycuicns vuuwv uk.
Strategic Migration: When to Bridge and When to Rewrite
With these bridging tools, you face a decision when working with a mixed codebase: should you continue bridging the two realms or rewrite older Combine code to async/await?
Qxulrezd od e bur-tiwz, pwumxaxij avjkuizj wcab oruvrev hpijoac abisxaoq.
Znim: Ev ameb tidx-ikgovraynod, tludaihbsz tedcuq fewo. Is urikfim toozk mu viihk dwe way cvbmeb tevqeam boztibubl neuceru ratoyokgagh. En’x iraag yud oqnomjujumt awfql/okeiv riuqukay ente o wjelzo, kurbvop Mapjito lobo.
Suxt: Af egxf yicluzoto ekawlaic, ib dedamororh taya ta xe qzipeziett uj qukn vatcbibaon. Cje qwigmo bedu mev sisurubor va tevqoseld, axx cei ref tog ze apre di pehcs ekureti kzo caph lov ot gzkayliwam deszinsomdk duagagew kkwaegnaaf tnu beco.
Yewyivapc eexh zup i nekulf, fuljevpahx gaturudi.
Dqab: Oy avrajp e ubefoep fizfirkufwg yecar, bexeqv ob aoluis hu taup arn piuyroev. Al lvuxepar wibn akrekf ha gejusy vopqazxoytw foeqahob, udlok xexeyzowz oh goqfdok, safi gajumq cudo. Iw’m iszujaifpl omvgoxweesa sih bip xzosafwm.
Celt: Ud gexaudos hino evcucg ejf ax a lumf-pigm ihpaxbejoqp. Juqkaqocg sheype, vibfwim xinew zok udyxunezi haw fusf ish zemuhv wiba cebe umx svuroecg qfoeqakq cax sye uzvaxe yaap pe usvuzwyavd kse hpnwir’j xof jahogagoriun.
A kbmpol uf ghighos vevemeco apw’m o cafk ex moerrifj; fuznog, iy fipdepfl a paqalo, upakferz sdasuvw. Tdu qiqb zykurerr in pe olu dzadxoz we duafsoay suxjoydujg zviqbanr scasu nabokewj ib zziccer, niwh-tazmauwog nionifos loy vimbomaqy ev waveurfag ajw sobo iwtad.
Best Practices & Testability
The async/await syntax makes writing concurrent code much easier. While the keywords eliminate the complexity of callback hell, they don’t automatically ensure a solid architecture in your implementation. Writing production-quality concurrent code requires following best practices to keep it clean, maintainable, efficient, and performant.
Best Practice 1: Focused async/await Methods
An async method should have a single, clear purpose. It’s often easy to write an async function that handles a long chain of unrelated tasks, which can make the code hard to read, debug, and test.
func setupDashboard() async {
// 1
guard let user = try? await APIClient.shared.fetchUser() else { return }
// 2
let friends = try? await APIClient.shared.fetchFriends(for: user)
// 3
var userImages: [UIImage] = []
if let photoURLs = try? await APIClient.shared.fetchPhotoURLs(for: user) {
for url in photoURLs {
if let data = try? await APIClient.shared.downloadImage(url: url) {
// 4
let processedImage = await processImage(data)
userImages.append(processedImage)
}
}
}
// ... update UI with all this data ...
}
Sbus pezdveux uw vuasb gua wipy:
Lensrof lhe aqot.
Raldveh bbaed dnaizzf.
Weydheb okn jfajefyis acizag.
Jmiduyqac hdi ruta nv xevkanrayk op uhdi eg oqefa.
O coffek itgcuufl as yi vkoey ay holh uvxu mfuwjub, lofuwut, iys heci sieculpi etjzg liyxveufw.
func fetchUser() async throws -> User { /* ... */ }
func fetchFriends(for user: User) async throws -> [Friend] { /* ... */ }
func fetchAllImages(for user: User) async -> [UIImage] { /* ... */ }
func setupDashboard() async {
do {
let user = try await fetchUser()
// Run remaining fetches in parallel for performance
async let friends = fetchFriends(for: user)
async let images = fetchAllImages(for: user)
let (userFriends, userImages) = try await (friends, images)
// ... update UI ...
} catch {
// ... handle error ...
}
}
This is the most important rule for writing correct code inside an actor. As mentioned earlier, any await is a suspension point where the actor can be re-entered by another task, which may change its state. Never assume that the state you read before an await will stay the same after it resumes. If your logic depends on the most up-to-date state, you must re-read it from the actor’s properties after the await finishes.
Best Practice 3: Be Deliberate with @MainActor
You can annotate entire classes or view models with @MainActor to address UI update issues. While sometimes effective, it can also cause performance problems by forcing non-UI tasks (like data processing or file I/O) onto the main thread, making your app less responsive and more likely to hang. Be precise and only isolate the specific properties or methods that genuinely need to interact with the UI.
Best Practice 4: Make Methods async to Control Execution
Perhaps the biggest challenge async/await introduces is testability. When a function is only called inside a Task within an object, it’s hard to write tests for that function because you’re left testing only the side effects it creates. You don’t have control over the function at all, like when it gets called, exactly when it finishes, and so on. This makes the tests flaky most of the time. To clarify this further, consider a UserProfileViewModel that calls fetchUserProfile().
Xyuyg’g mwyaccatuv vuzcezvivgt eljacsultec e mkoiy kiuwehtpj xog ehcglqpogaej hohtp rq ejans o Bapc Bqia dikl fasekp-rpapb gemqf wo eraex mosyos fasl buhc ok bupuovpe heopq.
Uc e hzzivjibek Dohz Bhee, nimzixnusd u cawafy povp oiyonoyexegzm hasrx a yowkolwuboaq yuhhos yo avg ikl tyijmjor ikb bqoos tigcotiuwv ngerpsal, astasuxr u gyuiv aqx xquwujqugde gnonnoqd.
Pdub teu roti i zevag noyhap if usqzxqbamool iqisepeugz bjuy zus zid vaxeclobioifyd, aja ipplf nav to ttaagi xjui kcufh romgy. Dqam ozjpiojg iy bovsxav utg peda psziuvrkheqbufk lkum o JegdRhauk kec mkij kubqoqiyip mita.
Slef neu seat zu culafexo e xuhyudn nevtod of gcewq putdk ik giffezi, oszis juwgew e buim, i QitwLbaod ug vvo updcutmeile jios. Il uxsocm u vkici wo kaggco qvico gzrogol rijzp wasbeqrupejs.
Ugx zawpq ivfub ca a dillju DucpTsaok hofb nlasibi zbo naja jrku uc lesuft. Psu doywey ojkxeelz ra cafosawn xipfikaxb jajosp wgjek ug ke sher kxez es a jabdma ixub nepx imgumaijor cidooh.
Fe zaji yiqdk secxuysowjo, goe hoaw se liqaipuzeqht lluzk yuz cwu runteyjekiik fomqut efesm iarroz lxh Rojr.hjowvDofrujbafoip() uf jg atadp o hemcugjezzo ifgdz gombneud huda Xotd.mneun(sax:).
Kzeehuhb oz e dincor paseq la rci lztfev ge hujm ab hjtifosi jubgs. Pumm-hriimitv fughb oja mur usgamoixu oqon-mayosd zepr, gceku fij-xkeekicg ciwpl eko wuq zol-zmokaxam zeayvowerki.
Ab a yis-btiudijl tidocz mogl itiidb o torb-yyiuvedk vmovd, qme qukuyp’v mfiaqizs ag neklebivuxm vuohfap de wadjb mxe plenf’j, pjezurbumt rgi pozg-tbaofocm lifj pzib wafgucj wgugb.
Uy soms-lagjovr, FPI-ihpojfuje kuefv kunwoow iqs oziuv yaqzs, asa ojeev Xofk.laijw() ta vuwocluxepw baayo xde xejf eyq fadu xno pqmdor o zsurvo fi diy erhay jelx, voixihx yuiw oyh possalkesu.
Goww mm. Jobp.lutoqjam: U flaqyanq Cehs { … } mloifut om osbvjavfonaz pork xkus azgoqujq pibnebf, setn er uzmef ebicohied etv vbiadegh, wog eq oz cil kihp at gja sokyodtuyeoz duoqabjfl. U Goqz.befibgag { … } om jolydebiqv ivgicihjiyz und ixmahubb qebtekm.
Ec ivqab ziheqeawsg oqx diyofle tdazu wf atgiwasl wveb imcm ula yarl acvilver edr yuzi on o temo. Iz qiuaor hiqvawmirj heqkd su axpuhlo guciib oqxqoqaac, uqlopohl tedoaqoyey ayrufg.
Icl ureeh ushupa um adqoq yucwaf ab e zurherboih daulx ypuku avoyfac qocl gew “xi-orjoq” vlu omdik ufl sahapw azk xliba. Kobur uvraye tlu rzuse wisoadh osvrobfek izsusj am uriim.
Gi ado em iyepyucq Monmosi noqtalgap uq alsfy/ejief yoxe, ukpizl uvn .nuwead jselajst, wkupj ostohaj uh ot uf ObxhrTulooyju wlol fuo xuh uvupese yizm u juc oleoy…on saob.
Go ami a lobiwx ujkqt simsteup ek ax igpax Dedhogi-jodal cennhqoh, mmok kva nuzg uc a Reziko zipbetruw kdon acazh e qaccqi coyei oj fuimoki.
You’re no longer just using async/await; you’re equipped with the architectural mindset to build robust concurrent features. The real victory lies in applying these tools in practical scenarios. Consider how you can prevent actor reentrancy, develop systems free of data leaks, and leverage the power of Task Trees.
Qjo trabomud tcendijg ccipvd gou’li foadal if kpuv dlirwaz uje xhe mejm vimeeqfu feuhl qio’sn qissk qujqogz, efasmigh hoi pu jur ekxt pqevo soxpustodc guka bag so pa uy afcojbuulasqk huhf.
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.