# Getting Started with Swift Async/Await

> Learn how to use Swift's modern concurrency features with async/await to write cleaner asynchronous code

- Page: https://midgarcorp.cc/blog/getting-started-with-swift-async-await/
- Author: guitaripod (Midgar Oy)
- Published: 2024-01-15
- Tags: swift, ios, concurrency, async-await

Swift's async/await syntax revolutionizes how we write asynchronous code, making it more readable and less error-prone than traditional callback-based approaches. Let's explore how to leverage these powerful features in your iOS applications.

## Understanding the Basics

Before async/await, handling asynchronous operations often led to "callback hell":

```swift
func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: userURL) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }

        guard let data = data else {
            completion(.failure(NetworkError.noData))
            return
        }

        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}
```

With async/await, the same code becomes much cleaner:

```swift
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: userURL)
    return try JSONDecoder().decode(User.self, from: data)
}
```

## Creating Async Functions

To create an async function, simply add the `async` keyword:

```swift
func performTimeConsumingTask() async -> String {
    // Simulate work
    try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
    return "Task completed!"
}
```

## Calling Async Functions

You can call async functions using `await`:

```swift
Task {
    let result = await performTimeConsumingTask()
    print(result) // Prints: Task completed!
}
```

## Error Handling

Combine `async` with `throws` for functions that can fail:

```swift
enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

struct NetworkManager {
    func fetch<T: Decodable>(_ type: T.Type, from urlString: String) async throws -> T {
        guard let url = URL(string: urlString) else {
            throw NetworkError.invalidURL
        }

        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(type, from: data)
    }
}
```

## Concurrent Operations

Execute multiple async operations concurrently using `async let`:

```swift
func fetchUserData() async throws -> (User, [Post], [Friend]) {
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let friends = fetchFriends()

    return try await (user, posts, friends)
}
```

## Task Groups

For dynamic concurrency, use task groups:

```swift
func downloadImages(urls: [URL]) async throws -> [UIImage] {
    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 {
            if let image = image {
                images.append(image)
            }
        }
        return images
    }
}
```

## MainActor and UI Updates

Use `@MainActor` to ensure UI updates happen on the main thread:

```swift
@MainActor
class ViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

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

        do {
            users = try await networkManager.fetch([User].self, from: apiURL)
        } catch {
            print("Failed to load users: \(error)")
        }
    }
}
```

## Best Practices

### 1. Avoid Blocking the Main Thread

```swift
// Bad
Task { @MainActor in
    let data = try await expensiveOperation() // Blocks UI
    updateUI(with: data)
}

// Good
Task {
    let data = try await expensiveOperation()
    await MainActor.run {
        updateUI(with: data)
    }
}
```

### 2. Handle Cancellation

```swift
func processLargeDataset() async throws -> ProcessedData {
    var processedItems: [Item] = []

    for item in largeDataset {
        try Task.checkCancellation() // Check if task was cancelled
        let processed = try await process(item)
        processedItems.append(processed)
    }

    return ProcessedData(items: processedItems)
}
```

### 3. Use Structured Concurrency

```swift
func performComplexOperation() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        group.addTask { try await subtask1() }
        group.addTask { try await subtask2() }
        group.addTask { try await subtask3() }

        try await group.waitForAll()
    }
}
```

## Conclusion

Swift's async/await transforms asynchronous programming from a complex challenge into a straightforward task. By adopting these patterns, you'll write more maintainable, readable, and robust code. Start small by converting simple callbacks to async functions, then gradually adopt more advanced patterns like task groups and actors.

Remember: async/await isn't just about syntax—it's about writing better, safer concurrent code that's easier to reason about and debug.
