Skip to content
All essays
iOSMarch 27, 202514 min

Swift Concurrency: async/await and Actor

Modern Swift concurrency with async/await, actors, and structured concurrency.

Ü
Ümit Uz
Mobile & Full Stack Developer

Swift 5.5 introduced the modern concurrency model that makes asynchronous programming easier. Write readable code with async/await syntax.

async/await

Simple async function

swift
// Define async function
func fetchUserData() async throws -> User {
    // Async operation
    let user = try await URLSession.shared.data(from: url)
    return User(from: user)
}

// Call async function
Task {
    do {
        let user = try await fetchUserData()
        print("User: \(user.name)")
    } catch {
        print("Error: \(error)")
    }
}

SwiftUI with async/await

swift
struct ProfileView: View {
    @State private var user: User?
    @State private var isLoading = false

    var body: some View {
        VStack {
            if isLoading {
                ProgressView("Loading...")
            } else if let user = user {
                Text(user.name)
                    .font(.title)
            }
        }
        .task {
            await loadUser()
        }
    }

    func loadUser() async {
        isLoading = true
        defer { isLoading = false }

        do {
            user = try await fetchUserData()
        } catch {
            print("Error: \(error)")
        }
    }
}

Structured Concurrency

Task Group

swift
func fetchMultipleImages() async throws -> [Image] {
    try await withThrowingTaskGroup(of: Image.self) { group in
        // Create parallel tasks
        for id in 1...5 {
            group.addTask {
                try await fetchImage(id: id)
            }
        }

        // Collect results
        var images: [Image] = []
        for try await image in group {
            images.append(image)
        }
        return images
    }
}

Task Cancellation

swift
class ImageLoader: ObservableObject {
    @Published var images: [Image] = []
    private var loadTask: Task<Void, Never>?

    func loadImages() {
        // Cancel previous task
        loadTask?.cancel()

        // Start new task
        loadTask = Task {
            do {
                let images = try await fetchMultipleImages()

                // Check if task was cancelled
                if !Task.isCancelled {
                    self.images = images
                }
            } catch {
                if !Task.isCancelled {
                    print("Error: \(error)")
                }
            }
        }
    }

    func cancelLoading() {
        loadTask?.cancel()
    }
}

MainActor

swift
@MainActor
class ViewModel: ObservableObject {
    @Published var data: String = ""

    func fetchData() async {
        // This function runs on main thread
        let result = await fetchFromAPI()
        self.data = result // UI update is safe
    }
}

// Or

class ViewModel: ObservableObject {
    @Published var data: String = ""

    func fetchData() async {
        let result = await fetchFromAPI()

        // Run on main actor
        await MainActor.run {
            self.data = result
        }
    }
}

Actor

Preventing Data Races

swift
actor Counter {
    private var value = 0

    func increment() -> Int {
        value += 1
        return value
    }

    func get() -> Int {
        return value
    }
}

let counter = Counter()

Task {
    // Each call ensures actor serialization
    print(await counter.increment())
    print(await counter.get())
}

Actor with Cache

swift
actor ImageCache {
    private var storage: [String: Image] = [:]

    func getImage(for key: String) -> Image? {
        return storage[key]
    }

    func setImage(_ image: Image, for key: String) {
        storage[key] = image
    }

    func clear() {
        storage.removeAll()
    }
}

// Usage
let cache = ImageCache()

Task {
    // Thread-safe cache operations
    await cache.setImage(image, for: "profile")
    let cached = await cache.getImage(for: "profile")
}

AsyncStream

swift
// Create AsyncStream
func generateNumbers() -> AsyncStream<Int> {
    return AsyncStream { continuation in
        var i = 0
        let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
            i += 1
            continuation.yield(i)

            if i >= 10 {
                continuation.finish()
                timer.invalidate()
            }
        }
    }
}

// Use AsyncStream
Task {
    for await number in generateNumbers() {
        print("Number: \(number)")
    }
}

Continuation

swift
// Convert callback-based API to async/await
func fetchUser(id: String) async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        oldAPI.fetchUser(id) { result in
            switch result {
            case .success(let user):
                continuation.resume(returning: user)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

Sendable Protocol

swift
// Sendable types for safe sharing in concurrency
struct User: Sendable {
    let id: String
    let name: String
}

// Actor is automatically Sendable
actor UserRepository {
    private var users: [String: User] = [:]

    func add(_ user: User) {
        users[user.id] = user
    }
}

Best Practices

  1. 1Structured concurrency: Use Task and TaskGroup
  2. 2Cancellation: Check for cancellation in long operations
  3. 3MainActor: Use @MainActor for UI operations
  4. 4Actor: Use actor for shared mutable state
  5. 5Sendable: Use Sendable types at concurrency boundaries

Modern Swift concurrency makes asynchronous programming safer and more readable!

Related essays

Next essay
Swift Testing: XCTest and Swift Testing Framework