Day 23 – Swift Concurrency & Async/Await Explained for iOS Developers

Concurrency has always been one of the most challenging aspects of iOS development. Before Swift 5.5, developers had to rely on GCD (Grand Central Dispatch) and completion handlers, which often led to callback hell, race conditions, and hard-to-read code. Apple introduced the Swift Concurrency model with async/await, Task, and Actors, making concurrency safer, more expressive, and more maintainable.

At CuriosityTech.in, we emphasize concurrency as a mandatory skill because modern iOS apps depend on background work: networking, database queries, image loading, Bluetooth, or HealthKit. Without concurrency mastery, apps freeze, drain battery, or crash under load.


Why Concurrency Matters in iOS

  • iPhones are multitasking devices — users expect apps to remain smooth, responsive, and non-blocking.
  • Networking, database queries, or image processing must happen off the main thread.
  • Poorly handled concurrency leads to:
    • UI freezes (bad UX)
    • Race conditions (unpredictable bugs)
    • Crashes

Swift Concurrency Model – Core Building Blocks

1) async and await

  • async: Marks a function that performs asynchronous work.
  • await: Suspends execution until an async function completes.

Example (Networking):

func fetchUserProfile() async throws -> User {

    let url = URL(string: “https://curiositytech.in/user”)!

    let (data, _) = try await URLSession.shared.data(from: url)

    return try JSONDecoder().decode(User.self, from: data)

}

Usage:

Task {

    do {

        let profile = try await fetchUserProfile()

        print(“User: \(profile.name)”)

    } catch {

        print(“Error: \(error)”)

    }

}

✅ This eliminates nested closures and makes code linear, readable, and safe.


2) Structured Concurrency

Structured concurrency ensures tasks are managed in a hierarchy. If a parent task is canceled, child tasks also cancel.

Example:

func fetchDashboardData() async throws -> (User, [Workout]) {

    async let user = fetchUserProfile()

    async let workouts = fetchWorkouts()

    return try await (user, workouts)

}

  • Multiple async tasks run in parallel.
  • async let ensures they complete before returning.

3) Tasks and Task Groups

  • Task: Creates a concurrent unit of work.
  • TaskGroup: Runs multiple child tasks concurrently and aggregates results.

Example (Parallel Downloads):

func downloadImages(urls: [URL]) async throws -> [UIImage] {

    return try await withThrowingTaskGroup(of: UIImage.self) { group in

        for url in urls {

            group.addTask {

                let (data, _) = try await URLSession.shared.data(from: url)

                return UIImage(data: data)!

            }

        }

        var images = [UIImage]()

        for try await image in group {

            images.append(image)

        }

        return images

    }

}


4) Actors

Actors are reference types that isolate mutable state. They prevent data races when multiple tasks access shared data.

Example:

actor WorkoutStore {

    private var workouts: [Workout] = []

    func add(_ workout: Workout) {

        workouts.append(workout)

    }

    func getAll() -> [Workout] {

        workouts

    }

}

Usage:

let store = WorkoutStore()

await store.add(Workout(type: “Run”, duration: 30))

let data = await store.getAll()

✅ Guarantees thread safety without locks.


5) MainActor

UI updates must always happen on the main thread. Swift Concurrency provides @MainActor to ensure this.

@MainActor

class DashboardViewModel: ObservableObject {

    @Published var user: User?

    func loadData() async {

        user = try? await fetchUserProfile()

    }

}

This prevents the common beginner mistake: updating UI from a background thread.


Migration from GCD / Closures → Swift Concurrency

Old ApproachProblemNew Approach
GCD DispatchQueue.global().asyncHard to manage, nested closuresTask { … }
Completion handlersCallback hellasync/await
Locks & SemaphoresRisk of deadlocksActors
Manual thread switchingBoilerplate@MainActor

Error Handling with async/await

Swift Concurrency integrates seamlessly with do-catch and throws:

do {

    let result = try await fetchUserProfile()

    print(result)

} catch NetworkError.timeout {

    print(“Request timed out”)

} catch {

    print(“Unknown error”)

}


Cancellation in Concurrency

Every task is cancellable.

let task = Task {

    try await fetchUserProfile()

}

task.cancel()

Inside async functions, check for cancellation:

try Task.checkCancellation()

Best Practice: Always support cancellation in long-running tasks (e.g., file uploads).


Debugging Concurrency

Tools:

  1. Xcode Debugger – Shows async call stacks.
  2. Instruments (Concurrency Template) – Detects thread issues, deadlocks.
  3. Logging – Use os_log for tracing task flow.

Mandatory Best Practices

  1. Never block the main thread → Use async functions for heavy tasks.
  2. Mark UI updates with @MainActor.
  3. Design for cancellation → Don’t ignore task cancellations.
  4. Use structured concurrency → Avoid detached tasks unless necessary.
  5. Actors for shared mutable state → Don’t use global variables.
  6. Mix with Combine only when needed → Prefer Swift Concurrency.

Example – Fitness Tracker Dashboard with Async/Await

@MainActor

class DashboardViewModel: ObservableObject {

    @Published var steps: Int = 0

    @Published var calories: Double = 0.0

    func loadData() async {

        async let stepData = HealthKitService.shared.fetchSteps()

        async let calorieData = HealthKitService.shared.fetchCalories()

        steps = try await stepData

        calories = try await calorieData

    }

}

When user opens the dashboard:

  • Steps and calories fetch in parallel.
  • UI updates happen automatically on the main thread.
  • If the user closes the app, tasks cancel safely.

Common Beginner Mistakes with Concurrency

  • Using Task.detached unnecessarily → loses structured hierarchy.
  • Ignoring await → async functions won’t execute properly.
  • Blocking with sleep instead of Task.sleep.
  • Forgetting @MainActor → UI crashes.
  • Mixing GCD + async/await → messy code.

Expertise Roadmap


Conclusion

Swift Concurrency with async/await revolutionizes iOS development. It simplifies code, prevents race conditions, ensures safe UI updates, and enables scalable apps. Mastering it is mandatory for modern iOS developers. By applying structured concurrency, actors, and cancellation handling, you can create apps that are fast, safe, and responsive.

Leave a Comment

Your email address will not be published. Required fields are marked *