Concurrency: GCD and async/await
The hiker taps "Sync" and TrailLog freezes for four seconds while it uploads. Scrolling stops, buttons ignore taps. The upload itself is fine — it simply ran on the wrong thread.
The rule
Slow work off the main thread; UI updates back on it.
iOS runs your UI on the main thread. It handles touches, layout and drawing, roughly 60 to 120 times per second. If you block it with slow work — network, big files, heavy maths — the app freezes. And UIKit may only be touched from the main thread.
Grand Central Dispatch (GCD) is the older tool. You send closures to queues: the main queue, or global background queues.
DispatchQueue.global().async {
let data = self.buildBackup() // slow, off the main thread
DispatchQueue.main.async {
self.statusLabel.text = "Synced" // UI, back on the main thread
}
}async/await (Swift 5.5) is the modern tool. An async function can pause while it waits without blocking a thread; callers write await at each possible pause:
func uploadJournal() async throws -> Int {
let (data, _) = try await URLSession.shared.data(for: request)
return data.count
}
@MainActor
func syncTapped() {
Task {
statusLabel.text = "Syncing…"
let bytes = try await uploadJournal() // waits without freezing
statusLabel.text = "Synced \(bytes) bytes"
}
}Task { }starts async work from ordinary, non-async code such as a button handler.@MainActorguarantees code runs on the main thread, so UI updates are safe. View controllers in UIKit are main-actor types.- The code reads top to bottom, like normal code, instead of nesting closures inside closures.
The shortcut
For each line, ask: "Could this take noticeable time?" If yes, it goes off the main thread (await an async API or dispatch it). Then ask: "Does this touch the UI?" If yes, it must run on the main thread.
Button tapped
Runs on the main thread. Start a
Taskso async work can begin.
The trap in GCD code: updating a label inside DispatchQueue.global().async without hopping back to DispatchQueue.main. It may seem to work, then glitch or crash at random.
Drills
Answer fast — trust the rule.
Why does a slow network call on the main thread freeze the app?
In GCD, where must label.text = "Done" be dispatched?
What starts async work from a normal, non-async button handler?
What does @MainActor guarantee?
Remember
- Slow work off the main thread; UI updates back on it.
- GCD:
DispatchQueue.global().async, thenDispatchQueue.main.asyncfor UI. Modern:async/await,Task,@MainActor. - Trap: touching UIKit from a background queue works until it randomly does not.
