Skip to content

jeffersonsetiawan/AsyncAwait

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🍳 Cooking with Async/Await

A comprehensive Swift async/await learning app with interactive cooking examples.

📱 About This App

This app teaches Swift's async/await concurrency through cooking metaphors and interactive examples. Perfect for developers who want to learn modern Swift concurrency in a fun, hands-on way!

🎯 What You'll Learn

  • Lesson 0: The Journey - From callback hell to async paradise
  • Lesson 1: Basic Timer - Simple async/await with cooking timers
  • Lesson 2: Concurrent Tasks - Multiple cooking tasks simultaneously
  • Lesson 3: Sequential Steps - Chaining cooking operations
  • Lesson 4: Error Handling - Dealing with cooking mistakes
  • Lesson 5: Actors - Thread-safe kitchen management
  • Lesson 6: TaskGroup - Managing multiple dishes

📚 Swift Async/Await Cheat Sheet

🔤 Basic Syntax

Async Function

func fetchData() async -> String {
    // Async work here
    return "Data"
}

Throwing Async Function

func fetchData() async throws -> String {
    // Can throw errors
    return "Data"
}

Calling Async Functions

let result = await fetchData()
let result = try await fetchData()

⚡ Task Management

Creating Tasks

Task {
    await doSomething()
}

Task {
    try await doSomethingThrowing()
}

Task with Priority

Task(priority: .high) {
    await importantWork()
}

Detached Task

Task.detached {
    await backgroundWork()
}

Task Cancellation

let task = Task {
    await longRunningWork()
}
task.cancel()

// Check for cancellation
if Task.isCancelled {
    return
}

🔄 Concurrent Execution

Async Let

async let data1 = fetchData1()
async let data2 = fetchData2()

let results = await (data1, data2)

TaskGroup

await withTaskGroup(of: String.self) { group in
    group.addTask { await fetchData1() }
    group.addTask { await fetchData2() }
    
    for await result in group {
        print(result)
    }
}

Throwing TaskGroup

try await withThrowingTaskGroup(of: String.self) { group in
    group.addTask { try await fetchData1() }
    group.addTask { try await fetchData2() }
    
    for try await result in group {
        print(result)
    }
}

🎭 MainActor & UI Updates

MainActor Function

@MainActor
func updateUI() {
    // Always runs on main thread
    label.text = "Updated"
}

✅ Recommended: @MainActor Class

@MainActor
class ViewModel: ObservableObject {
    @Published var data = ""
    @Published var isLoading = false
    
    func loadData() async {
        isLoading = true
        // This automatically runs on main thread
        // No need for MainActor.run!
        
        let result = await fetchDataFromBackground()
        data = result // Safe UI update
        isLoading = false
    }
}

⚠️ Less Preferred: MainActor.run

// Only use when you can't make the whole function @MainActor
func backgroundTask() async {
    let result = await heavyComputation()
    
    // Switch to main thread for UI update
    await MainActor.run {
        self.isLoading = false
        self.data = result
    }
}

🎯 When to Use Each Approach

Scenario Use @MainActor Use MainActor.run
UI Classes (ViewModels, Views) ✅ Always ❌ Never needed
Mixed UI/Background work ✅ Preferred ⚠️ Sometimes
Pure background functions ❌ No ✅ For UI updates
SwiftUI Views ✅ Automatic ❌ Never needed

📱 Real-World Examples

✅ Best Practice: @MainActor ViewModel

@MainActor
class PhotoGalleryViewModel: ObservableObject {
    @Published var photos: [Photo] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    // Everything here runs on main thread automatically
    func loadPhotos() async {
        isLoading = true
        errorMessage = nil
        
        do {
            // This background work happens automatically
            photos = try await PhotoService.fetchPhotos()
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
        // No MainActor.run needed! 🎉
    }
    
    func refreshPhotos() async {
        await loadPhotos() // Still on main thread
    }
}

⚠️ When MainActor.run is Needed

// Background service that occasionally needs UI updates
class BackgroundSyncService {
    weak var delegate: SyncDelegate?
    
    func performSync() async {
        // Heavy background work
        let results = await processLargeDataset()
        
        // Need to update UI delegate on main thread
        await MainActor.run {
            delegate?.syncCompleted(results)
        }
    }
}

// Or when bridging with non-async code
func legacyCallback() {
    Task {
        let data = await fetchData()
        
        await MainActor.run {
            // Update legacy UI components
            self.tableView.reloadData()
        }
    }
}

🚨 Error Handling

Basic Error Handling

do {
    let result = try await fetchData()
    print(result)
} catch {
    print("Error: \(error)")
}

Specific Error Types

do {
    let result = try await fetchData()
} catch NetworkError.noConnection {
    showOfflineMessage()
} catch NetworkError.timeout {
    showTimeoutMessage()
} catch {
    showGenericError(error)
}

Optional Try

let result = try? await fetchData()
// result is nil if error occurs

🎪 Actors

Basic Actor

actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }
    
    func getValue() -> Int {
        return value
    }
}

Using Actors

let counter = Counter()
await counter.increment()
let value = await counter.getValue()

Actor Isolation

actor DataManager {
    private var data: [String] = []
    
    nonisolated func getCount() -> Int {
        // Can be called without await
        return data.count
    }
}

🎯 Common Patterns

Network Request

