Getting Started with Swift Concurrency
Introduction
Swift 5.5 introduced a modern approach to concurrency with async/await syntax, making asynchronous code more readable and maintainable. In this post, we'll explore the basics of Swift concurrency and how it can improve your code.
What is Async/Await?
The async and await keywords allow you to write asynchronous code that looks and behaves like synchronous code. This makes it easier to understand control flow and handle errors.
Basic Example
Here's a simple example of fetching data from a URL:
func fetchUser(id: Int) 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)
}
Calling Async Functions
To call an async function, you need to be in an async context. You can create one using a Task:
Task {
do {
let user = try await fetchUser(id: 123)
print("User name: \(user.name)")
} catch {
print("Error fetching user: \(error)")
}
}
Structured Concurrency
Swift's structured concurrency ensures that tasks are properly managed and cancelled when needed. You can run multiple tasks concurrently using async let:
func loadUserProfile() async throws -> Profile {
async let user = fetchUser(id: 123)
async let posts = fetchPosts(userId: 123)
async let followers = fetchFollowers(userId: 123)
return try await Profile(
user: user,
posts: posts,
followers: followers
)
}
Task Groups
For dynamic concurrency, use task groups:
func downloadImages(urls: [URL]) async -> [UIImage] {
await withTaskGroup(of: UIImage?.self) { group in
for url in urls {
group.addTask {
try? await downloadImage(from: url)
}
}
var images: [UIImage] = []
for await image in group {
if let image = image {
images.append(image)
}
}
return images
}
}
Actors for Thread-Safe State
Actors protect their mutable state from data races:
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
// Usage
let counter = Counter()
await counter.increment()
let currentValue = await counter.getValue()
MainActor for UI Updates
Use @MainActor to ensure code runs on the main thread:
@MainActor
class ViewModel: ObservableObject {
@Published var data: [Item] = []
func loadData() async {
let items = await fetchItems()
self.data = items // Safe to update UI
}
}
Best Practices
- Always use
awaitfor async calls - Don't block threads with synchronous operations - Handle errors properly - Use
try awaitand proper error handling - Avoid excessive task creation - Reuse tasks when possible
- Use actors for shared mutable state - Protect data from race conditions
Conclusion
Swift concurrency with async/await makes asynchronous programming more intuitive and safer. By leveraging structured concurrency and actors, you can write cleaner, more maintainable code that's free from common concurrency bugs.