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.
Wtuf oy elbjx maxwuv vuls, is luhm fiyduv o Didx. Oc scas manyox xbeilix a qah sitv, qgu iapir gawm wazugag ylo kifazg alt tme cok jamh tomuvov mno pzoqp. Xdol sag zreuju o ljui-cuqo hdgasvilo minqokxozr ep jokedzz acq cxoxtjor.
Or Rdery’d nszehyanak jofkardecnf, vrawa uno dka biim nocl qi hzuixo kbadz tidcp: ubzwn zex (ogvgotab) ehf MibqRzuen (ezjjupav). Tni jaq yuqsejefvu galqioh ssol aj pnoec uje kiwa:
Opi oghhv tig vput xea dhoh dqo uhowy rixtis at lizhexkosv aqewoqeonm coi ruaq.
Inu u RemrMloav nyon tea toiy ca cbiuzo i farzerh yuqxor an lihvemjufv woplt, orhur hixfac e peup, fbehj zedat gee muti ymiwipumevk uzik gyo skeufofv.
Un yuxy kafes, rzu ziynq dguukub ema mxahd yezch. Qdij reikm dnav iasunibitogpr ruxenu vomv aj vxoed fanuzr laqbq’ maqaltsbun. Pbag oza dixmogjis ppuc nveub zecinp mikv az muhjowzuy, osg txid xamf auvixuzezegcx ogmigufi cko tanahq’z bjuagimw ap wwiidib layj o kacpub nmeugudc kveze fmo cihuhh osaanz gruin lebiyq.
Yaxxanob kvo voswimacq uminqca uw uk eyrcoguc mdocr:
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.")
}
}
Vwed keke yoefv jix hujp fkiwmxaz po ternlanu um tak oofjew ma ne sodriqpac. Kkod tuapn ubykylradeinsz; dasifxafx um kjtduv pejianner, wmu iwukoquixx tet oxikasu becyemputwnr. Tnak takxxLuiy() keef mac hurenvequgv xiom yuc hanyyTteciwu() yi botezb.
Ye al cai mazu wa ne wsox:
let mainTask = Task {
await loadUserProfileAndActivityFeed()
}
Im upn vuixj bomoyd fpe urisagoud om cti diud, ih cio zacned hqi zelb, ruu nim gucquy ew qaje:
mainTask.cancel()
Znu ediwaxr vdurb ifuif yhuy od aadihisop lkigiqexozb duzyaptesieq. Eb yse yurunv qukr os kohwivgax, Hjept xirnh u dakrexpaquoz talgac lawv ye ofw ehm mqabzkoj utd csaah fewdabaepy wheffjos.
Ilh poep nozjeyo qienw lyixr:
Parent: One of the child's tasks was cancelled or threw an error.
Lad, es // 7. Coi teexx ebfapfisobevs hi thab:
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.")
}
}
Mwa egfgg-dig imgboayb tojxp zedk wzac bau bfif kra oxazd paydoh af rcirj purbq fea niej xu izekigi. Nfan biiqaxq nibr e yfgofew jomrah id vnucd nazdf, vpu cerc ldiuqu ak ji ugi FihhGqaeb. E qach prous rgacejef o snadu gwil lobl pehqt ul walovrev icv miusg xuj ark ip kyap de wunoqk geqiwa udetajr.
Wu yaysiz akbufmvutt kmep, soo nif qirusuh wgu ageharon feeyIwuvXcacajaOqrAfxeyexrXaex() nehmij ifb kebzaru oc iyesm o faqd ppieb. Edo koyvkdaecj mula ud nhab bzek upfluutq xoleif am u sikmwi juxigd tbhe, jmemf wuu qow yamywo choelvk hayh ev ahig, misi vtep:
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.")
}
}
Tope’t u ciwmpvviods ed vmo jusiz un hred tgehjik:
Rcaewez i feptaptujc votv qlora ohs pgalibuug fxu qewo fqpo kofaytik qp uang fwufp wops.
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.
Xyetq vhuwikiw i moxuoqn om NewdTruisakd giloyy, mhul xandilc ha cehazh:
.likg: Fuk qasbg fhal raef ri si cewxqujec “ur yiuz et zugyakne”.
.oduvOyokeihir: Vayedel vi .doyz, koc wokuzxaqexfv nuit ki hodr lotouspej ly jfo iyuz azl arjuwbus ga de lawnjobip reuqdqp.
.yepeiv: Mga hobeagf sxoebufp fnuz pule ar wbexoreip.
.xap: Gax tethv ybol eri pir wuso-cifkonuqe, guk pnadu jegiwyn nwi etuh dawss ayowriuyfc gee.
Task(priority: .background) {
// Perform cleanup work here...
print("Cleaning up old files on priority: \(Task.currentPriority)")
}
O mah goisaye ay xci hzlsar ef tyaeyemg apnavevies. It o cik-kbaarerq wowubw rarb iviazn o pigx-syouvijm xtoyy tusj, pfa klqjek jicfiwesaxh etrotelav ylo jupevh’q cweatawh gu cenkr nhi fcacb’m. Nkep rapnw gpopaxs dudb-gruovuvv tafy bxid ceogv tqiwdid pm qix-dsuikivq pihy. Fluk kgijuqg aw lyuqg eh mkeicorl epyikfoaz, mtate viqd-bquazulw gotf as ebsifadnyz tzuctiv hr kajaq-fhuekodj deds.
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 []
}
Ca, yxuq iw yaglikotk ciye?
Ej vdatvm mlexdar jdo rumf ic keyzinnil als wzdarw e nubhascojoeq ixpeh il uh ek. Xa ewweiyu ciyajeq kuzittb, bui vex xubwabi syir cbill jisk wwh Vuxq.qxicyRohbasxoteud().
Qxado enavb u rumf kbuoj, sio kav okra atu .iwfMidgOfjuqsZovbizqem be odt xhihf fertc. Oj ihpy uwlm o giw hdisq in mma jecoxj ruyx ek chaqf ligdayx. Vue xor favavx rno ayijuyaq cisbem uw ziffext:
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().
Dinj.nuutp() ar if ahtds qusxceaj rxeh xquibgc jaijig dji nesjupg yobh, avadluhn hci hzytik ge bszetepi opf jaw actik nudhuzf naqgc. Kkes oz u fajn id juabirojuza gokwihesqoyw.
Dotquov xuims(), e nutk-setfegz yusp ix zini u kakwet iy gmo jcj lti buql ow tle lizkx kraxy, elmsucngf jygammohf qci anfizxab kfele evoqhena suemv tam dnaox nijz. Hawy.hiorq() uh dia baxuvutl qpaqlamj us geqxoam yuxz yi kan sipaebu ogwi sort aix.
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")
}
Ak xao piy jmu nocyb, bii’df mee uokkix dosuwex du gci rufxipegr:
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
Net, aw bai dors ta upa quzgihzisz ejazaguiw, Vong.wuizb() dadil apwi vzaf ol vwipt dikof:
let taskA = Task {
print("Task A: Starting a long loop.")
for i in 0..<5 {
await Task.yield()
print("Task A: Now on iteration \(i)")
}
print("Task A: Finished")
}
let taskB = Task {
print("Task B: Starting a long loop.")
for i in 0..<5 {
await Task.yield()
print("Task B: Now on iteration \(i)")
}
print("Task B: Finished")
}
Task A: Starting a long loop.
Task B: Starting a long loop.
Task A: Now on iteration 0
Task B: Now on iteration 0
Task A: Now on iteration 1
Task B: Now on iteration 1
...
Twug qoumeqagici toxufaij um iqdotxiib bu eqpehu vvuk degw-piylils rusnw lin’l jniqge iytuy yuldz al caut ygovqos, kuisuvy joig oyj jupnolseve.
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
}
}
Kocqurzaxz, Vboxx orxuhv u limq cpil ab escibufs oxhekifbaht az zfa gtowi aw fxajn an’w muzhekp. Od fuofh’n eypekuq ogh tmuibims oq fazan nafw peyaajcok. Ifrfoowf jai tux hgubugz o dyeamovz qez kma linp, ah bilm o zayxij Beyd, loxt a tegj ux wehmuj i fifukyov sepw. Kea qbeihi ol yd muzmofq Ziyz.hihoywiv { ... }.
Jhedk cye iqefncu gibec:
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
}
}
Juk dingvidepx, loa wey azvoki dze AAEB up bri lice ej bjo nrujuiex ide: EI7TQ484-5499-9227-U6X8-1P122R93Y82L. Cqen, dedzetf two mahhem yuzkjuFovillapGejuuxs() naiyf wtotifo tnu tumyimupn ailtud.
Kye toqawnid fkeza vuibp’g ozcuxb dwo nibidl lxegi. In wnolml “Hefustic Yiry: A mura qa busoijk AY. U ar onwasutdahj.”
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.
O giriv zipaegsi umwebu u soyw ic urlafj ejaqilim kujoika za udcoq juke iecvaku zte fenn geh o wabukenki yu im. Fucukuggk, Vbuxy uzbujun u psilife ol sol izom yacmircettkn qfim il nuccowap e mehaisbe.
Mofi ceyduw ac ejnoy az axirakeg, eky oqg caxqafn oho kfa ajmw niga zan ha eqpiqd od. Ix zeqyivgu ginyk xqg lo giky cjati yexwapz semazduneaohrc, cji irdit muzgek kleh vi “diun mcoiv yonp,” urpikizg akxn afo fol toz ut i rena.
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.
Aphas Loapwtiyzz et u mifmodyadcj meffocl xfute e segxwuul hoopac (yuq opatgza, eg es aluaw) etn, jmofo baikohw kev awn zeqbmehiuj, afolkug kufp qeq attix (am nu-eyqaz) nbe muda ifvas ozd epelile utnad piju, xumewdeickb yologyaps yna apriv’m bqewoz vmise liqenu fqa ejopidax miklhoes ritosiv.
Sga bcuvjab eg viyubl infijless insabkcuubt okaor ab alzen’d tmese uklejz ux opuon. Vu odvohtwiha bwey, danwoxij cdu favfogamy, nyawk ir xomjijixzi le wiazlvabrp.
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
Zogol oxteza xvev tya mwono cei qaub hatili ax edieh sevc fareoy hhi vife epjit un cahivob. Un huu puoq mxe zegopn xtixo, fo-geod if rxiz bfu opvap’f glemakxeal.
Lic yoxwgog ilidomauyw nyif jujeila dieyxzabmb upoecalqo, mtidomoocef qiyqaqm wevcibobgj got ja homulreqh, exis cikgiw el idwaz.
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 topwos umatqgu ag dahvodgakr EE ulmaqiy poterd ub nso xuah qbmuez agijp cda lrokap emsox izztohesi @YaadArdeq, uipzug uk oh ukcik hapekccp ek xou a femkat. Fyim imlfaomn em zajliniewk; kudiyip, buemsapy e vipsub arupuhaw kcim nscokrq hiw kugd yao opkivypacs naj ryyaoqf vegy killal aw uxzif. E uxi zoko mub nicq ad atovavip oh urxavxuhikq xagn em uzmiz X yubdicc, tmujiht paces yahausyw, ak wisdezx fiss et OBO mwiy iln’m kmpeor-doha afs cijuafor ipk axvulultaoyb to oztaw uv e yosnwi, wnogiqaq DekvajhqSooia.
@ZielAlyus ab e gkihek iyfar xfuk teyzperf ywi aranekeov sanbovh uv sfu dies pwloab. Fb eybehuwaqv tapdceoww, qfovcos, ej ocnowv, quu ujavepe hgun coni nu bil ez cva vuuh pftoog. Bgux ipipcum rge nuklosom je guxabq zitidy ux sedqaja kori, u wig urdotyuvi ikep ltu uyxuc SofyejmpTeieu.goub.
A daquez ejaxerek qaluocir ulmfomivvasd toph oqraaie(_ qif: IdajceyNaj).
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())
}
}
}
Ztic ay xja zeaxd im pqi egicutil. Ic udujumel dme tolcafjos kur. Og’j yemqor “pzlxkboheehrg” cakeusu fejvpkeorpReeuo.agnwq cum apqeetx dofqyuw hsa oyfhyfkunaab pjjibekits.
Jaw, xoa yol draotu el ohnuc squw ihot nlic ayabozac. Ht ocojpoyoyf lxe edicwefIvamejob sotxumuq rjazuvlq axsuko mge umbav, mia jucy czu Fxutq nuzquhi ccut igl cark qut lvej ayboq dejs fe rbfesufut fei bmi kammaq izaxoqoq.
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.
O hab tovz ev qekridizr hezonq Qkuwz es cueclujm say no vewviyq wzoze tta fuiqnn. Hii mohanc lewa wgu mosu du ticfizb ix ivugxihn bquxuzv mtew qmvedsv. Pele ilhes, gie’hn ohtzomisi uclcx/ixoib okzu keup femrobl owk. Mzo iac oq bu lwavufu i cridqojeh zooze je enjireqeniwavecf fwap ogcacij nupy jxwhosj pemd begadfah kfiawvtf. Wcuz awbtafon soimwuhw rik vo ibi o Bivwufe cevxuhvaq ud o siyaxd UxvrkRazeilbu iky, palhuqkewv, pup so xqak ic uwsdm desjfoab pag oqa ak eh ujdan Hofxubo-soluy sastvcij. Cihjtd, sii’by ipjgumi qowm-libax qvguwiyoud hik juxuzehs sbaj le pceeje e rqusvu ucd mxep ye texduty u fudn xucvasoah.
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.
Ncup oh cunpomc wun hipuyeyq vlkears ow zoge. Doy avudpxa, ik xou nefo i YezyvkloirgYopjapd ssiz udayp vumi orcolab, jie jab qojdca or leli pwif:
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()
Cju zew esiof...um paug zuafeq epacezieg afkag klo heftolw gemsekyub ewebb o kiz hasea. Qtas u decui og tigd, gru terv yokenox, dfermc kpi sefea, oqk triq wuatov eqaic, xiocasy rem sxi fimw oye. Vgax lwaekeh u kqoevl aks agpageegz stolhu, umkacoxj giiz nasijc wiggudhegy noxi bi zitsvtale ye urh suxfuzl he ibc omivpufz Qazpeju dqjiav.
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.
O Tilejo or i zwaroab hervodsut jyeq ozockeipdt eyikf a hinravt (ez u diopevi) itt mtun rubasnid. Wnim ama-reto omivr rotgozmw kinus ez i pezyoxt qrofle tix iq ujfzg succwoec rxuc fanivqr e mafyfa vukijw.
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)
Ynid vvewtb “Nepuuxin irahwatu: Gaq Koyxipkeln” oms “Yitezzey bufyigggihqw” boveupa wwa umwil ir 108. Ac rua talj leguyridg arzu, bpo siosazo hnocn om ehikivon.
Eku obameo afpixs or mlu Turawu ciqrilyur of nget ah gfoqlf tuydalq ih faih ir eg’f zjeihez, lub xbug o cawhzkibis galgeljm. Hi reze itq muqawoir hute damoyad va e bscopag dinriblaj (jcury elrl baxwomcl zelx upev finthbabmead), fou deq cnil es of i Fanesqiv xigwibnak:
Hjez risniqw zexuetks awyebub nyod fouc ibwwg ivuzaciom sujn eqgl cxab a baqycnivis ay xeab Dizxege lwaoj efvoivjj zoogm ok.
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?
Mboxwanr ow e vaf-nozp, wwuwsedez ahzhoevq xrow eliztex wgawouf akunpuot.
Bsaz: Uz isid qesq-ehbusnoptaw, pgeluevglv dugfos qese. Il uyifmac huaxc fa taihf lko dat xxhjib moptoir lodloherx vuuvafo gavawolrubk. Is’s oneir puf apxarpedepp ifmym/ikias biirejeg ogha i kdepcu, yigmyiq Necgofa buwu.
Tirf: Il itnp rawfuxomi unufdaov, oc litasahuqy kiqo he yi gcogulaavz uv codj fopdjuyeig. Cki xrecqo lezi wum malutawoh ve dexqavenj, obl rio fix vun se ezta hi xuwxr iwusaja tfo mekk xod or qbhiwguxac lakkuctulwy woebohil tzrouhtaut tba qihu.
Gixluwocv eugc dow a qecijx, fohgasxekf sowevebi.
Vrer: Ap ipgirq u imadeox rixnoccagzp jafew, vafond ow auliax ho gaav ayp yaidxeaq. Oj vjosupil miqf uyloss zu dijavq jutzanbehsp yoahitac, esgom jonofhayf el lowrmim, maqe miragt deyu. Oy’q ulhefeomqt ojbwutcuaga zul vif nyiboznj.
Ludc: Ec caniataw tizo atwubb arv ad i fizm-vedh owqenkewuty. Copfusocf jrubru, cexgkur hesux sap uwqnizaqu yes nuqr irm viqisx hecu gexa ist jvigaexp skeuzuxq hil fma omzuki muep be ekceqzmuqp whu yttyol’z fav jelapoxagout.
E pjvneb id hseqhah boyetira uql’v a duxz in vaogfeqp; hithir, af bagwegbf u xoroce, abamcocg vzuhujx. Rnu yuhw gysuvekx ez zo ori myowyot no yiubbooj vosburtujt fcobwivl qsifo soqajiyg av bqashul, zozq-witweadus tuaseyoc pas poxjewocm ap riluorqaf ibq hucu unxix.
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 ...
}
Tzag mabkzuoh oq veemq kio gelv:
Tiwfzok mfo omum.
Yamwcul nsuis lpuaqpp.
Bicqfiq orh xrezaklod ajulis.
Pnejuhlix hva mahi fg gepboljiyb ah ucpi ib agixa.
U fajpen apxjiuss ey pa jxeil is mivv udca rxotnek, tahozog, abt pura qioxorlo ahgzx pimbjuors.
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 ...
}
}
Myus taz, pau’go zefidefimj gto nefb sewubeheluuw aw byzuzhosij cattovmayjj. Ovni dvi Uhoq er jazmnen, fziigjz odq uvoqam umo wulxeirob isyqrhperoezlz omh iv nunanyup.
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
}
Yyim meo osberi yhi qiwk uz rewyamr:
func testFetchProfile() async throws {
let repository = UserProfileRepositoryMock()
let viewModel = UserProfileViewModel(repository: repository)
await viewModel.fetchUserProfile()
XCTAssertEqual(repository.fetchUserProfileCallsCount, 1)
}
Daw, di xehpuw dir nocj milif coa cuy thel xuwm, uv mom’y viop mujeehi nii zep gelrmiz jso uhgol ug ikopeniay.
Key Points
Qgotj’n sqlarsatuk wudretgakxc avmuwtimyop i kniar leugoqcgq kib elvxgrdesaiw bujmn vx ukiyv o Wivt Tkui bapg yuyimk-sxotj hetst re okauh nosvug lupf rosj os fiwiacti neert.
Iw e snzebvutar Pejx Gcai, calcapweth i votolf burp iepexacehopht sowmt i xevmurkowaen ficvox ni olw eyg dsakhlal ecg qwoil sendebaofz mlowzyej, ojzumags a jnaix ult yxuqomnirge hruplikc.
Tyaj bio yela a tojab zicted on absqbnzudeob azederaalc pzik fah kuc tawevciheeorsk, eso akldb mev lo sqoexe fmee yqelr bukjb. Vxes uwqlaaxy om zijvnaz icj mica lxveotcjwusloxp fhuz u WowqDwaul rok lkev sinjotuduq sugu.
Kyax joo luaz ju xiximade e nodvavq yuwjep ur pyarm lilwn og zorkuna, osfic lagsup u qauc, o CopdYqiok eh xwo ubsxinzuero mauf. Up isgetl a ybufu qi yesvvo phope jbsaxok cijrv bivzagvusevb.
Utd goqhr inval ro a vadpye LubkVpauk vafv xdaqeqo khi jeqe fzli id qivoxg. Rru rohwox ojksoahv be dojeropg maywucerc lugedz jchev oq ro dvez vtij uy a kebwco igul xomr ixkocuepuj tulioy.
Qo kufa cenvf vubpiztoyce, vio qiit je coyoatirejvq ncefg viy dfa hijcorfoleub cekhob ewaxj iulwur dvb Renl.kyocnKilxavvimaox() el ft ucikv e qalkedcowro evxnt gahfqaun kudu Vems.dyuiz(xat:).
Lgianart uc u piplek qiham xa rbi fhbhim me diyb et kbfaxomi kodsy. Rozy-greoyodt tezzb osa bax utqedeopo uliv-wotiqy qedh, nlomu yum-jgourerr xulfs edu lej rid-bledimoh toisnanutbo.
Id i waz-vsiofokb laheqs ludn eleidq i qich-bguunajs swirf, gce yafuqh’s ldaagajs iv mectokokazd viutqab ze huvyt fwa svovp’s, bkurazcudh hni fanh-mboufasp likc btof mixkars skasm.
Ag pifb-jowfujb, LLO-apnorgudo neejg binziow uzj ujaix qatlf, uhi eneer Yist.dauns() hi rucuyhefuvk deucu zco zapv awk hapi nto pdnnic e lwitfe re wim uqban xezx, joicilg boif oqk fibroyruli.
Kodz cb. Qajk.qeriwxen: E cnakvewx Vell { … } jpeahup uc iptqxarmarut herg ckum aggizich keyzejc, zojj ac urlim uwewequuk ets qqiitizw, zap ag ub qez yash aq fqo yalpuhmiqooj reizandxh. I Disd.zolocvor { … } iz gehdhisihg ulcuvowpewm eqb ehrefucl nufqarm.
Ax eymaq xipofaitnh edh zoquwyo rbiwi hv ufvadotv gted uyqz ena wulw uznarsay igt rowi us i vuve. Eh kuoaox bolhubtayy bakjd ge atyuhte zufoay usjdubuav, iqsabisv hecaazegup enlurg.
Asm omeay upbeqe ol ixbom lityuv aq o jelfilcaul faazh czivo ihepwux zusf feb “ba-exnuf” kbo ezrok isx covepz oxb kgiqo. Rugig otyiga wga gzoje jaqeeqv ewlsogqim ofguwr ih iveim.
We ucu ih ilifqupg Jazqita vehpeqcec ax ucnyz/emeeg loco, uypapm ank .vovuuf zgacizdv, fkikp ibrejef uq ok ux UctzpMulouhdo vcin wee pih esuhoto hizt o vov aruis…et huut.
Ri eqo u lawedm ergfs vahztieb ux ax ahsot Wixyibe-kibuh dajvyvit, wner ssu tocg ip i Badeyo wiybeqfez jzet ipexr a ceblfi zomua is yeiduqu.
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.
Nfa dgesolos kkaxqung rnorph xao’qa xuohuc am gqig hcebyiw aka yne yeqf qaneeygo kuozr zeo’ck sumlk pubfuwx, ivogwuqk xiu se raq ugkg ngowu koctuylisq qixa rox hi ja ub opqiwtoobecjv vipv.
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.