func fetchUser(id: String) async throws -> User {
    let url = URL(string: "api/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

SwiftUI Integration

struct ContentView: View {
    @State private var data = ""
    
    var body: some View {
        Text(data)
            .task {
                data = await fetchData()
            }
    }
}

Async Sequence

for await line in URL.lines {
    print(line)
}

// Custom async sequence
for await value in asyncSequence {
    process(value)
}

✅ Best Practices

DO: Use structured concurrency

// ✅ Good
async let data1 = fetch1()
async let data2 = fetch2()
await (data1, data2)

// ❌ Avoid
Task.detached { await fetch1() }
Task.detached { await fetch2() }

DO: Handle cancellation

func longTask() async {
    for i in 0..<1000 {
        if Task.isCancelled {
            return
        }
        await doWork(i)
    }
}

DO: Use MainActor for UI

@MainActor
class ViewModel: ObservableObject {
    @Published var isLoading = false
    
    func loadData() async {
        isLoading = true
        defer { isLoading = false }
        // Load data...
    }
}

❌ Common Mistakes

DON'T: Block main thread

// ❌ Bad - blocks main thread
DispatchQueue.main.sync {
    await fetchData()
}

// ✅ Good
await MainActor.run {
    updateUI()
}

DON'T: Forget error handling

// ❌ Bad - unhandled errors
let data = try await fetchData()

// ✅ Good
do {
    let data = try await fetchData()
} catch {
    handleError(error)
}

DON'T: Overuse Task.detached

// ❌ Bad - loses context
Task.detached {
    await doWork()
}

// ✅ Good - inherits context
Task {
    await doWork()
}

🔄 Migration Guide: Converting to Async/Await

Converting Closure-Based APIs

Basic Completion Handler

// ❌ Old: Closure-based
func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
        } else if let data = data {
            let result = String(data: data, encoding: .utf8) ?? ""
            completion(.success(result))
        }
    }.resume()
}

// ✅ New: Async/await
func fetchData() async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? ""
}

Multiple Completion Handlers

// ❌ Old: Nested callbacks
func loadUserProfile(completion: @escaping (UserProfile?, Error?) -> Void) {
    fetchBasicInfo { basicInfo, error in
        guard let basicInfo = basicInfo, error == nil else {
            completion(nil, error)
            return
        }
        
        fetchPreferences { preferences, error in
            guard let preferences = preferences, error == nil else {
                completion(nil, error)
                return
            }
            
            fetchFriends { friends, error in
                guard let friends = friends, error == nil else {
                    completion(nil, error)
                    return
                }
                
                let profile = UserProfile(basic: basicInfo, preferences: preferences, friends: friends)
                completion(profile, nil)
            }
        }
    }
}

// ✅ New: Clean async/await
func loadUserProfile() async throws -> UserProfile {
    async let basicInfo = fetchBasicInfo()
    async let preferences = fetchPreferences()
    async let friends = fetchFriends()
    
    return try await UserProfile(
        basic: basicInfo,
        preferences: preferences,
        friends: friends
    )
}

Converting with withCheckedContinuation

// ❌ Old: Legacy callback API
func legacyNetworkCall(completion: @escaping (Data?, Error?) -> Void) {
    // Some old networking code
}

// ✅ New: Wrapped in async/await
func modernNetworkCall() async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        legacyNetworkCall { data, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let data = data {
                continuation.resume(returning: data)
            } else {
                continuation.resume(throwing: NetworkError.noData)
            }
        }
    }
}

Converting Combine Publishers

Basic Publisher

// ❌ Old: Combine Publisher
func fetchDataPublisher() -> AnyPublisher<String, Error> {
    URLSession.shared.dataTaskPublisher(for: url)
        .map { String(data: $0.data, encoding: .utf8) ?? "" }
        .eraseToAnyPublisher()
}

// Usage with Combine
fetchDataPublisher()
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("Error: \(error)")
            }
        },
        receiveValue: { data in
            print("Data: \(data)")
        }
    )
    .store(in: &cancellables)

// ✅ New: Async/await
func fetchData() async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? ""
}

// Usage with async/await
do {
    let data = try await fetchData()
    print("Data: \(data)")
} catch {
    print("Error: \(error)")
}

Complex Publisher Chain

// ❌ Old: Complex Combine chain
func loadUserDataPublisher() -> AnyPublisher<UserData, Error> {
    fetchUserIdPublisher()
        .flatMap { userId in
            fetchUserProfilePublisher(userId: userId)
        }
        .flatMap { profile in
            fetchUserPostsPublisher(userId: profile.id)
                .map { posts in
                    UserData(profile: profile, posts: posts)
                }
        }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
}

// ✅ New: Sequential async/await
func loadUserData() async throws -> UserData {
    let userId = try await fetchUserId()
    let profile = try await fetchUserProfile(userId: userId)
    let posts = try await fetchUserPosts(userId: profile.id)
    
    return UserData(profile: profile, posts: posts)
}

// Or concurrent if operations are independent:
func loadUserDataConcurrent() async throws -> UserData {
    let userId = try await fetchUserId()
    
    async let profile = fetchUserProfile(userId: userId)
    async let posts = fetchUserPosts(userId: userId)
    
    return try await UserData(profile: profile, posts: posts)
}

Publisher with Side Effects

