Why Your App Freezes – And How Swift Concurrency Solves It.


A Quick Story from My First iOS Project
Ever tapped a button and waited... and waited... until your app finally responded? That’s what happened when I was building my first SwiftUI app.
A few months back, I was building a SwiftUI app. Pretty UI, smooth animations, everything was perfect… until I hit run.
I tapped a button that triggered some data fetching, and suddenly — the screen froze. No animation. No response. I thought the app crashed. But after a couple of seconds, it came back.
Turns out, I had accidentally blocked the main thread by doing heavy work directly inside the UI code.
That’s when I learned: Concurrency isn’t optional in real-world apps — it’s essential.
🧠 What Is Concurrency?
Imagine you're cooking. You're waiting for water to boil (slow), but instead of just standing there, you chop vegetables in the meantime.
That’s concurrency — doing multiple tasks at the same time without blocking progress.
In programming, it means:
You can load data in the background,
While keeping the UI smooth,
Without freezing or delaying user actions.
⚠️ The Problem Without Concurrency
In iOS apps, if you do something slow — like:
Fetching data from the internet,
Reading from disk,
Processing images…
…on the main thread, your app stops responding. That’s because the main thread also handles user interactions and UI updates.
If it’s busy, the app becomes laggy or even crashes with “Application not responding”.
Old Way: GCD (Grand Central Dispatch)
Before Swift Concurrency, we used GCD like this:
DispatchQueue.global().async {
let result = fetchData()
DispatchQueue.main.async {
updateUI(with: result)
}
}
It worked, but it was messy and hard to follow — especially with nested calls. We called this "callback hell."
This code might look okay at first, but it gets messy fast—especially with multiple nested async calls. That’s why we called it "callback hell."
But here's something really important to understand:
We had to write DispatchQueue.main.async
because UIKit (and SwiftUI) requires all UI updates to happen on the main thread.
So even though fetchData()
runs in the background (on a global queue), once the data is fetched, we must return to the main thread to safely update the UI.
If you forget this part and try to update the UI directly from a background thread, your app might crash or behave unexpectedly.
🚀 New Way: Swift Concurrency
Swift 5.5 introduced a modern, clean, and readable way to write concurrent code using:
async
andawait
Task { }
Actors
and@MainActor
Here’s the same code, in Swift Concurrency:
Task {
let result = await fetchData()
updateUI(with: result)
}
Looks more like normal code, right? But under the hood, it's doing everything GCD did — only safer and easier.
📱 Real Example (Simulating a Network Call)
func fetchUserData() async -> String {
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
return "👩💻 User Data Loaded!"
}
Task {
let userData = await fetchUserData()
print(userData)
}
Task {}
launches the work in parallel.await
tells Swift to suspend the task until the result is ready, without blocking the main thread.
🛣 What’s Next?
In the next part, we’ll dive deep into async
and await
, and build real examples using a SwiftUI view.
You'll learn:
When to use
async
functions,How to create your own,
And how to handle errors in an async world.
Subscribe to my newsletter
Read articles from Tabassum Akter Nusrat directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Tabassum Akter Nusrat
Tabassum Akter Nusrat
Former Android dev at Samsung R&D, now diving deep into iOS development. Writing daily about Swift Concurrency, SwiftUI, and the journey of building real-world apps. Passionate about clean architecture, mental health tech, and learning in public.