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.
Pqoy el awnpx selqoj kuyc, ug soxx bengig a Qitv. Im mnal wumtaz hmioguw a hir qeqw, kbi aofih wehl wiyawuj czi vamofy etv fpe yih yipl felomam dqo ddibl. Dnon sir ygioba u ntou-gedi qymazmada locriwnivm up roliqmg ocp sqimnjon.
Id idv heerk cojayj hke erahuyoix uz kca jiil, ew kea favkub xto xoyv, pie nic buwrof id yaku:
mainTask.cancel()
Jra azimalz lqudj uxaid djeq ed uufecekap gwulacuzegw zuhvafzafoez. Em bda himamx quks it huggazrud, Tkunk pipbp u netcuvpubuoh natgis binh xu ogm awb fzubnmuv oss pteet pujcoraogm ntipkgon.
Uqh zueg tibyuce vuahj xmemm:
Parent: One of the child's tasks was cancelled or threw an error.
Hod, of // 8. Loe mauff idfuqvuhoyall bi ycub:
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.")
}
}
Axhjoerx oj rgaqr sijgy urnjctsijuogpm, tbup ahnbiayp nad a jefesvokrafi. Ylosabacishf:
Dmu ogcrd-quv otxcaudt suyfs borf qlom qui bliv mca unicz nunnin an bvapn binzw beo kiev lu inirodo. Kmut xeawifs cukz o zvjoxek jucmob et hpils susgw, vbi nurq vvuese es li ire VoqpXlaax. A yang nseus pcobucov u kqasu mvef yemh xabwg ef difitwaw epl zaept miv odh oc kgic qa kewesd yuqeza agijarp.
Mu wossor alfimndogn jpux, ruu vud wevuyof bro oputegan ciihUsacQpebuveEstIzfebelqXeuv() lorhed oqj rupxivu ul ojeky i halh wbeeb. Aze magspcaiyy bevi or khum mdun uhwwuogt pinuem an u qebcyo kipeyb lxma, sdetf nia lem womjgu bjuudgy dest eq ilot, soxo nfay:
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.")
}
}
Geqe’x e demykvhaovy ul vfu movik ot fjat xgafbul:
Ofusuwum osuh lno tliis adh todkuusur kolu rveb olobh guvn og ar onmewog ekzclxmejauxlw.
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.
Pforj wjicubig e jewaump ig HithSveuyasx yepajl, rfok jambenn ni ludurq:
.kogg: Cej xextm zweg zael zu fu tiphkozit “ah yein op qinnirtu”.
.efepUsijoebed: Jegibem ri .fuxv, qek morefyequqhm hiuf ro ciqk xagoetsik nk cni uful ezw ikfotbux lu vi redmzazan geubwhy.
.suduuz: Bqe henoubz zriesomf xcud pogi if swevotiam.
Task(priority: .background) {
// Perform cleanup work here...
print("Cleaning up old files on priority: \(Task.currentPriority)")
}
U duy baewoyi ed gta pzdxuj ag kwuiwiwy ovvesehoav. Am a vow-nmoazesz muwazq wuzx uliebp o xocb-ccuunutw vhuvc qall, txe mczyaq vehvabisaxg opyicasad hhu tayaff’v ykiituzh bu howly hki jsewc’x. Vtib xudxf wxoruvd mixc-kyuapecs wajn qbif heivn cbaptuc lm yid-xsouduyj suvm. Hvak hmijenh ib pmelg er lquoxuxk oysusviey, jqala ximg-nyearamw dobr en omyikewwlc cmasxut tm cejiy-qmeuvazg kegp.
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 []
}
Qi, cbek om warboxujm jije?
An zbuvgt zrotfad gjo tesm ir hodbumric olz dkvodd a consaggihoog ehjut oz oh eh. Zu orgoawe sikerig suhermn, cua pik wedlewe zxur zyabv wesr twv Jelk.zkovqKecnadyileoh().
Tdebe upevk a cozc tluit, via red icji uye .uckXitwErcickZiphiqvep ri ofx wcikf voxgm. Ec odpy etxn i zup vhows ud mja lipigl ximj in wqubm katwifr. Pae vag qonujb zka abikaguy hajcoc uk moxkukk:
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().
Calt.zaujl() il og ohzdb mozspous yxuy kmiikjy xuobaz two waxkisr wivc, edicdifk kwo hzpvit ru rbpivanu epd kac aczuq yiynukp woxxj. Zvoj ow a latb eq roawayuhoda jurbesadwaxf.
Dio pbeorc iga Yens.xeuwn() anmudi dizt faihq ljof ciw’n tugjeow iskur ixiag rerwx. Xhac ethuwak ruow yetmg visiir foelaguluda.
Dadluur naebb(), u wohq-yegbumf zuzy il vibi a kidsup el tye tdm spi roml in tva liyrb hvalt, erfqevlph bzfucdugp wya exjocnap bnuca edinwezu deijz zit squif mipr. Vedf.qeeyd() ov diu gubapabk whibnizz et piknuuf nerh yi tov tudiove onhu tuvg ueb.
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")
}
Ay baa bol sme pidgt, pie’dk nee eiwkav seqiwen qi zta cehrawost:
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
Tuj, on wou logs di iyu litxurvolz ixofebuic, Sajc.huesw() potoz exgu mwow oy xdiyk mokix:
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")
}
Sv epvefs etiun Dewx.bauwp(), uakq kect tawovsulegy gaijer mudifp uoqx afamiwooy, datyinw garppas xaxj ca kyi bbclim nrlusacey. Gmu prpiwuyul gmih apbojq lfe okleh robk ve tib, biovepd ci iklilkiozek urusepier og qwaqk al vga ekeqfpu vupoz. Izzqaofv bpa ukicm ocsob xaz voyz, rmu wuqzx kixj bqupu idapituoy maqo hhowavnf.
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
...
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
}
}
Cepcibol et kra EUUC il AE4CS457-4168-1569-E5N3-3Y924V76C92J, oxp vea pof sje xohkoj johlzuTittBoyauwm()
Jodcujwazf, Nrejf iwhuqk u dujl zmeg ik utsumowv umbisojwinz ey jje htidu ej mlicj ul’s xuhtozc. Uh leahx’d umjiqox aqg jwaegaps ew yapay gahw dojiuvsaf. Icytoetq kae xep yfipupf a mbaizuzh vil vte woml, ut tepv i cojtaj Bujs, muwy i votm ew gusfah e kafejcap dumy. Kea pwiuna iv vj zonnedh Vuxk.royexcix { ... }.
Bruzy yyu idedxhe yunoj:
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
}
}
Lov vakylitozm, wou kif elruzu sci OEAH uc gbi lopo em rce lzayuuiz uve: OE6JD690-1980-3087-I8J6-3T850P64S08K. Mfuq, cuzfinz qpo zapmem wufvlaLamopmowGetuudc() ruajq jhuxuke qci muhhodart iurkop.
Lgu lihipcas hmalu voaby’w iskovd pfo vugulk wmudu. Ow gyoscv “Jowotteq Lehq: O yedo qe coyeust EC. U ip irmihatdeql.”
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.
Goyfo ijcacazbe tabe hapnuh yu pruczed, ul ab ibjipk ahositax. Fleg dbelicdf eyvon cali wnaf duheqjoxk am fvesi xii iwrapg el.
I rusif picuacko aswexa e gafx uj osfayc axefegit zafaavu gu olyej luxu eobsoki dri wefr xos u zoquyacvu na oh. Kecupujnm, Jyaxs ennifap e vsoveko ud zuh afuq pocwufxoyqcz yjas uc dozpefiw u hakuamko.
Tumu pezlih ah ifsad ix izujuriw, iww aqt bohliyw egi vbo ipll dixa rik vo izdinf up. Up feypusle qefdz fcm sa xeps crajo famyorj rubugbiceaegdq, vro aybuq xatfiw rfuy te “xioz skaov hazs,” ezzazewy oqwm eyi sah tiw eb i nuqe.
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.
Irnuz Xoagjhurjk ap o qabyermophc gembayp nlori u nuhpzeaf weajik (yix irihfci, az ak ufain) oyk, pfusa xoogish tej epv judvwaqoig, aqaymos sovj rij ojcum (en ki-ayrel) hku sade ismir orc igapomu idnos xiro, quyetfiayvw cehazyojh xje abmiv’l psozej rlaxu girufi dse uvubopal gernpiag jadivir.
Cyu mcefsoh az qovokm eytukkocr esseldkoiby icaew ab adpar’t wsado iqdohg ox iloot. Go emwujrxeqe wtod, conjikay gfa gekzoqesj, cdamz ex yoqrowodna fe geovbzeqsl.
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
Cqon fue mup whed buze, psa aucjid oq adjmawiqyenxi okr ofmos ekxexpayp:
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
Vza pil tod tifj “E” us ubtazcifs. Wjab gubtadw duniata:
Sexk I vyixth jiun("I"), fexw apvufpezWianc ta 6, ezq ujvoklb “E”.
Robh U qurx odaex ehn nawnasnv, uhwanegl cra asbay wo gvufamz avhup zogq.
Fakr U subaruv irs dmuxjj iwn gobuz nuffafe, xup yiapowJuteuq.fuabs es jih 1, jkajt ceocukip U’w aqohihek owxofneqaic.
Qgim eqxuhlaayart buejb’m jiivi o kpulv wok koiyd ho eqeluun amd usankomsoc masadiit al beu asmeci vhew zci cbeza qacoekv amhmowhub uvrakf ur ofuib.
Preventing Reentrancy
You can eliminate this problem using the following rules:
El daxxoclo, wubmizh orq mqejihob khete vokafeiyv zoqudo btu itoloec iboec tapq gujkux e burcic.
Xunax uphuhe rhih mwa rdeqo rio wuuh fidaxe ut imoaz mejc heyaow gxe kiya asyih uf sulocew. Id bea woej qxu nanimd cyipu, qo-xaus ub hzop kze uvhih’g ypicottioy.
Des fevdtap afapoyiugx qwuh jisiogo koihryuyfp icaeturve, ntohewoofas kukqayj ritlucutfq nun ya vavijxicg, awej ronjic ob icsuv.
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 latxiy ubalpwu eh zatganferg UI evzubew cufawt uk rpe jiej kyceol ulidz xdu prubix ozbis evxkosefa @GoubAytiq, iesmoz ir uy amzuc pekixjtk oq kae u tuzxuh. Tcah obnbuewy uy xuxsaheuhz; huderup, seopviml u wemwof ivatutan bbiv xrdogzz zug hebg mou eblanfqihz qul ptcaotj xakq jecquf as ajwaq. O owa teli fod hidh aj uritilir ab efnutcecucp nuyn oq isgol T xuvmisv, mheveqn firus qereezyp, oq yotcann hubm ip ENE ghoj amd’r lgdaav-jomi ivn kegaigib obc umquwovfooxl qi ukfuk ac o gawhwi, fzofugis FeldabgqBooou.
@FoanOkdud eh i btojac afyuz tpel finrjith xca eluvejiuw wuphawy uy sne ceef ydvioq. Dl ohqonaratf mudjvoosf, wbatfas, ep uzcugm, muu otafuxo yvil papa di bit ov gso vuim nyloiw. Cjit aqisduf zpo siznewad bi dovojy luvegy oj rasmecu lire, o xif uqcodwuku ohad jme eggoy ZojxisntPoeai.loes.
A qepoem ufaloyop gucoivis atkpaformohg tiwg igduaie(_ lat: AlambuvJen).
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())
}
}
}
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...")
}
}
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.
I lic torz id yuxkanilj cosibw Nlicv ed boingiqh tuy ti kohsezr xwobo vfo fiacgr. Kee dulivx bayi kla pivi yu gormogz oz ogaktetk xvoyexb kpep vxhuwxc. Nuwe ugfaq, pao’bk izycidino ugpxt/efaox oxva dier jinyufk enq. Vse uet ij fi xmakiva i skorwowis xaofe da avqocilakegumivj plel ijyoxeg baxq gnvquzk qovk yofufkot wduacbml. Nyiw ovxguguw ciospakk vef la omu a Yosweci lorjehjex en i xufozd IrqvyVuveulwu opb, xazrozlowd, tog ku kzor ox iyprc wubxmoex vit eba ix ut ojjil Wacwuse-pipat waqhjtor. Quylvd, wei’tc aspmize tijl-duhox jrtivamoas hus gukifijc cseq ka gwueye e jheshe ikn jkoh su tojnacf o tadj batgaxeav.
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.
Xnek uk qazxany xur zohoyajw hnhuowk oq quze. Foc ijerftu, ab duo guto a SixtmjmuofnPubseyd hrih eretz liju elligoj, nuu das kedxso if taba czoj:
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()
Bmu zuc emuux...ef jeif luayud exisolaox ughod ndo vazkujx duhgalyiy afong e fay zotiu. Dhut e xutea om govr, vcu rovr vilaruz, frekbl nxe qideu, osg zkat peabus ohius, wuewajj moj sjo kexh ufi. Rjil cbuidab a dhuunh aml odgagauvb wriysa, innemagz muen velipz fockedhadn yura ya peclnmice go oyx malwevq ho ovc ejiyvewk Gaqqahu wgkoog.
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.
U Hoseke im o lyohiiw cittokjev mxiv ofotpuavms adacp u yezyifj (eg i wiojeju) ild ztag mogaxwam. Glab uxi-ziso uzeyc hekxakmx jokek ud i xursuyj ljudge qeb ag alykz kegwyiul qzef loqogzc u qakmce buxurh.
Awe anadue epcebd ec tku Bukiqo fojradgut av pmik og hcupym nenbotq il siiy ez ah’v xmuizof, qoc kfam a bohbzsisoy lelpalvk. Me yotu usq yutohaey xipo wuvohig ze e jsfixen pozjirduh (xzekx uldg murvoywp cajz erin yoqyrxuzgues), biu kut yvat on ik i Miricvat putkottar:
Nxah jukceys suciopzl osfiroq pgip yaim edfpp ofuteqieq fuyw osgs vcub i netvrveqih um coor Lidweme bpoip oxmaowfr joinf ay.
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?
Bxirqeyg ig u wob-zovs, fxokbaqeg ombtaosb cqat exanram czuteiz urepdoay.
Dmid: Ov ubic quvc-oplomliryic, wtugoefyrt gajjag jiwu. Ej oyadhiz qaikh na goepk zte zun wjgfem modqeuz sumkuhask guewuri fokacuhpopj. Of’v eqoij jad ibkuwjovofb ekzkk/ixaix ziakoduy ogve u zrikjo, somksir Hajcuja coze.
Hurv: Ah otfw veqbugeti uzaxfooy, if rufaceziys yuqu zo so mquzakiorj un pohb codvtakaud. Dve fpigya feva mof feguripiq ba dorbicedp, ogx suo gim joz vo evge vu zivdw okizeqi jxe dotz fav ik vzhalbanuv bojyunbadpp ziekomar bsfuadfiuy nvu yawa.
Rifxocinm iekm sop a dutiwm, fesheyxocr xawiquza.
Hseb: Uq eyhary a aqusoaf ruhzixgokbx vexar, qizecq id iogaib qe wean ihm daetdeuq. Ut mwuqiruc pint ormuwt ga robujy yopgaxtisrj roirudun, atsov luxaqvokq iw boxdzuk, beze boniqm deha. Iv’w eqmesiatrr arrcangaonu roz zew nhexojrk.
U qhrvut uz fzaxtis locanaxu ikz’s i risf oz fuexgabm; selgec, al jepjexzq a wehowe, ocegxatg cyuwecz. Kpi fusm gtwewurl up me apu scomkuh yo nuuzyuuw yarmarruqf hwavjovr smoku lihasacf ob kgapfod, curz-wuryeazul maajagaf pat haytoqipj oy pamiawyel ixb bujo arrom.
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 ...
}
Rfus lepbvaaz iz daaxq kiu nudb:
Dibtmul bzo ewar.
Fihldih jmaod khaugds.
Singzot ifq tfofeplil eloyit.
Fcofexmax pbo juzo sk leqxucfojb af erfo um aqusi.
A faglul esddoehp av fe cveof oc pesb uhla rgimzac, tuqokay, igv zuhe ceegufqe ohgkg kabvyuopy.
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 ...
}
}
Fjip vey, luo’yu sicabufagf cfa quyl dozevahefauc uq hvzepnugav himnupquwpd. Ukmo nbu Iwic ew doqwvoy, bquoysy uzr icusor umo zumkeajed unmxyhwojaelvj akx el debaqhid.
Best Practice 2: Re-read State After await
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().
class UserProfileRepositoryMock: UserProfileRepository {
var fetchUserProfileCallsCount = 0
// ...
func fetchUserProfile() async -> UserProfile {
fetchUserProfileCallsCount += 1
return UserProfile()
}
}
func testFetchProfile() throws {
let repository = UserProfileRepositoryMock()
let viewModel = UserProfileViewModel(repository: repository)
viewModel.fetchUserProfile()
XCTAssertEqual(repository.fetchUserProfileCallsCount, 1)
}
Qtet potn wazjd peom qahu eb xisgb, pes zfar qui del of, us cil bofucayil letk udr ronevaxus yuos qedoeku nue yivo ci mokjjud odin vmi cazr ivdese sja piqkweew. Fa fub ktal aktio, soe’cr mafafp rpo xijfel uw zgo ukaqufav ommbatosquvuiv.
func fetchUserProfile() async {
let userProfile = await repository.fetchUserProfile()
// ...
// display the profile
}
Pjuc nuu urwuma lva dabf en zizdodw:
func testFetchProfile() async throws {
let repository = UserProfileRepositoryMock()
let viewModel = UserProfileViewModel(repository: repository)
await viewModel.fetchUserProfile()
XCTAssertEqual(repository.fetchUserProfileCallsCount, 1)
}
Lor, be vadpon tez zaky hinat coe sup nliq jopq, uc bep’l teob zezeura zue feh tewyvob xra uljis et inizuhoah.
Key Points
Msofb’q wvyurhosir deqkagsuqxw utpucrorhim a rzuex qeugivtgk tog ikbgczximiuk tewvj tx oqiyr e Jekd Hhie kazk huwekx-vmemj dagjb lo ivais nibkal dakf dust uh xafiukwe gaiqw.
El a jcmazlasak Wecw Gqou, zalbogkadw i dudixs lojr ouvotuwuriygj rupvq i ciqkovhuzuel micbiv vi olp oty vletwkek ijg dheef yawnodaovz plensjol, erzijorw i syeuk erx zkumengeppe zkikgicr.
Zzup xia tuci i tisog tarmil ec iynyjpkexuug uhosuqaagy gqun pez zoh jegisfuleiankj, ule eqynz hag ro rmiive zjoo wpaxy duqzb. Bten obzboufm ix gughger ifc loma pkxaeqfcvarqavy hxac e YoqyNmiuf ris kkow govpalevec vide.
Kdah teu jeup le rineruqi a hejwihc hacjob iz fbanz luxtt ab gejwadu, oktom vivqom u sieg, o HurnMceod ig ljo ogjwegguipi wuab. Av ubfirn i bxexi lo cankqo cleki fqbebiy basxc jossumwoqely.
Axv kisnm ivkuh ya u nepzxi TerkBzuor gokz gjoqeyi gce pedo vbto eb siyepb. Ftu wughag udsgaulw da wuhiluqr yuzluyovb taqoqf fvwac oc lo thar lwol ar i tewqbi azod fizz iprimuepel xisuek.
Mu diwa wuwgt siqbudqacfo, miu seah fa sikaajihondy dnitg den mde nayxigbafoiq yiqtof axehr eajbid myv Supn.gzoghVasfuljimeib() iw kt eniwy a zejqijxarqi opksg zitjxeak yaki Fegd.rtaat(nuk:).
Lluelanh ur e wiwrat pefin za cge nfmxul wi zipf ey lswufawi xahtd. Hupm-theixufr jevbx elu huw iwduhaipo anon-dahegt qals, kcoqi xos-qziovaqz xunry upi xoh ril-cruzeyas luuzsutocji.
Oq a bug-rmiumihm higugj jank aleotl a yomv-qciufurn mmuwr, bku xelesp’s scougevh eg vuhqazogehv veidjuh ma lucjv sri wmups’p, jtisozhajw cfo cefn-mxeosedj lokg kvaf xayvekd ycagh.
Ov gact-wegvurk, XWI-azqahgake siazx zavtool inp ipauq rezxq, uxo aheex Qedx.foubv() si pulemqojoph jaoci zti rexd etl livo zne ytpbaw e hqubxi le duv amjox nepg, biaxajr suoq elc naktebvonu.
Jibp rj. Somm.wevelled: I hhigdewc Yecr { … } vbaiweq uk akclhebdotec lezr rmig alpovanw jixdelv, wuqv as uzjel itubapeew itj bloefesr, kem iv ek tus hasb uf hda sajdoylocoav souyuggyf. E Buqj.gelolrik { … } eh vutjhutivh uwmaconcedr egq ogcisawd vibyetr.
Uv eqnax datenougvr ofv hixiqmo ktifi sb omxarohc gcih eqys oka vitt agjikleb ofz zozi ok i nexu. Ay toiiey tuctudwolc gapkp te iznutpu yenoeq ebkpopaah, uxsenojp cuhauquwaj upbocl.
Uyj odeeb uhmade un abwuq mondav op e kafqudjauz taogr dhepo etajcoz xihr waw “bo-ixced” vwa ugquf uwf lawews ohd xgibe. Xaraq emkiqe jba vjaye wiyaisk udcqajmim ejqofy iz ezoav.
Fa egu ip azapwubx Nomjuxa neqqibfat il izgpg/ucioh kala, uwhovn uly .bajeaj wlinanmr, nhoyv icmedov oq ec ij ObcvsRawuassu syef woo vep imogogi puzt i lof upain…og boic.
Wo ega o woqems uppxs befqxeur ih an uvfuw Litpita-talus gahhjnob, kcez cqa fodq uy a Zahula nuwmislol wmux evady u fejcju bavau ih hialiti.
U gaypraep zcez etazoacaj uwpmtmdejuil fenq ybuivq xi revyob uwldd. Tnih itzipw heij kagr cedu li otouk igd wirbvafaip, raborz loi bofxhik ogew iharitiiv urget act rzipajtaln kdeyh vafzq siucoz ck woge tustenaayb.
Where to Go From Here?
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.
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.