// ❌ Old: Combine with side effects
func loadDataWithUpdatesPublisher() -> AnyPublisher<String, Error> {
    fetchDataPublisher()
        .handleEvents(
            receiveSubscription: { _ in
                DispatchQueue.main.async {
                    self.isLoading = true
                }
            },
            receiveOutput: { _ in
                DispatchQueue.main.async {
                    self.lastUpdated = Date()
                }
            },
            receiveCompletion: { _ in
                DispatchQueue.main.async {
                    self.isLoading = false
                }
            }
        )
        .eraseToAnyPublisher()
}

// ✅ New: Async/await with explicit updates
@MainActor
func loadDataWithUpdates() async throws -> String {
    isLoading = true
    defer { isLoading = false }
    
    let data = try await fetchData()
    lastUpdated = Date()
    
    return data
}

Converting SwiftUI Patterns

Combine in SwiftUI

// ❌ Old: Combine with SwiftUI
struct ContentView: View {
    @State private var data = ""
    @State private var cancellables = Set<AnyCancellable>()
    
    var body: some View {
        Text(data)
            .onAppear {
                fetchDataPublisher()
                    .receive(on: DispatchQueue.main)
                    .sink(
                        receiveCompletion: { _ in },
                        receiveValue: { newData in
                            self.data = newData
                        }
                    )
                    .store(in: &cancellables)
            }
    }
}

// ✅ New: Async/await with SwiftUI
struct ContentView: View {
    @State private var data = ""
    
    var body: some View {
        Text(data)
            .task {
                do {
                    data = try await fetchData()
                } catch {
                    print("Error: \(error)")
                }
            }
    }
}

ObservableObject Migration

// ❌ Old: Combine-based ViewModel
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    private var cancellables = Set<AnyCancellable>()
    
    func loadUser() {
        isLoading = true
        
        fetchUserPublisher()
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] _ in
                    self?.isLoading = false
                },
                receiveValue: { [weak self] user in
                    self?.user = user
                }
            )
            .store(in: &cancellables)
    }
}

// ✅ New: Async/await ViewModel
@MainActor
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    
    func loadUser() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            user = try await fetchUser()
        } catch {
            print("Error loading user: \(error)")
        }
    }
}

Migration Checklist

✅ When Converting Closures:

  • Replace completion handlers with async throws
  • Use withCheckedContinuation for legacy APIs
  • Remove manual thread switching
  • Simplify error handling with do-catch
  • Use structured concurrency for multiple operations

✅ When Converting Combine:

  • Replace Publishers with async functions
  • Convert .flatMap chains to sequential await
  • Use async let for concurrent operations
  • Move side effects to explicit locations
  • Use @MainActor instead of .receive(on: DispatchQueue.main)
  • Replace .sink with do-catch blocks

✅ SwiftUI Specific:

  • Replace .onAppear + Publisher with .task
  • Use @MainActor on ViewModels
  • Remove AnyCancellable storage
  • Simplify state management

🚨 Common Conversion Issues

1. 🧵 Threading and MainActor Issues

Problem: UI Updates on Wrong Thread

// ❌ Wrong conversion: Missing @MainActor
func loadData() async -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    let result = processData(data)
    self.updateUI(result) // ⚠️ Might not be on main thread!
    return result
}

// ✅ Correct: Use @MainActor
@MainActor
func loadData() async -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    let result = processData(data)
    self.updateUI(result) // ✅ Guaranteed main thread
    return result
}

2. 🔄 Forgetting to Handle Cancellation

Problem: No Cancellation Support

// ❌ Wrong conversion: No cancellation checks
func longRunningTask() async throws -> String {
    for i in 0..<1000000 {
        doWork(i) // Heavy work without cancellation check
    }
    return "Done"
}

// ✅ Correct: Check for cancellation
func longRunningTask() async throws -> String {
    for i in 0..<1000000 {
        if Task.isCancelled {
            throw CancellationError()
        }
        doWork(i)
    }
    return "Done"
}

3. 🔗 Incorrect Error Handling Conversion

Problem: Lost Error Context

// ❌ Wrong conversion: Generic error handling
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
    // ⚠️ Lost specific error types and HTTP status handling!
}

// ✅ Correct: Preserve error context
func fetchUser() async throws -> User {
    do {
        let (data, response) = try await URLSession.shared.data(from: url)
        
        if let httpResponse = response as? HTTPURLResponse {
            if httpResponse.statusCode == 404 {
                throw NetworkError.userNotFound
            } else if httpResponse.statusCode >= 400 {
                throw NetworkError.serverError(httpResponse.statusCode)
            }
        }
        
        return try JSONDecoder().decode(User.self, from: data)
    } catch let urlError as URLError {
        throw NetworkError.networkFailure(urlError)
    }
}

4. 🔄 Improper Continuation Usage

Problem: Multiple Resume Calls

// ❌ Wrong: Can resume multiple times
func convertLegacyAPI() async throws -> String {
    return try await withCheckedThrowingContinuation { continuation in
        legacyAPI { result, error in
            if let error = error {
                continuation.resume(throwing: error)
            }
            if let result = result {
                continuation.resume(returning: result) // ⚠️ Might be called after error!
            }
        }
    }
}

// ✅ Correct: Single resume path
func convertLegacyAPI() async throws -> String {
    return try await withCheckedThrowingContinuation { continuation in
        legacyAPI { result, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let result = result {
                continuation.resume(returning: result)
            } else {
                continuation.resume(throwing: APIError.noData)
            }
        }
    }
}

5. 🔄 Sequential vs Concurrent Execution

Problem: Making Concurrent Work Sequential

