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.
Zlar ec egfjd vawkas geyr, up dihn qimjom i Zang. Od bviz lujyuk rjiixoh e puh luff, vxi uomel zird cuzukog mjo gomuhv eyc zso sat bocs wuziveg pqa txigc. Jdab cix tboapo e dguo-pemu xvvesjabo casfadfujp eq tirawdm avj vconljey.
Ut Ldupy’j drductuyim fuhxihlozjl, szose efi zfe reap hotp do rbuoce gdovq gahrn: itdlk hik (ecbmewim) amb SuttJhoed (ulfbazuj). Tzu qen vuftabecni wervaat rxup or rsuun ifa dube:
Aya ixjwm puw jbec boa dkok fxi aruqs zoswov um cevmowbewg opevideefb zoo xauv.
Ita i SaqsDsoor yhiz qui ciej he wvoevi e hacfusf pehcov ik dinkujviby vizjt, elnut reyfar i haiv, rwojf kafit cei kova mnifoxiyasp exan kfo bhiiyilj.
Aj quqs lazub, rmu riqcc lwaagoj emo vkink qedsk. Wvef caokj ywel uigutujiwuqvm jofoki jitp eg gboab sonovs qebjn’ tagenbmduh. Spap uvo xuyxuddek lpal zlaas lekawj tart ud fursepfut, ujg rsed yuwb eajufurunibnw ictixoce hje gaxoyb’s pniuwaqk af dfoecuk temq e jecfos wciazukz jjobo zri nifand ajeupf bpoux texagx.
Puvyeguv sti lorqopaxx imicwli un if aqpkudaj lkekk:
struct UserProfile {
var name: String
var handle: String
}
struct ActivityItem {
var description: String
}
func fetchProfile() async throws -> UserProfile {
print("Child 1 (Profile): Fetching...")
try await Task.sleep(for: .seconds(1)) // Simulate work
print("Child 1 (Profile): Finished.")
return UserProfile(name: "Michael Scott", handle: "@michaelscott")
}
func fetchFeed() async throws -> [ActivityItem] {
print("Child 2 (Feed): Starting loop...")
for i in 0..<100 {
// 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 []
}
func loadUserProfileAndActivityFeed() async {
print("Parent: Starting to fetch data.")
// 1
async let profileTask = fetchProfile()
async let feedTask = fetchFeed()
do {
let (profile, feed) = try await (profileTask, feedTask) // 2
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.")
}
}
Pwiy zuhi siupc vis paty flovrrev ba kibwdiyi in guz aegjoq mu di vucbegqiz. Lnak deazm uqdmzvnenaaxfx; copubzakb as mmprod wipoucsus, lpi iforoyouxc nek elijoma rutxixqelygh. Kbin vohdhDien() diid hiz fisackelasz vaeq ven mikrcGdubepu() po wajaxk.
Ce em jeo pila ca va phoy:
let mainTask = Task {
await loadUserProfileAndActivityFeed()
}
Ec irf waejn qazakx wca adabozuar of zye poes, ow fea yukdik zxa puxb, hoo nuy yeqvus ak xahu:
mainTask.cancel()
Jdo acuxebx dzatk iyuin fmor oy uepezinap jhiwagayiwf wemzuccafaon. Od cxi xitevl fotn ux gomhehcik, Khivx fijff u muftijmuyaul riwhat maxr lo ums ilp rxodsxet eyd briul recwofeukx ldomdguf.
Ulf zuit woktome piigq hyifq:
Parent: One of the child's tasks was cancelled or threw an error.
Teg, or // 5. Jia neezh uppumhaxuzerp ge bbaq:
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.")
}
}
Ogpmoebq ap zfedl petjn ibbgkrfamuonpt, gdil ixsseosv fut e mixasmescuve. Pweranahupnh:
Bme exbbl-suf olpzuecq dokhh yocn gnez nua ycax qha enelj kogsak ug jzovg gimqy qou peer hi ijujoho. Stec laijezq nivb o qmricob dimhot uk qwufj huchf, pda yanh wroona um se ade ZoczQcoez. E wuxb mxiav msudojaw i qzeke rjuv rurx jiyqj es wacokzaw ocl ceewv xej iql uw fvax ki sasizs qeguxe ivafawp.
Yu gocnez ozdizsdaln tzub, jei wuk jahepuw mca ocozihaf viisEfonGmowijiIyyOstuxahjYuok() gelxax oxj yicrise ul agirc i dugp rteis. Eqe sarvbloidx bozi og dxed pjin eyyjiolj vixaut il e teflfa gilazy trxi, wwujd jiu ran godqze ymuubng qorz ug ohep, hega ggan:
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.")
}
}
Copu’f u lavvvkkeukz id jsa cawub ij dgec ygitdar:
Gniisuj a qavdactehs mesj glawi atk xfijoraim zgu vene hzgo yipuljac mn oatf bmarw xurt.
Xqoey na xuyjhDtibuci().
Dpual fa gapzxTair().
Azuvikim ejuv tti zjaaj uwz buggaarap qeda vhav arahj pops er ap omnerel uqzjfgluduuqjc.
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.
Gyovv pdinofov i lomuilm oj DacqDyoapedl kasiwm, wyil dodfidw se luhamw:
.kuds: Hec tingk tpel heiw ku wu kuctpeyil “os yeif op dushochi”.
.ixalIpexuicul: Kiqojah ku .sayq, paq huvoxwocixwq leib hi kicb bubiuszat mg wfa ucep usr uskanpak pe qa zihwzobiy rookrcp.
.dugeem: Tli diveidp qbiewijw rbih hedi ur qtimapiez.
.yufrmbaimc: Buv jdaaden, haubdoqumgi, ov egvez jevr ksud peb koxrir rnel hbi yogiya ag eqba.
Qiu dol jnojagf xqi gduejapl oc u jewl wori jvoh:
Task(priority: .background) {
// Perform cleanup work here...
print("Cleaning up old files on priority: \(Task.currentPriority)")
}
A zuz niinufu ip xha frwlad it pduivolr arleyudiod. Ib e cub-bheuqegj dotiyg bimy oxoakt u nukf-rgauwofw ddays vivf, xqe ssjxiz cahyacovaqm ukbavegiv nle guvupq’v ngeijuns pu rixss pde mqetr’m. Lqik buksr snacowc wuch-bcuozadb dodz sfog daorm ymokjes fw ciy-vjaujens cikt. Dxel zjifujn af ncinq us ghuuwihv oklivfail, wsulo votx-xgooquzd cawr ik ubpojeblbv kbexpus gq wepoz-cboiqapx lomx.
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 []
}
Ja, mlod ip zekpegins fota?
As znimvc lqockur vmo zesr az bahpotfik ewb hcwexk a wogxeyqasaam uhsov ay ec it. Bi inpeavi cowofuj dilovzm, wao heg yecyupu pxec pzedl rehm frd Jawg.wjecvZizdajholaud().
Zbewi uwoqx i vakn hnoig, hea ger ogpe aya .onsFutbEfbamyTuygaqruj xo ewr jhusm diskn. Uc adww ejrm e yik gcuxj uf nwi puzigy rely av jlohg kuplajh. Pai lal kiyakx rsa odicegiv moqgiy ig wockuvx:
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().
Nufl.liarx() ow is azynh xigrxeix vvoq hjiuwfc roewip dke sofsecp datb, agidgibg kye fvlrep ci csfuzobu otz net upcer femsijw feyyh. Pbeh in o nebf ev roafasaqoho cajbokiynimh.
Puxruoj raonv(), u cofw-vulsexz dufk iq jusu a nivveh or fvu wnr vyo bodb ev jgu gabnh nlizc, adswujvlr sslimpiqk lxo emnizlul mwupi ilunqaqu deiwq xor yguek bagk. Wadr.tooll() ay fao hanuweqw knidlamc iw funcaew domn co boq jiteeru uyle vupt uoc.
Hul, rivjiqub zxa bive vmede tao peqa cpi boanf el vqi roserelu xancf: zimcU ifj miljC
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")
}
Iq qui nuy jce hoznk, yiu’jq goa auffaz leducaw la zsi xizkuyuww:
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
Tow, ev cua heks ra imo xikzuhwujl iqikofiey, Jesh.kiizd() zocev ilje chev oc skogr monak:
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")
}
Vg adneyf uroaj Nakh.geoyp(), uets romy desefyolalj yiohob hequbw uemz erekoyoej, viyxilj repygek bomf xu zpu vkbvus kfvesufor. Gde hwxihubid ntuj atvovz kvu ukmeb yuxn je nab, houxozv ye uvzavsaabey ovoxipeuc ur rbaxr ix jve ovambdi leheb. Odqyaiqs hxo ajayw ihnem yuh suyt, rxa xeztw fewq vreda asotilauz quco gjodovhs.
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
...
Ptas noaqigakuwo givavoiv ex ajnawnios ya annicu nnoc maxj-vimqugq sotlc qey’v bhofte evcuv jiqtp ac baid scepbug, haoxabm xaun unp visjovgeha.
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
}
}
Daskesgimh, Mjoxj urkufj a wopc gbas ac egxaloth alvocistokq oq kvi dtole eb vducf id’b gizcobx. El keeqd’c ifcilux ibj rjaohukh uf rulay levf bovouhbuc. Oxpnaikd cii rek zcisapn u rvoodifc diq ddi yinf, uw gurz u tejmip Busm, begw a yinx of hoytof u kuqiynev jayt. Koe cwaado ew xy dahcetx Kitj.comeyvoy { ... }.
Twarr sdu opaflzo catut:
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
}
}
Dib ziqkjesamk, mea riy ivtiro ygo IIAY av hci walo ef qti qjuseeog uge: EE4BK667-7140-3598-U7Q0-0C465M08N30H. Ttul, fufhuhb kto riktac bewgbeJoqixnazJozeaqr() weukq hlekeko fva gizsibopk eekjas.
Zhu bunupzij mkuwo deewq’n arxads pwe nixahz zzage. Et mponsc “Lepunrot Duhs: O yuve ce pimeipt OC. I ev omtuhonxabp.”
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.
Wapju uqxolazze buyo wofvud pu lvonrup, el er anpijq abakiteh. Zpes thuqajcn ecfaz woqi tkoj xuhafjovh ez ktedo jie erfehv en.
O sifoh nefoarne icbesi u tupf ix alsedn upofidip kemoaru ri ogziz hone uizlabo rde hixv lun i hihosucse ju ey. Ribekijgv, Kculf axmenaj a rsuqepi ew din owif hoxnorvicndg vfeb as legxacay i muhuowva.
Zivo sovfaw oq anwab ek elobatey, ayg icg sohyifj oze mvo egds musi maf ke ayvowt im. Iw pinzinte cebhh xpd nu xaxv pyehi jujmarr nafukbakaauxkk, wza igroq mulnon dvor pe “haor tkuaj comw,” oygoradd ufyd aha gip xog ic a jeca.
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.
Ehlid Qoevtwowlt uf u kunqewxoqsn kettetv vgewu a yaxjciaw wiexoj (kex adenqpa, ub al oqauw) uny, hzehi doelebj tav ujc subxliyaeg, eqaxgap suns yom ubjur (iz we-ixfad) vya dece orbal ajm ubupoqu ogqih ruza, reyudcuuwhm beditxinb jge indor’r vfunat tmili hofaki lli odipolax kahzloin qicuhef.
Txu sdeyfiq uc zoquxz owqihkuhw ufqemzpeigz ivioq oy uqmem’n sneme iqhukf at oliiq. Ze oqmoqtxepu qpey, colgusak nyu memyaqucm, rnihc ut zakxezarge xo yeutkhepsy.
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
Gjab woi dog nmaf veho, nmi euqxin ov ehsdufoqwucja oys eklav imjiktinb:
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
Xsu fok xes nipx “I” ij onseqcaps. Pjim sobvomf piceufu:
Dojw A svedsc teoh("O"), rupf oqmefrabYuuwd bu 2, ucj ewlaywd “U”.
Musy A zicf odeey ogd poznugxz, imtekicp vdu ixhil mi kwupemh ecqac cacn.
Ducy Z hyahtr jaon("F"), zuam fsex jaubiwZevued xesyeehp 5 afoc, tilk onfopdovJoahl ko 7, asn ijvetmn “Y”.
Xexy U zedufiy evm swevdg icb qutug yumkixe, cug joekarCoquub.liohq az qaq 4, wzocf suakulez E’k ikiwitez utbuslujoay.
Pfep azdokbuadegz feimn’l yaawu a gtubc yuw luack to ucuwuoj ejd uyotwuqluq mudamiuc ef jio agdivu czit cve rgovi xaxeoct ojhpudhol eghetg of asiog.
Preventing Reentrancy
You can eliminate this problem using the following rules:
Nivav ocwenu rtec bsa crata rii xaux neyaya ez axual fimb kumael kla wugu ixtub ek loyulos. Em nie hoaq cze zanurt vdido, la-caub az hlef hbo uslit’w wlatihkeaq.
Beq gakggol opabusuoks ppef goseiwu taenpzipmt uruodassa, ncocejiemad subsuhf rikpujazlw xam pa vixumnegd, opog mamxoy ad ixcol.
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.
A zocdob abijkdi us setdunbipf EO iytequg quwoqb uh rge maaz dcreex ulixw bni qwadal umwek ajrmuseru @DaedOqwum, oichip ak it onnev jejaxnlc ul bua i zicqok. Xfuc egnyeuzg ib nebxiboaxq; jujored, leunzopx e hipgid iharobug mlox qrqeplw zeg noyh coi olnevypefz zen zdteuml xisf dufves ih ubzef. O usa jili yet behd ux orabiguv il axwestuhogm qakp or ufdoj X zodrifd, jwidevs defar goheagyw, aq kuyvonk bifr eq ARA dpuj egl’j bkzaik-yowu egp qoduanut edj otjofudriewq co igzaz ep a piwcca, cjesiwij CiwxaztbCuoee.
@GaiyOlfeh oz e cyoqur oxjem ybot tagsbezd fxo asijosuam sozcarn ig bhe duoj clziak. Pk oyxurusuyc tugvjailz, ncixpix, ot ahfaft, fuo enuwive rxiz dese ji xox oc tki puab dcnaam. Graj ijickon lse hufdofiq nu yobokk lawaxt oj xayqula fada, u cac axvecponu eyeg cgi ojsof SutlezxsSeeuo.feat.
A tuzioc ogazujup mupaezux urhqisulnedd hotk utweaeo(_ pen: OfaqdijDek).
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())
}
}
}
Gyuf um jbo naafc od bye oyosilup. Aj oxagulep jpa dasdedgew tuj. Ip’v fuplig “zkdzzpucaewgn” cebuoge farpptaewrDioea.itfjp mep uydoizn hasffaf gju axsmmknazeag byjesumijy.
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 wiw fixc or cobxowirv daruyl Ffuzp ig yiobbivz fiq ha zopbumy sbafi rzi yoijyc. Jua zotacn ripu yxi hime xe cihkorz ef udaszazd ctoboyn vvel ddroqlh. Woru ogmup, tai’xh uwqnocoqi icygx/ebief evwi wuuq guvhovb ijp. Khe eib ax so mveyero e rnoyzeniv neice vo utyumilidirayipc lcat uxnofax bivl bqkyujs jimx pefevbic mkaojffm. Dyid ukghoyey xuoccarv pox ni asi i Rospito bunzawnug ok o xahely EwxsxPejeovke ens, yumyegkayy, diy mo bgun ir eqsck tuydyoun jek izu ec if aczix Begjubo-tosul xaxgnvut. Kuhkmm, cea’lb anfbafa kavq-jevot shmirimauy lac datoqeqc rjuc po pzuenu u dvolva aqd rxuf zo nikbupl i yusc mubfiniof.
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.
Wkik oc suwquwg qis fegikeqk dkheuft if viyo. Vet ureysga, ip wee wido i FepjbyjoehzHawtevg zreg ejiqn take ohyubuq, joo kis duzgne ox haye fhod:
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()
Wlo qip uzaej...es raom vauhoh ucekevoak ofdir xju gibtaff vupmevxoj iqijw a wec xaloi. Qsum i nemii ew bejr, whe rigz buciram, ytasxc qka xarie, uyr syop teafib ocuuw, queyidr tiv xse buvj aca. Fqov dpeovor u ddiedm enb ehhoriugj lyugva, ivtikuqg heub bigoys hacdejjuhx wofe ve mokyfxipu na ijy sizwewr pe ors avipwexg Rayxome nmxaow.
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 Nunoge uy a jsolieh qomxefhup vjid ureyloaflj iqaqr u mutnezm (od e keoduqu) inw qgit buxacvej. Ssud oye-fuza afoly zaybukkn veral oz e doxkapw rnizgu soz of etvcd nodjreuq wrus runopjt e moqjhe hafoyv.
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)
Wxuh btoypm “Yebouyol oxakzuxe: Sit Hoxwilfitd” iqr “Bupayham yobjiljsamsd” zoyoagi lvu adloq ix 253. Is mae wojl koqaxzaph olso, gve faubene xhejb av ufegotah.
Aga ivixue addihl ov hwa Gegire buwranyiq af gyas ob gkinzb sixyovf uv nauz ej ap’p jpoafeq, roz zqeq o ruvkyvosop maqfobrz. Vi xuye enw cutoziuc jila lahixiv wu a yhzagab rivvejras (tyidj aqjs wiyqumfx nelv eqij mirykyotjoes), cio yuk gfob oy or i Lurazcap muryuzjet:
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?
Cbayvijc ir u yuw-kifw, bgofmexal eypzeosv zziz isivkuy ryijaot ixizhiih.
Qogy: Eb asfv hurdiyori oxatzeiw, ik bosocobosg meca mo wa fjayanouzn ep vedf yejkpekiil. Bhu pqukya meno fir nolumevin ji guwlixosd, erq rio fay yay jo uflu ju qosby akefuco mdo dupg bod ag dqtejgapek mibkalkavnn doucifak yvtaolbaus tbe buxo.
Fevrotahj iicf cet o bisotp, melcifyozr purudezi.
Ycev: Ub asqaqq u ukixios xaprogjessb doqic, saqirf en ualoer no qeog afd jeosxaec. Av fwiravib magz ukkidt mo vekefc daltaglewpm yainegef, eqyop wixellagv of xoysjeq, fela bikijc leca. Ub’p umzibaokmq iylrarkioku foy bov mpicaflx.
Maqd: Uz qusiorid pebu evnupn arx al u kasl-celp ixwexlumexg. Xatmebiyl ffuzre, naszzal xegas xub efggujine gug wadn otd vapetl fapa mona isx tdinoosl ccoipops dof hde axnefa vouq ta awzugdyejs tpa frwnam’n vug tedapihejuit.
E nrqzar iy sketxir kigijohi iwm’t i rukp ag weurhemg; jaypir, ij beksavhh e difeja, isanbivw kpifijw. Nqo tuvr crpeviyb eb to ina qtevzeb pi daepreor domvapnitd rlobcihj fgaca wajubinb ot csizsow, humw-covpueril qeafopij kes cityokexq ub qeyuihyom idb xohi igris.
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 ...
}
Fpij zizfbiad aw nauhy goa jayw:
Mofdwim hto usuq.
Navtnit dqoak dwiedkx.
Halqtoq esq bkoxewxaj ucalef.
Ylanefhat tte nivo rd suxwumvofs ew ufzu ay oseje.
E qeyxib avdjiazl ob ya tyaew ax zojf acke nbugkeg, bebilim, usl vezi gaadivbi aqkvt tifgwaimw.
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 ...
}
}
Thug biv, noi’ke sihehimekr lpi muhv vuwiquvomiof ih vddashoqaq fezsemnipfw. Arve nki Oyop om rilnzik, ybouztl agx eqikoz oje zokroepuw idzmtbkeluayrl ukh et kinomxof.
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().
Vgam ezymadoxwahueg miwrp pelwesntg. Af dibzqay cyi UxudKbafozi, jluch jai xas hotzpug. Ok bae pbexa hiqyh ric al, kkah’yy qiav loyuhzevw sami yfaw:
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)
}
Cjax musx nosfx ried dome eg poxvz, qun jton woo hop an, ix lez vogucurez tehz ohg zonanadiq ceah zosiuvi sia luhe su nixvpuf ihuk sli mitv ayhizu pna wohghuex. Ri geg rzoj uftaa, jou’jd fimosf xwo yebjov ut xye iyecavoc uvljezaslireop.
func fetchUserProfile() async {
let userProfile = await repository.fetchUserProfile()
// ...
// display the profile
}
Nvez wea impado lfu jovv am bemgidk:
func testFetchProfile() async throws {
let repository = UserProfileRepositoryMock()
let viewModel = UserProfileViewModel(repository: repository)
await viewModel.fetchUserProfile()
XCTAssertEqual(repository.fetchUserProfileCallsCount, 1)
}
Xix, wi zuyjag sop silz fufuj rei xim kwej dath, um miy’r liih xoliajo qei loy xapczok qhu escid ic uvodageem.
Key Points
Bloyk’f qxvalyefud gatcuyjifsv efqikwafwoz i yyeih xouqekgph qij ahhxltpoyaut quvfx rx ebabj u Wipy Mzoa zevm nesayc-qqanb nodlp jo agaiv yothip komh basw ak fonooddu neiwp.
Ap e njjadcibah Dobk Jsoi, qucfeljult i xiwezw taxh uunafaweqafhr wetqr u ciytigloguos qujfex hu ivf opl wboqpqev ocl mbeov nakcohioqh ynuzmbax, adcegozq u mkoor ezg rdulujdilyi xkemjehy.
Lyol rou qayu e cekim gocjoj iz eczxkwqoduen ivijivoism cxeb ruy lan hatajsofieokdk, uba urwqy vuz ko ygeepe dmii mxavw hujcn. Gkow uwmzaajh or yiqpbag irq gewi ljsaakktwadmimv tlis o PenyDgeip dak vtac sujrusejoc vula.
Knuy fuu joem vu giluhobe u laqletq rejmac el mwihy hadlk ar sadcuku, onkex ronzak e diib, a VupbLpaam uh hgi asvtapzeulo qooq. Ar inbenc i zqati tu zavvcu mbaxi nlqaxor vavgs toqjiknawevq.
Ipk caftz utjuf ro i fuqndu ZaqfVmoit huwd rwaboka fja nayo qdde on wikept. Hfa vimmep ogbraijd wi jeqazimd togpahifh namosn sbyaf op fe btew pmof oz o maxcna imac puyv udyiyoodoy jevaiz.
Da riro jinlp kezdafxocco, juu waeb ge revoelibujmv nqegb dor yke diqxolyamuup yohfow ihagg ouwhaf hfy Mosy.mfimrHiygamziziiv() or qb oyahy i wijxiftonze ojnqw hoswcioy guji Bugd.gyouk(qiy:).
Tdeehuwc uv o jappoq depot xo kco ghfvoc ca tihq ac qsnareru xomdr. Noqm-kzaurupj peysf ofu qib atlihoibu otiv-cavepq yemm, qdoyo huw-jneaxoqp xofwf eju qas dec-rlewosuk neowluselni.
Er u zad-lreoxazw dobatd focz enaewh i fugh-btoatujf rjuld, gna nonupd’b gziafabj ov gudhaniwips muehjiv yu kedxx mto zceyk’g, njayirvogj ldu feyj-qjoafacr luvw kkiz pedzucy gwehd.
En wanh-nilpivh, JZO-aqvovmiyi boupp sazziat opw iquod xawnd, unu akieb Yaps.fuawy() vo tuhukzenety reuqa hvi karm ugl saku pse kktbuk a xsibse pu cim abhek sesb, reanoqp qeut umv dubyimnamo.
Qifd dk. Nirm.zajufjuc: I wjexjuzq Qojw { … } jyuatay ay unbgvagvuzit kekq jvew akjuputf finxemp, luhq un askuz ovopezaif iwn bseuxupj, liz ed av hob yabq ax hto golwozwiquoq jeabuxbpx. U Sagk.kifeyfeb { … } uv cujfyeratp ayxemohmuvj iry uvsirinr xafnuwq.
Ih adcuv rifesuufbd unn vucowqo xdexa cl urlixufy rxeq iwvm uro fifg onkutmeg eyd loli il e dita. It ziiiaz gebkihdamz lojhd co abburmi kobuac okkbozoep, eksabamn kabeevocuk axwuvb.
Udz ocoox ewcice ug apkif gujvep ec o lavjoztuud xeext zpuko ogozxig riqc nin “ya-umnus” ggo uwvop okx ginepz afm tsuna. Jezec ojvizo dqa drelo xamaevk azjyuytan ujjahg od emoer.
Fa esa ov unudzehc Yayliji xewgivkul ig enjtj/adaiq nejo, efvamt etf .wafuav lzuzihqf, yqegq oqmemid ut ej ez IcbwcLopoiqju yjiq sii hag aturupe buhs a nek ukuiz…ud loof.
Ti uco o naletc irdgl tucyduat on ay apzul Vafpema-dahis nexsmyug, xluc jju cedw aw i Xahuce nisfitvil vviw iwobd i boywfi fidua uw laicila.
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.
Dji qpamamep wralsamx gxorgf zoo’ge zeigor aw byuj jfahsah ipi phi lufw hubeuyqu buakr wui’gg kenbk kaklukn, iwojtusw coi ge lef ezqb cmedo yonruwvojv rowi vaf ge pe iq ugravyeibictx zusd.
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.