-
Notifications
You must be signed in to change notification settings - Fork 0
22 — How Everything Fits Together
Table 7 orders a full meal — an appetizer, a main, and a dessert — and the kitchen has to get all three to the table at the right moment, not whenever each station happens to finish.
The appetizer and the main don't cook themselves. They go out to different prep stations, and different chefs pick them up and work on them at the same time — the cold station plates the appetizer while the grill chef works the main. Nobody sits idle waiting for one dish before starting the next. If the table ordered two appetizers for two guests, the pass has to hold off doing anything until both plates are up — that's a job for whoever is counting outstanding dishes, not for guesswork. Once a dish is finished, it doesn't just sit loose on a random shelf where two chefs might grab for it at once and collide — it gets logged into the shared ticket book at the pass, and that log only ever gets written to by one hand at a time, so nobody reads a half-written entry. Only once everything is accounted for does the head chef — and only the head chef — carry the tray out to the table.
None of this is a new trick. It's the same order line, the same chefs, the same pass, and the same head chef's counter this book has been describing all along. What's new in this chapter is nothing — it's just all of it, running together, on one table's order.
👉 A real order is never handled by one station acting alone — it's several stations, each doing their own job, coordinated so the finished plates land on the table together, at the right time, through the one counter that's allowed to serve it.
Every chapter up to this point isolated one GCD tool at a time. A real feature almost never uses just one. Take a common one: download an image, resize it off the main thread, cache the result, and then display it.
-
DispatchQueue.global(qos:)(Chapter 5) does the download and the resize — the prep stations. This is expensive work, so it has no business running on the main thread. -
DispatchGroup(Chapter 10) comes in if the feature needs several images before moving on — say, a screen that only appears once every thumbnail has loaded. Each downloadenter()s the group,leave()s when its image is ready, andnotifyfires once every image is accounted for, exactly like the waiter refusing to call the head chef over until every dish for the table is up. -
A
.barrierwrite into a shared cache (Chapter 12) protects the in-memory dictionary that holds the finished images. Multiple downloads can finish and want to write to that cache at around the same time; the barrier makes sure each write happens alone, so no read ever sees a half-written entry. -
DispatchQueue.main.async(Chapter 5) is the last hop — the only queue allowed to touch the UI, exactly like the only counter the head chef is allowed to serve from.
Strung together, the order is: kick work off on a background queue, optionally wait for a group of them to finish, write the result into shared state safely behind a barrier, then hop back to main to show it. Nothing here is a new concept — it's four tools this book already covered individually, composed into one pipeline because that's what a real feature actually looks like.
download ──▶ resize ──▶ cache (barrier write) ──▶ main queue ──▶ display
🧑🍳 🧑🍳 🔒 pass ledger 🧾 🍽️
(global queue) (global queue) (exclusive write) (head chef) (UI)
[ optional: DispatchGroup wraps N of these
when several images must all finish first ]
Work starts on a background chef, gets funneled through one exclusive write into the shared cache so no two chefs corrupt it, and only the head chef ever crosses into the customer-facing part of the app.
let cacheQueue = DispatchQueue(label: "image-cache", attributes: .concurrent)
var cache: [URL: UIImage] = [:]
func loadImage(url: URL, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let data = try? Data(contentsOf: url)
let image = data.flatMap(UIImage.init)
cacheQueue.async(flags: .barrier) {
cache[url] = image
}
DispatchQueue.main.async {
completion(image)
}
}
}
// Waiting on several before proceeding:
let group = DispatchGroup()
for url in thumbnailURLs {
group.enter()
loadImage(url: url) { _ in group.leave() }
}
group.notify(queue: .main) {
print("Every thumbnail is loaded and cached")
}👉 The download and resize happen off the main queue. The cache write is exclusive, so a concurrent read never sees a torn value. The completion always lands back on main. And when several images matter together, a DispatchGroup wraps the whole batch — four chapters, one function.
The same feature, written with async/await under an actor-backed cache, collapses the whole pipeline into a handful of straight-line await calls:
actor ImageCache {
private var storage: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? { storage[url] }
func store(_ image: UIImage?, for url: URL) { storage[url] = image }
}
let cache = ImageCache()
func loadImage(url: URL) async -> UIImage? {
let data = try? Data(contentsOf: url)
let image = data.flatMap(UIImage.init)
await cache.store(image, for: url)
return image
}
func loadThumbnails(urls: [URL]) async {
await withTaskGroup(of: Void.self) { group in
for url in urls {
group.addTask { _ = await loadImage(url: url) }
}
}
print("Every thumbnail is loaded and cached")
}Count the machinery that disappears: no DispatchQueue.global(qos:) to remember, no manual enter()/leave() pairing to balance, no .barrier flag to place correctly, no explicit hop back to .main — loadThumbnails isn't even marked @MainActor because nothing in it touches UI directly. The actor's isolation replaces the barrier outright; TaskGroup replaces the DispatchGroup counter; await replaces the completion-handler hop. Same four responsibilities as the GCD version — background work, waiting for a batch, safe shared-state writes, returning to the caller — just expressed with roughly a third of the lines and no nested closures.
👉 The GCD version needed four separate tools, each carrying its own bookkeeping; the async/await version needed one actor and one TaskGroup, with the compiler enforcing what GCD only asked the programmer to remember.
A real feature is never one GCD tool — it's several, handed off in sequence.
None beyond what earlier chapters already noted. This chapter doesn't introduce a new mechanism — it's the same order line, chefs, pass, and head chef's counter from every prior chapter, now shown working together on one order instead of in isolation. Whatever gap exists between the restaurant and the real GCD API was already called out where each tool was introduced.
In the loadImage example above, which GCD tool is the one actually preventing a data race on the shared cache dictionary — and what would go wrong if it were removed?