// ❌ Wrong conversion: Made it sequential
func loadUserData() async throws -> UserData {
    let profile = try await fetchProfile()  // Wait for this...
    let posts = try await fetchPosts()      // Then this...
    let friends = try await fetchFriends()  // Then this...
    return UserData(profile: profile, posts: posts, friends: friends)
}

// ✅ Correct: Keep it concurrent
func loadUserData() async throws -> UserData {
    async let profile = fetchProfile()
    async let posts = fetchPosts()
    async let friends = fetchFriends()
    
    return try await UserData(
        profile: profile,
        posts: posts,
        friends: friends
    )
}

6. 🎭 Actor Isolation Violations

Problem: Accessing Actor Properties Incorrectly

actor DataManager {
    private var cache: [String: Data] = [:]
    
    func getData(key: String) -> Data? {
        return cache[key]
    }
}

// ❌ Wrong: Forgot await
func useDataManager() async {
    let manager = DataManager()
    let data = manager.getData(key: "test") // ⚠️ Compiler error!
}

// ✅ Correct: Use await
func useDataManager() async {
    let manager = DataManager()
    let data = await manager.getData(key: "test")
}

7. 🔄 Memory Management Issues

Problem: Retain Cycles with Tasks

class ViewModel {
    var data: String = ""
    
    // ❌ Wrong: Potential retain cycle
    func loadData() {
        Task {
            let result = await fetchData()
            self.data = result // Strong reference to self
        }
    }
    
    // ✅ Correct: Use weak self or @MainActor
    @MainActor
    func loadData() async {
        data = await fetchData() // No retain cycle with @MainActor
    }
}

8. 🔄 SwiftUI Integration Issues

Problem: Wrong SwiftUI Async Patterns

// ❌ Wrong: Using onAppear with Task
struct ContentView: View {
    @State private var data = ""
    
    var body: some View {
        Text(data)
            .onAppear {
                Task {
                    data = await loadData()
                }
            }
    }
}

// ✅ Correct: Use .task modifier
struct ContentView: View {
    @State private var data = ""
    
    var body: some View {
        Text(data)
            .task {
                data = await loadData()
            }
    }
}

9. 🔄 Testing and Mocking Issues

Problem: Hard to Test Async Code

// ❌ Wrong: Hard to test
func loadUserData() async throws -> UserData {
    let data = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(UserData.self, from: data.0)
}

// ✅ Correct: Dependency injection for testing
protocol NetworkService {
    func data(from url: URL) async throws -> (Data, URLResponse)
}

func loadUserData(networkService: NetworkService = URLSession.shared) async throws -> UserData {
    let (data, _) = try await networkService.data(from: url)
    return try JSONDecoder().decode(UserData.self, from: data)
}

✅ Conversion Checklist

  • Threading: Add @MainActor where needed
  • Cancellation: Check Task.isCancelled in long operations
  • Error Handling: Preserve specific error types
  • Concurrency: Use async let for parallel work
  • Continuations: Ensure single resume call
  • Memory: Avoid retain cycles with [weak self] or @MainActor
  • SwiftUI: Use .task instead of .onAppear
  • Testing: Make dependencies injectable
  • Actor Isolation: Add await for actor calls
  • Performance: Consider Task.detached for heavy CPU work

🔴 Common Compile Errors During Migration

1. "Expression is 'async' but is not marked with 'await'"

Error Message:

Expression is 'async' but is not marked with 'await'

Real-World Example:

// Common scenario: Converting URLSession callback to async
class NetworkManager {
    // ❌ Error: Missing await
    func fetchUser(id: String) async throws -> User {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        let (data, _) = URLSession.shared.data(from: url) // Error here!
        return try JSONDecoder().decode(User.self, from: data)
    }
    
    // ✅ Fix: Add await
    func fetchUser(id: String) async throws -> User {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(User.self, from: data)
    }
}

2. "'await' in a function that does not support concurrency"

Error Message:

'await' in a function that does not support concurrency

Real-World Example:

// Common scenario: SwiftUI ViewModel loading data
@MainActor
class UserProfileViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    
    // ❌ Error: viewDidLoad is not async
    func viewDidLoad() {
        isLoading = true
        user = await NetworkManager.shared.fetchUser(id: "123") // Error here!
        isLoading = false
    }
    
    // ✅ Fix 1: Make function async
    func viewDidLoad() async {
        isLoading = true
        user = try? await NetworkManager.shared.fetchUser(id: "123")
        isLoading = false
    }
    
    // ✅ Fix 2: Use Task wrapper
    func viewDidLoad() {
        Task {
            isLoading = true
            user = try? await NetworkManager.shared.fetchUser(id: "123")
            isLoading = false
        }
    }
}

// Usage in SwiftUI:
struct UserProfileView: View {
    @StateObject private var viewModel = UserProfileViewModel()
    
    var body: some View {
        VStack {
            if let user = viewModel.user {
                Text(user.name)
            }
        }
        .task {
            await viewModel.viewDidLoad() // Use .task for async functions
        }
    }
}

3. "Cannot pass function of type '() async -> Void' to parameter expecting synchronous function"

Error Message:

Cannot pass function of type '() async -> Void' to parameter expecting synchronous function type

Real-World Example:

// Common scenario: SwiftUI Button actions and UIKit target-action
class DataViewController: UIViewController {
    @Published var data: [Item] = []
    
