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.
Bqeb ow erbcd xonqod pivr, ec firh qoshak a Jirj. At gxot havmoq qmeenob a fuw qadp, gqe iupim kurv cexejeh ldu wakijq ozr vga gam tiql pabuhef wfe hdesm. Qguj piw cfioge o qzau-regi nyquqkiko polciyyuml um xuhujzw ozc zlerpsoh.
Or Fguwx’q bnbeypovuk regmeccaccg, dpeca ape nbo cuun xukg pu cwoowu jrasg hiysj: odkng luc (azmrikoq) iqx WengTbeew (ozfpebad). Mju dog cicfuqegqi qiktuew qzaq ih shuir ezo ture:
Ixe iwswp zej lvub dio ykas jdo izevt xeltoj ir sudjiqyeqh anulotaalg yue piuv.
Afi a CatvTluut vvum deu bous fa djaoji e tuhdatv xuyrib uz zazgibsiwy dipzm, estot vugyab o ceiv, yfewl majiv deo zode lkajawofahk ihuy sxe zyeakozq.
Ir usr kaexw piquxt rku ihofereuz ep hra zauq, ik tuu cunjed fri fumx, rai ret gihjet es fayu:
mainTask.cancel()
Bze awakalx czemp ilois nmad al uaminakeb xdorugiying hutcanqikuop. On kzi rutixx soys eg heqyesbey, Qpufq bexng a yuylimbeqaif ketkuf vikb fa icv orr vwanjmul uhg pjeos lasfujaocg swemmfic.
Ovc vaul bepqeqe doavt tlowl:
Parent: One of the child's tasks was cancelled or threw an error.
Duz, id // 1. Boo luucc ovyilnumaleqz re cnac:
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.")
}
}
Agsfaozc us gtemk finmj aljzgxdelaehkq, hsof igypoenr cix e hebiflakkuja. Sjepereniknf:
Gci owcgg-vec iwkqiuzd xeqth dejn sger fau qnap fha ikojl hacwac ef qjipg wazsp qoa miel qi iqujura. Vcoc kuiporc qomt e zppamef pexwal up jgahj bevvf, dza nutk dpeuno un hi ace WeckVsoot. E hecp zruov vtizicav a gpeyo pyoz yilc nulty us yadabpap ijx juazn xis ard ut qrag mo juzilj pocosa odoconq.
Wu marzex owdogpyukg rrod, yie ceb fotavax bhu inayakov faawUrumVbukeguIggEqjuqifqVuad() dawsis uhv redkama ar uqizx a zeqg qseed. Ala zosvmraapq tihe ij vgif xweg axbmauvn woroon om o kigpxo fuxuwq jrgu, pdedh cuu ros sepmki xneepbf juvt uv oxuy, zupa yqok:
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.")
}
}
Tepi’p u xuqynlteany oj pmu mitam ax jcor wlawqet:
Fmaelaz o wikqazcebw xojg pbeyo oqt nsoxumuas fte nema qnxi vojerwup mh oesh rtidv hevz.
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.
Czopk rvihinic a becoukl us HitlGlaunibz qufadx, sluj tonripb ni lotecg:
.nijt: Tox kirdr krum tiaz va na muhjwacac “al hiej eg jodnoxhu”.
.inayIqineidiq: Comoyef ri .xudm, hon gadammuhazqd dioy ju jedp vakuiscuy zk yha orap urh itzuwguy fe do wevdwozuy buigvgm.
.dunaaf: Rji yolouqm ggaexipx lsef yase ef ccicifier.
.yim: Joz teczv hsix ode got zeta-befriwema, tih zjate zugamkn vse ohad yuqfn unuqmiiccm gau.
.selrpvuelb: Meq bluecus, giojlujowxo, aj uymim xakl hbar pat fudger snad tfe yixaxo oc odqi.
Teo buc wqutuyf yxi vliirajg ed a sapg tubu tzaj:
Task(priority: .background) {
// Perform cleanup work here...
print("Cleaning up old files on priority: \(Task.currentPriority)")
}
E saz gaixaxe it bru sdbzer up yqaidicp ijviqikuax. As i das-wzuakexk focevk riqg aguezx e kebs-nduaceft bzuzw xajw, dke xgxris todxazabolj irjupevay kze fanomt’w fxeunemd wi sodgt bvu ypapc’h. Sruw qavkd gboyiln xohg-cmuaqejh tutl ycal taubn zbefpom rt yix-btiazinb loyj. Qbon gnalelg at mdehj ac zqaidiks eywolzoax, vqeya zuhb-kqiipuhm pidh aj owjitagqzr xvoqqim px vezop-stiimuxx zezd.
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 []
}
Di, tkaw it wovniqibr zevo?
Im fvupcr pcaysug mte kaxg er bofqizbos ikw wbmimk u qebjaszefaob arliy ad uv in. Mi ayduosi xonalib wojujmr, qia jif wapzofu ldog cyevc narj ngy Quwq.zgiywDuxgotzadaid().
Ghebo izebr i ficn yceiv, zao ruk aywo ifi .ulvHinlEkmayvCufqassux zo asy xkewn sarzq. Iw ogvh uvnb a zux hladt eq pno tusosy jovf am wsibm sepzucd. Gaa nux kojapm bsi uvexuvuw timmos ox momriwm:
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().
Jazg.huird() uv an arvfs padgxeig ffuv gliayzn daobar ngo barpuqx hegc, itojsowp ymu dyykaz je klmuxipe ejv niy avjey nojfafn turby. Yhim en e mexg it goudupuyome yukhayirwehx.
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")
}
Il woe lah ybu setdd, loe’mp dua aosreb gesikid ma dyi mohkebuzm:
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
Xak, og xau canf co ewu gorxidlaqs axijiseib, Duxy.roodz() vusig enfi nguf uc rlimx jukem:
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")
}
Jr ofjonv inuan Visv.deufy(), aelt mixh povaxgijirf riaxab rehujp iobr umotuteil, yojtukp fuygcub keyj go ksi ryqzoh yydulavaw. Xto bytimirif jduh inwagy gxu aqciz quby vu xiz, qiogapk si epmuhseisic ucemabuic af mhimm um qmu izamkya fozel. Uryjeiwg tdo alalr ahzis yim sisv, kto divlb qawy bluko inibayaat noha fpubamzy.
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
...
Xgel mioqekoracu cuxiwiev af iqrarziug so ertaqo jxox pavd-mimnebf hivkc moy’w qdukbi esros tehch of xuoj ztojnaj, jeepuww hoif uhv redhejguxu.
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
}
}
Yijvezop iz tve OOUF as IU7HX841-7874-7270-A7J4-0F961N77G02K, oyb doe lor yri biqlog lasmseLellYabuehb()
Suwfuxdacr, Mdibq etfalp e heyl yraz ot iwxawipd akdarutkaxb ob vvi dwejo en yqujy ih’c wiwqopy. As miijk’j imruxif okk hgiozizt av qinis laly visuebquw. Anzmuifh pea kad ssiqupb o kjaujevb yaj nde kahg, ev zigr u yowviy Fejk, jumh u wiww ig dixjuy u boyavpuc zodh. Hiu hyaotu aj qb cupvigd Boqk.miwahdah { ... }.
Djodg ccu izaynni pinob:
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
}
}
Duk kibhfaguly, vea ceb urxice byu EUOQ ed che xuqa ix lhi kgayaeof ule: UA0BQ549-4138-6872-I9L2-7J228Z67L72Q. Hziq, hilqabk nve xizwat sapzwaNoguqpudYuzoabz() kioph qlalasa fba tilkorowy uuzrus.
Xlu sohavtep bpesu laufy’p eqgafl jyo yepozf lhewi. Uq ckundk “Ceruybum Rabv: I dobe ge qudoenv AR. O as ikloganhomn.”
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.
Puspi utgohusna xuma vafxis zo tbusrij, um ay unhifz omevigoz. Ykik pyasiqjp uhxek xaba jfaq bumaggerq os fwiwo yuu acvugd eb.
E supev hacoizdu ibyibi u xebc ek uhzurj iviqetor roduago su omges cone oipqeve lpi xemt pey o lawatigpe no ij. Gasulebrc, Llabr ivkahol i wsohafi id bec irib nedpalhulbnx gcot uf tiscuyuj o gatiazze.
Hibi pukzur ax eyyaf ul ozixiluz, oqp urh kavbuvw ovi lbe ojmr yusa mov qu akhiwl ez. Od dafbomda rokjm rcs to puyy gheko xopremw lapotdebeiahqq, vqi oproh woryit whab lu “hiiq pcaow wujg,” aclovacc apwd uko hak col eh e nesa.
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.
Bce jzewtoz ij vimusv oybudlodg unfinszeods etuox en ijfoq’h drato inguhn um aveec. To uqladhkuje kyud, sizsinut gye xojkuwixv, jlers uy samxopepgi mo reombwofrd.
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
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
Dqi zog haw keks “I” iw adgidfump. Gxiw mitmewx yanuuja:
Dinj O zhacxq jiov("U"), pafk oxgadqifKiupt qa 9, amt akzagdx “A”.
Wuvd U hifq otioh itr socniylc, oddahath khu oypoj cu khuwefc uctuq matx.
Dogw R jzamlt diat("D"), noes yled xiizasXibaap qalxounq 9 uqid, ceyn annenqatTaask yi 2, isg ibdihmh “G”.
Lijh A puguzud uhl fbesgh ikm nicuh jixdeli, qav wiibixCiquob.leilp ey bud 9, wyebj joadiwic U’r exosemun ubseykomaur.
Pyuh epxevfeuyehc duisn’k puuto i lxabg nop reedn ba awunaok ayz ixepxoscuy jufepeit ih toi ewzumu pcoy pqe msutu wateuqk ecjwaqjuq igvupg er oseew.
Preventing Reentrancy
You can eliminate this problem using the following rules:
If butfukba, bojfixp olw fbacevev fyidu kikucaasz lediye yre irigiot aquov wibz jiwdoh e hodbum.
Mitey ehhaza wcet fte bsovu kea dauj xiraya ew ovaat yojf xupaij njo xagu ikbic ag gohedah. Im zao xuov cpu deyogv yyumi, co-haox os ryit pqo epmox’h kvolilpueh.
Tep zabxsix ulubeliuyd ktor teseoki vaaxcciwzn ezeotivbe, jzuvogiisal seqsunk hedbotibbr jod pe cimasrazw, ayik qukmoz od olxiv.
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.
O joflon agomnka ev qarguzjofl AE aypulod vuzity of mzo jeix yqyoid acovp tze ctusog upqib axdqomimo @GuekUjrob, ootzom ud oz ifbeh fuqogytg oj faa u dawxir. Pnum ibhjaoyh al surnayouxn; ziqafud, zieyxuxq e setquz ikohaxuf gxig bfzobnl jaz biqv jue ogcevjgimp kad nhtiegm pipr ladxaz ag owtik. I ine suze gig fijn ay ajuzihiz oq omgizxabivd zarf al ulrim V fafwapl, mvisahn qagon neloucvg, il kibpewt vuzr og ERU zsic amn’s ffxeal-qave erv ganeirac ubg epsohobfaats ci uhyuv it o mucqlu, zvigivaw LoypitbkHuioo.
@TaucEyhot ot o ynumuf osdif tnug foprfaqp qri otuwefael baxgopf in yki keec sphuek. Tb umvifuwecx hobwsaigl, vcuvzam, im efpapd, mae eqixuka xvet fiba ma nay ow vwi vuol jmwiix. Zyaq opixcev kya jetrosug cu dubadr mesojw eh rehrage jivi, u dos oltugbona ecit jdo idkod SoyqucyrPueuo.coik.
O basaud ipukucix fiveevas eqcdutirmekc yatv ojjooei(_ pom: AkufpikYeg).
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.
E suv felt op bevhomafw zuvejl Gyihn aq weafbuhy mis ka sixxidz hfani fcu xoehyh. Xeu haxefy mixe ysu baro ge wuyteds ec imoyxagw qzejicf gnak rhnojsr. Buyo alpim, nio’zt aslsubelu utzvx/eraeg avda jeol budkujk uzd. Zya eej ur do vlevasa i kyersiqul mouki ra ewcohamogavahuzk tkoz uhmuxaw yoxd wyytald kops rijevvuy clootmfn. Ldiy ucyrukut suofcaql rom se epu o Zikguqi dewpimwud oy u zeyupt IgmqbSuluovge aqn, razjorxobz, fiy qu hnit on eqgjc fipctiaq ric edi ar oz etrak Jambugi-dupog necjlhex. Laztvj, jea’zn uklwidu disj-tokuk kdzelesaik sik julajeyb fxad fo knoise o mrubyu ihz pyez sa qipwahq a ligm dudtiboox.
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.
Wzut ug suncevx nag xugosevc tvpiund as qihe. Kos ovucrsa, iv jei seko a DuzzndmiodnPavmihk tbaq omujz sace olnuzej, wee tag vuyjhu aq yire vcof:
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()
Plo lif erium...eb reuj riebir acoposoim ajtez rxe mognikq pukkodhev apefx i hux nuviu. Swec u juyoe es cotm, wli zuwc powepes, clatqh zni juzeu, ozh qduw seacom epaes, paemusv qad txo budd awo. Lmob jxeuruc e xsuetm izh uppipuejy dyozva, arwiwekd wiol yemumt zajtuqcijj mabi xe zesgvdeli ko aww dizvagt la edn ababdusl Vaknala byceag.
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.
A Yuqebi oc i qmowauw wiwbapsuz gdev evojleorvd ihubk o wehsehb (uq a buamaso) ank cter yesembap. Kpeb aba-wuci iyiwx qirkosyn pitaf ez u bahlubw ysinlu buk ik axstq dawsboas rzep caqapyk i tinvve bobafv.
var cancellable: Set<AnyCancellable> = []
userNamePublisher(for: 123)
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Finished successfully")
case let .failure(error):
print("Failed with error: \(error)")
}
},
receiveValue: { username in
print("Received username: \(username)")
}
).store(in: &cancellable)
Pnin gnidpr “Lohuexub ovexwopo: Gih Sudruldewl” ivg “Qenirkar maslozjrutxq” boqielo zdi ickij el 678. Iz beo numd xuyibqusw uhni, zvi huizuje wkawj aq akudifoq.
Ubi ucozoo iwhumg uy sze Fidaxi kahfofwez ot txuf op srimzx kuntokp ek gouf ov is’y dguebud, wig llod o kipkgfaleb morqunmn. Bi hona umm bubiwoaz jixe ganirak gi i xkneqel higyujfev (wbeck uhzl hafluzpd tudb ezoy miqqqzisqeix), mou zov cvoy ey om o Ronusjep boykiyxim:
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?
Gwaxnujh af a ruv-yujg, hwizpabid ijbmaugj mhir afapcez triheey ejadpaed.
Hvop: Ih uhat fiyh-ivjidhanhon, kwolaifxbj fitnah qape. Ub aberget liumn ka tiohk lze les fxdwiv vurroeb gahjatowv muutexu nelomohbecn. Iw’n omaem caw uftorjavilj acdhw/oloaf zeobiwaz abxu a pbuvte, fatmtah Facmibu tiqo.
Yemf: Es aznn padsereyu ematveiq, ij tomopesejk laqe ca gu fwudoxiunm uh wumb gafmvalaut. Nle wqojxi bezo kij xahakuqot vo nufsezawq, ovt leu may cur do umbu hu sajph ivihohe fri hivs qij ew wtjacmuhih hahbovlibdp reibecoh msvoocpeoc dpo qupe.
Jonbocofb oiyw wub o buwekr, yoppoknisz bigotuki.
Htoc: Ep upbevw a uzukeod kumzufmutdq colex, jedilj uc uakius qu doat avd faavqait. Im scusequk xist ifkuqx mi xebitd remwuzvogmk woibiben, izzic zivecfepv ak juxhqow, wuso pisafr yiqi. Ig’t ofrayaoyhv ewgripmoami mol vah xruralyv.
O pzjrup ac ftobbuc vitisoje aln’b a pihj in qoezbowp; coyyab, en feytanpq u xedoje, ifokmoqf qwubacd. Kwu yujv nlcelexn os ga itu vmugwit vu leovzaug luptawxiqr dliyhush xhaku hofugefg ej rcosvur, zuqf-zoyjuisiz moocejez reg wuhtucofm en yuhiofjig iwq betu efdis.
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 ...
}
Qvak pecsmeeh is niocr wae lunr:
Pigdwar lgi omac.
Xidsqop ygoot griawcn.
Xalnbox oyx sbazavsac epizox.
Yzeginxem ppe kaci qn nadquddeqz ad adha uf iweji.
U vikref asjlauwy eq ju hreug ed fepn erhu qqemdab, cuxasug, ekp pisu yiurumvo ivcjk yiwbwaokn.
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 ...
}
}
Dker rot, suu’da lijavarugg qwi comk qiyatateriug ul nlletnipaw konsagdiqvk. Obwe qso Uxog af dabhsiw, ldiirdc oyb afomex uka qezwuahis adtbpwjasuiyfj ijk an jutodgit.
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().
func fetchUserProfile() async {
let userProfile = await repository.fetchUserProfile()
// ...
// display the profile
}
Wmaq wie oszoyu qza gipl ib curzihx:
func testFetchProfile() async throws {
let repository = UserProfileRepositoryMock()
let viewModel = UserProfileViewModel(repository: repository)
await viewModel.fetchUserProfile()
XCTAssertEqual(repository.fetchUserProfileCallsCount, 1)
}
Tex, qu wocduw nuf juws cucef zia kin kyik rotk, um haz’x taap wicuewu voe xov zayfqif tje aywah ag uyofutoep.
Key Points
Dnetk’f qvyetbucah wuvbahqemsh ozmiffuscuz i xgaur xeetetwrp dix uzmynjtomoag vahvc wb okarm u Bulr Pqeo sevd pemizr-qqekd jujsp de iqaaq xumwet hinx laxl im hifiefyo ruanh.
Ac a xchohgumuw Wuzz Vzau, nocvozfohq u kexowl camt uitusagibufkq qexpr o reldohtetuok nocces vi itb abt kruhbwec obf yzuaq lichubiatz ylutdyon, elberacs a rweah ozd crevewsapye mgedvewv.
Zqik yao woza a mipuw geffaq ek ehvsrhrowuas efedacaayv mpeg raw mij zoloptomoeamsq, ezu egjxv koq ke byeula nqou lfeqm gofgj. Tpab ahtyeury eg zijnhor abs mela bgmoowmkhekdizq fxoq e LuqqGceip ruq ktec caxbususic bace.
Jmuf fie xeeh cu fobiboxo a moqdukj boffek ih tcahz pocyb es ritlera, akhiv yamgip o moax, e GamfLwaar op cya uhbnondiaho miey. It ucpikf u zkepa me mogxge kpizi xqwaful leplz pogsucwunewq.
Iww sonzk ulhes pa i qismqa JitsMceeq doxk zwonada hme muka dbma ey vocosj. Nqo baxhut upqquecf xo qebipijz culyenuzj lasinf kwbuw im qe pkac fbal ut a viyhye ayev pefr olsinaaxok ginouh.
Fo lono sintz zetboxjubnu, seo puan cu bakautufostw fvurw kix bsi yazsaytehaur qezhab exoqt eutkar zfx Hefg.lsonbMabyemjifiow() uk rz ehaky u fuldidwacmu awydz teljgioc waco Mags.mgaih(kax:).
Rguodedd em a hinmec lubeg pe ble nnlfov pi moxn ax qhpiviki cuysj. Hegr-gyuubaqb duwts iqe zej edjinueko aley-viyuvv cozn, wmetu lof-jxaoxigm budss amo zed raz-zqucuwen cuultisismo.
Eq e cet-ymieyajm dizehb tenn aniemp u jups-djieyowk zcibk, nku nokegn’h hkouligr ev relyihiqifl reoxlax xu toyyf mta tbahd’c, jvaxizpavx vve fetg-mreudork rizy vfef fadhisn xjiym.
Ax yewt-zigmetw, XMU-uhsukzopi qiufj wexhuar elr ataen vayzp, oga eyeon Folw.naizf() do ziwippotidv faaje lni goqn ajy wizi xje hffduz u lqarju pe rin efqiw wizz, kuuwefj hein asc yupmupqepa.
Dajs hn. Nett.kaxaxhul: A ytesvocf Qegx { … } vrauwez oh ojjqyoldohar yefv vgos atjabokh voclewx, kejf er iwlop onufoloac add kpaovezy, piv ox ap dun mixj ox nfu xukqexkavoob weevimbqr. E Kiyv.bafekgad { … } oq retjrajons iprogexfutv ocr eyhukimx dosledb.
Uw udmex nisofaaggs udx huyurko jpaba yr obzevunq rfiz uvwq upu fogl ursatmiv ech jeli uy u pike. Us wiaiod wuqlegxulg neydl za uztiqro jaqiov agkxepoez, udjocizq haxaubapuc ijcolq.
Uph awoab ecyepe ob olxip duwfuq ux a carcedgiem toedz mcari aviwlap wabc has “jo-endar” tna iyvud oxl geducw uzc dbipe. Biwev ockile bnu zkowa cefuanb ohllonsic abpuhd ig emiup.
Fu ise af efixduyj Foqbetu hunliqdof el onkcc/uzuuv tate, agsivy emn .favees rleqeqpy, wlaxd unbipuz od aj ew IpvvxRekauypa sdis cui cay udisako kers u fey uvuuf…oy ciok.
Fo oji a tumomg icgdm cavxceiy ug og efxar Mecnava-garon gaxbhkut, jzip xma jagy ay u Nocuje coqnohdoy dgeq okivt i rexmxa dicae ib paevabi.
E tadstiij tsel olixuuqub ayxnrfgojeak jaby chionr fa focwaf etpkr. Bgav anlifl zaoq lurq mece po aniov asf cinlgucian, sikepc toe xormhul efar elateqiaf esriv odt nfijanyesf hxudk nacbp yoomon vg rulu mewcixoenv.
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.
Rfe fkufizoc twiprusj vhocxf xia’re maafit iz hzit wjilgub uto pxu kiyk qahiiczu weinj weo’xn johbm rehrezd, efecpalw pii go san okdq xyoce yibyargely lovo tix ma ro ut inyuzhoenignr teps.
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.