    // Async function to load data
    func loadData() async {
        data = try? await APIService.fetchItems()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ❌ Error: Cannot pass async function to UIButton action
        let refreshButton = UIButton()
        refreshButton.addTarget(self, action: #selector(loadData), for: .touchUpInside)
        // Error: loadData is async, but @objc methods must be sync
    }
    
    // ✅ Fix: Create sync wrapper
    @objc func refreshButtonTapped() {
        Task {
            await loadData()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let refreshButton = UIButton()
        refreshButton.addTarget(self, action: #selector(refreshButtonTapped), for: .touchUpInside)
    }
}

// SwiftUI Example:
struct ContentView: View {
    @State private var items: [Item] = []
    
    func loadItems() async {
        items = try? await APIService.fetchItems()
    }
    
    var body: some View {
        VStack {
            List(items) { item in
                Text(item.name)
            }
            
            // ❌ Error: Cannot pass async function directly
            // Button("Refresh", action: loadItems)
            
            // ✅ Fix: Use closure with Task
            Button("Refresh") {
                Task {
                    await loadItems()
                }
            }
            
            // ✅ Alternative: Use .refreshable modifier
            List(items) { item in
                Text(item.name)
            }
            .refreshable {
                await loadItems()
            }
        }
    }
}

4. "Actor-isolated property 'data' can not be referenced from a non-isolated context"

Error Message:

Actor-isolated property 'data' can not be referenced from a non-isolated context

Real-World Example:

// Common scenario: Thread-safe cache implementation
actor ImageCache {
    private var cache: [String: UIImage] = [:]
    
    var cacheSize: Int {
        return cache.count
    }
    
    func store(image: UIImage, forKey key: String) {
        cache[key] = image
    }
    
    func image(forKey key: String) -> UIImage? {
        return cache[key]
    }
}

@MainActor
class ImageLoader: ObservableObject {
    @Published var loadedImage: UIImage?
    private let cache = ImageCache()
    
    // ❌ Error: Accessing actor property without await
    func loadImage(from url: URL) {
        // Check cache size for debugging
        print("Cache size: \(cache.cacheSize)") // Error here!
        
        // Try to get cached image
        if let cachedImage = cache.image(forKey: url.absoluteString) { // Error here!
            loadedImage = cachedImage
            return
        }
        
        // Load from network...
    }
    
    // ✅ Fix: Add await for actor calls
    func loadImage(from url: URL) async {
        // Check cache size for debugging
        print("Cache size: \(await cache.cacheSize)")
        
        // Try to get cached image
        if let cachedImage = await cache.image(forKey: url.absoluteString) {
            loadedImage = cachedImage
            return
        }
        
        // Load from network
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            if let image = UIImage(data: data) {
                await cache.store(image: image, forKey: url.absoluteString)
                loadedImage = image
            }
        } catch {
            print("Failed to load image: \(error)")
        }
    }
}

// Usage in SwiftUI:
struct AsyncImageView: View {
    let url: URL
    @StateObject private var loader = ImageLoader()
    
    var body: some View {
        Group {
            if let image = loader.loadedImage {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                ProgressView()
            }
        }
        .task {
            await loader.loadImage(from: url)
        }
    }
}

5. "Cannot use mutating member on immutable value: 'self' is immutable"

Error Message:

Cannot use mutating member on immutable value: 'self' is immutable

Problem & Solution:

struct DataLoader {
    var isLoading = false
    
    // ❌ Error: Mutating async function
    func loadData() async {
        isLoading = true // Error: Cannot mutate
    }
}

// ✅ Fix: Add mutating keyword
struct DataLoader {
    var isLoading = false
    
    mutating func loadData() async {
        isLoading = true
    }
}

// ✅ Better: Use class with @MainActor
@MainActor
class DataLoader: ObservableObject {
    @Published var isLoading = false
    
    func loadData() async {
        isLoading = true // Works fine
    }
}

6. "Call to main actor-isolated instance method 'updateUI' in a synchronous nonisolated context"

Error Message:

Call to main actor-isolated instance method 'updateUI' in a synchronous nonisolated context

Problem & Solution:

@MainActor
class ViewModel {
    func updateUI() { }
}

// ❌ Error: Calling MainActor method from background
func backgroundWork() {
    let vm = ViewModel()
    vm.updateUI() // Error: Not on main actor
}

// ✅ Fix: Use await or MainActor.run
func backgroundWork() async {
    let vm = ViewModel()
    await vm.updateUI()
}

// Or:
func backgroundWork() {
    Task { @MainActor in
        let vm = ViewModel()
        vm.updateUI()
    }
}

7. "Cannot convert value of type 'Task<Void, Never>' to expected argument type 'AnyCancellable'"

Error Message:

Cannot convert value of type 'Task<Void, Never>' to expected argument type 'AnyCancellable'

Problem & Solution:

class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    // ❌ Error: Task is not AnyCancellable
    func loadData() {
        Task {
            await fetchData()
        }
        .store(in: &cancellables) // Error: Task doesn't have store method
    }
}

// ✅ Fix: Store Task reference separately or use @MainActor
@MainActor
class ViewModel: ObservableObject {
    private var currentTask: Task<Void, Never>?
    
    func loadData() {
        currentTask?.cancel()
        currentTask = Task {
            await fetchData()
        }
    }
}

8. "Mutation of captured var 'self' in concurrently-executing code"

Error Message:

Mutation of captured var 'self' in concurrently-executing code

Problem & Solution:

class ViewModel {
    var data: String = ""
    
    // ❌ Error: Mutating self in concurrent context
    func loadData() {
        Task {
            self.data = await fetchData() // Error: Concurrent mutation
        }
    }
}

// ✅ Fix: Use @MainActor
@MainActor
class ViewModel: ObservableObject {
    @Published var data: String = ""
    
    func loadData() {
        Task {
            data = await fetchData() // Works fine
        }
    }
}

9. "Cannot call mutating async function 'loadData' on actor-isolated property 'manager'"

Error Message:

Cannot call mutating async function 'loadData' on actor-isolated property 'manager'

Problem & Solution:

actor DataManager {
    var cache: [String: Data] = [:]
    
    mutating func updateCache() async {
        // Mutating function
    }
}

class ViewModel {
    let manager = DataManager()
    
    // ❌ Error: Cannot call mutating function on actor
    func update() async {
        await manager.updateCache() // Error: mutating function
    }
}

// ✅ Fix: Remove mutating (actors handle isolation automatically)
actor DataManager {
    var cache: [String: Data] = [:]
    
    func updateCache() async { // Remove mutating
        cache["key"] = Data() // This is safe in actors
    }
}

10. "Generic parameter 'Failure' could not be inferred"

Error Message:

Generic parameter 'Failure' could not be inferred

Problem & Solution:

// ❌ Error: Converting Publisher without specifying error type
func convertPublisher() async -> String {
    return await withCheckedContinuation { continuation in
        publisher // Error: Cannot infer Failure type
            .sink { completion in
                // Handle completion
            } receiveValue: { value in
                continuation.resume(returning: value)
            }
    }
}

// ✅ Fix: Specify error type explicitly
func convertPublisher() async throws -> String {
    return try await withCheckedThrowingContinuation { continuation in
        publisher
            .sink(
                receiveCompletion: { completion in
                    if case .failure(let error) = completion {
                        continuation.resume(throwing: error)
                    }
                },
                receiveValue: { value in
                    continuation.resume(returning: value)
                }
            )
    }
}

🔧 Quick Error Fixes

Error Pattern Quick Fix
Missing await Add await before async function calls
Function not async Add async to function signature
Sync context calling async Wrap in Task { }
Actor isolation Add await for actor calls
MainActor violations Use @MainActor or MainActor.run
Mutating struct Add mutating or use class
Task storage Store Task references, not in AnyCancellable
Concurrent mutations Use @MainActor for UI classes
Generic inference Specify error types explicitly

💡 Pro Tips for Avoiding Compile Errors

  1. Start with @MainActor: Mark UI classes with @MainActor first
  2. Add async gradually: Convert functions one at a time
  3. Use Task for bridging: Wrap async calls in Task { } when needed
  4. Check actor isolation: Remember await for actor calls
  5. Specify error types: Be explicit with generic parameters
  6. Use Xcode suggestions: Let Xcode guide you with fix-it suggestions

🔄 TaskGroup vs Async Let: When to Use Which?

The Key Differences

Feature async let TaskGroup
Number of tasks Fixed at compile time Dynamic at runtime
Task types Can be different types Must be same type
Error handling Individual try/catch Centralized handling
Cancellation Automatic with parent Manual control
Performance Slightly faster More flexible
Use case Known, heterogeneous tasks Unknown count, homogeneous tasks

When to Use async let

Use async let when you have a fixed number of different tasks known at compile time:

// ✅ Perfect for async let: Fixed, different types
func loadUserProfile(userId: String) async throws -> UserProfile {
    async let user = fetchUser(userId)           // Returns User
    async let posts = fetchUserPosts(userId)     // Returns [Post]
    async let followers = fetchFollowers(userId) // Returns [Follower]
    async let settings = fetchSettings(userId)   // Returns Settings
    
    return UserProfile(
        user: try await user,
        posts: try await posts,
        followers: try await followers,
        settings: try await settings
    )
}

// ✅ Good: Known number of API calls
func loadDashboard() async throws -> Dashboard {
    async let weather = fetchWeather()
    async let news = fetchNews()
    async let stocks = fetchStocks()
    
    return Dashboard(
        weather: try await weather,
        news: try await news,
        stocks: try await stocks
    )
}

When to Use TaskGroup

Use TaskGroup when you have a dynamic number of similar tasks or need more control:

1. Dynamic Number of Tasks

// ✅ Perfect for TaskGroup: Dynamic count
func downloadImages(urls: [URL]) async throws -> [UIImage] {
    return try await withThrowingTaskGroup(of: UIImage.self) { group in
        // Don't know how many URLs at compile time
        for url in urls {
            group.addTask {
                try await downloadImage(from: url)
            }
        }
        
        var images: [UIImage] = []
        for try await image in group {
            images.append(image)
        }
        return images
    }
}

// ✅ Good: Processing array of items
func processFiles(filePaths: [String]) async throws -> [ProcessedFile] {
    try await withThrowingTaskGroup(of: ProcessedFile.self) { group in
        for path in filePaths {
            group.addTask {
                try await processFile(at: path)
            }
        }
        
        var results: [ProcessedFile] = []
        for try await result in group {
            results.append(result)
        }
        return results
    }
}

2. Same Type Results

// ✅ TaskGroup: All tasks return same type
func fetchMultipleUsers(userIds: [String]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for userId in userIds {
            group.addTask {
                try await fetchUser(userId)
            }
        }
        
        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

// ❌ async let would be awkward here:
func fetchMultipleUsersAwkward(userIds: [String]) async throws -> [User] {
    // This doesn't work - can't create dynamic async let
    // async let user1 = fetchUser(userIds[0]) // What if array is empty?
    // async let user2 = fetchUser(userIds[1]) // What if only 1 element?
    // ...
}

3. Advanced Error Handling

// ✅ TaskGroup: Sophisticated error handling
func fetchWithRetries(urls: [URL]) async -> [Result<Data, Error>] {
    await withTaskGroup(of: Result<Data, Error>.self) { group in
        for url in urls {
            group.addTask {
                // Custom retry logic per task
                for attempt in 1...3 {
                    do {
                        let (data, _) = try await URLSession.shared.data(from: url)
                        return .success(data)
                    } catch {
                        if attempt == 3 {
                            return .failure(error)
                        }
                        try? await Task.sleep(nanoseconds: UInt64(attempt) * 1_000_000_000)
                    }
                }
                return .failure(URLError(.unknown))
            }
        }
        
        var results: [Result<Data, Error>] = []
        for await result in group {
            results.append(result)
        }
        return results
    }
}

4. Early Termination & Cancellation

// ✅ TaskGroup: Cancel remaining tasks when one succeeds
func findFirstAvailableServer(servers: [URL]) async throws -> URL {
    try await withThrowingTaskGroup(of: URL.self) { group in
        for server in servers {
            group.addTask {
                try await checkServerAvailability(server)
                return server
            }
        }
        
        // Return first successful result and cancel others
        for try await availableServer in group {
            group.cancelAll() // Cancel remaining tasks
            return availableServer
        }
        
        throw NetworkError.noServersAvailable
    }
}

// ✅ TaskGroup: Limit concurrent operations
func downloadWithLimit(urls: [URL], maxConcurrent: Int) async throws -> [Data] {
    try await withThrowingTaskGroup(of: (Int, Data).self) { group in
        var results: [Data?] = Array(repeating: nil, count: urls.count)
        var currentIndex = 0
        
        // Start initial batch
        for _ in 0..<min(maxConcurrent, urls.count) {
            let index = currentIndex
            currentIndex += 1
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: urls[index])
                return (index, data)
            }
        }
        
        // Process results and add new tasks
        for try await (index, data) in group {
            results[index] = data
            
            // Add next task if available
            if currentIndex < urls.count {
                let nextIndex = currentIndex
                currentIndex += 1
                group.addTask {
                    let (data, _) = try await URLSession.shared.data(from: urls[nextIndex])
                    return (nextIndex, data)
                }
            }
        }
        
        return results.compactMap { $0 }
    }
}

// 🖼️ Real Example: Download 1000 images with only 5 concurrent downloads
func downloadImages(imageUrls: [URL]) async throws -> [UIImage] {
    let maxConcurrent = 5
    
    return try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
        var images: [UIImage?] = Array(repeating: nil, count: imageUrls.count)
        var currentIndex = 0
        
        // Start first 5 downloads
        for _ in 0..<min(maxConcurrent, imageUrls.count) {
            let index = currentIndex
            currentIndex += 1
            
            group.addTask {
                print("📥 Starting download \(index + 1)/\(imageUrls.count)")
                let (data, _) = try await URLSession.shared.data(from: imageUrls[index])
                guard let image = UIImage(data: data) else {
                    throw ImageError.invalidData
                }
                print("✅ Completed download \(index + 1)/\(imageUrls.count)")
                return (index, image)
            }
        }
        
        // As each download completes, start the next one
        for try await (completedIndex, image) in group {
            images[completedIndex] = image
            
            // Start next download if more URLs remain
            if currentIndex < imageUrls.count {
                let nextIndex = currentIndex
                currentIndex += 1
                
                group.addTask {
                    print("📥 Starting download \(nextIndex + 1)/\(imageUrls.count)")
                    let (data, _) = try await URLSession.shared.data(from: imageUrls[nextIndex])
                    guard let image = UIImage(data: data) else {
                        throw ImageError.invalidData
                    }
                    print("✅ Completed download \(nextIndex + 1)/\(imageUrls.count)")
                    return (nextIndex, image)
                }
            }
        }
        
        return images.compactMap { $0 }
    }
}

// 📱 Usage Example:
func loadPhotoGallery() async {
    let imageUrls = generateImageUrls(count: 1000) // Your 1000 image URLs
    
    do {
        print("🚀 Starting download of \(imageUrls.count) images (max 5 concurrent)")
        let images = try await downloadImages(imageUrls: imageUrls)
        print("🎉 Downloaded \(images.count) images successfully!")
        
        // Update UI with images
        await MainActor.run {
            self.photoGallery = images
        }
    } catch {
        print("❌ Download failed: \(error)")
    }
}

Real-World Comparison

Scenario: Loading a social media feed

// ✅ async let: Fixed, different data types
func loadFeedData(userId: String) async throws -> FeedData {
    async let profile = fetchUserProfile(userId)     // User
    async let timeline = fetchTimeline(userId)       // [Post]
    async let notifications = fetchNotifications(userId) // [Notification]
    async let suggestions = fetchSuggestions(userId)     // [User]
    
    return FeedData(
        profile: try await profile,
        timeline: try await timeline,
        notifications: try await notifications,
        suggestions: try await suggestions
    )
}

// ✅ TaskGroup: Dynamic posts with images
func loadTimelineWithImages(posts: [Post]) async throws -> [PostWithImage] {
    try await withThrowingTaskGroup(of: PostWithImage.self) { group in
        for post in posts {
            group.addTask {
                let imageData = try await downloadImage(from: post.imageURL)
                return PostWithImage(post: post, imageData: imageData)
            }
        }
        
        var postsWithImages: [PostWithImage] = []
        for try await postWithImage in group {
            postsWithImages.append(postWithImage)
        }
        return postsWithImages
    }
}

Performance Considerations

async let Advantages:

  • Slightly faster: Less overhead
  • Compile-time optimization: Better compiler optimizations
  • Simpler memory management: Automatic cleanup

TaskGroup Advantages:

  • Memory efficient: Can process results as they arrive
  • Flexible cancellation: Cancel individual or all tasks
  • Better for large datasets: Doesn't hold all results in memory

Decision Tree

Do you know the exact number of tasks at compile time?
├─ YES: Do the tasks return different types?
│  ├─ YES: Use `async let` ✅
│  └─ NO: Are there more than ~5 tasks?
│     ├─ YES: Consider `TaskGroup` for readability
│     └─ NO: `async let` is fine ✅
└─ NO: Use `TaskGroup` ✅

Do you need advanced error handling or cancellation?
└─ YES: Use `TaskGroup` ✅

Do you need to limit concurrent operations?
└─ YES: Use `TaskGroup` ✅

Is the number of tasks very large (>100)?
└─ YES: Use `TaskGroup` for memory efficiency ✅

Best Practices

  1. Start with async let for simple, fixed scenarios
  2. Use TaskGroup when you need flexibility
  3. Consider memory usage for large datasets
  4. Handle errors appropriately in both approaches
  5. Test cancellation behavior in your specific use case

🏗️ Complete Migration Example

Here's a real-world example showing a complete migration from callbacks to async/await:

Before: Callback-based Implementation

// Old callback-based approach
class UserService {
    func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
                return
            }
            
            guard let data = data else {
                DispatchQueue.main.async {
                    completion(.failure(APIError.noData))
                }
                return
            }
            
            do {
                let user = try JSONDecoder().decode(User.self, from: data)
                DispatchQueue.main.async {
                    completion(.success(user))
                }
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }.resume()
    }
}

class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let userService = UserService()
    
    func loadUser(id: String) {
        isLoading = true
        errorMessage = nil
        
        userService.fetchUser(id: id) { [weak self] result in
            self?.isLoading = false
            
            switch result {
            case .success(let user):
                self?.user = user
            case .failure(let error):
                self?.errorMessage = error.localizedDescription
            }
        }
    }
}

After: Async/Await Implementation

// Modern async/await approach
class UserService {
    func fetchUser(id: String) async throws -> User {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        let (data, response) = try await URLSession.shared.data(from: url)
        
        // Handle HTTP errors
        if let httpResponse = response as? HTTPURLResponse,
           httpResponse.statusCode >= 400 {
            throw APIError.httpError(httpResponse.statusCode)
        }
        
        return try JSONDecoder().decode(User.self, from: data)
    }
}

@MainActor
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let userService = UserService()
    
    func loadUser(id: String) async {
        isLoading = true
        errorMessage = nil
        
        do {
            user = try await userService.fetchUser(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
    }
}

// SwiftUI Usage
struct UserProfileView: View {
    @StateObject private var viewModel = UserViewModel()
    let userId: String
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                ProgressView("Loading user...")
            } else if let user = viewModel.user {
                VStack {
                    Text(user.name)
                        .font(.title)
                    Text(user.email)
                        .foregroundColor(.secondary)
                }
            } else if let error = viewModel.errorMessage {
                Text("Error: \(error)")
                    .foregroundColor(.red)
                Button("Retry") {
                    Task {
                        await viewModel.loadUser(id: userId)
                    }
                }
            }
        }
        .task {
            await viewModel.loadUser(id: userId)
        }
        .refreshable {
            await viewModel.loadUser(id: userId)
        }
    }
}

Key Improvements:

  • Cleaner code: No nested callbacks or manual thread switching
  • Better error handling: Standard do-catch blocks
  • Automatic main thread: @MainActor ensures UI updates on main thread
  • Built-in cancellation: .task automatically cancels when view disappears
  • SwiftUI integration: .refreshable works seamlessly with async functions
  • No memory leaks: No need for [weak self] with @MainActor

⚡ Quick Reference

Symbol Description
async Function can be suspended
await Wait for async function to complete
Task { } Create new async task
async let Concurrent execution
@MainActor Run on main thread
actor Thread-safe data container
try await Async function that can throw
.task { } SwiftUI async work

🚀 Getting Started

  1. Clone this repository
  2. Open AsyncAwait.xcodeproj in Xcode
  3. Run the app on iOS Simulator or device
  4. Start with Lesson 0 to understand the journey from callbacks to async/await
  5. Progress through each lesson at your own pace

🎯 Key Concepts Covered

  • Structured Concurrency: How Swift manages async tasks safely
  • Actor Model: Thread-safe data sharing without locks
  • MainActor: Ensuring UI updates happen on the main thread
  • Error Handling: Proper async error management
  • Task Management: Creating, canceling, and coordinating tasks
  • Performance: When and how to use concurrent execution

📖 Additional Resources

Happy Cooking with Async/Await! 🍳✨

About

Lesson of Swift Async await actor

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages