| name | swift-concurrency |
| description | Swift concurrency expert for async programming. Use when working with async/await, actors, Task, TaskGroup, MainActor, Sendable, continuations, or managing concurrent operations. |
Swift Concurrency
Expert guidance for modern Swift concurrency with async/await and actors.
Async/Await Basics
Async Functions
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)
}
func loadUser() async {
do {
let user = try await fetchUser(id: 1)
print(user.name)
} catch {
print("Error: \(error)")
}
}
Async Properties
struct ImageLoader {
let url: URL
var image: UIImage {
get async throws {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
}
}
let loader = ImageLoader(url: imageURL)
let image = try await loader.image
Async Sequences
for try await line in url.lines {
print(line)
}
func notifications() -> AsyncStream<Notification> {
AsyncStream { continuation in
let observer = NotificationCenter.default.addObserver(
forName: .myNotification,
object: nil,
queue: nil
) { notification in
continuation.yield(notification)
}
continuation.onTermination = { _ in
NotificationCenter.default.removeObserver(observer)
}
}
}
for await notification in notifications() {
print("Received: \(notification)")
}
Task
Basic Task
Task {
await performWork()
}
Task(priority: .high) {
await urgentWork()
}
Task.detached {
await backgroundWork()
}
Task Cancellation
class DataLoader {
private var loadTask: Task<Void, Never>?
func startLoading() {
loadTask = Task {
do {
try Task.checkCancellation()
let data = try await fetchData()
guard !Task.isCancelled else { return }
await processData(data)
} catch is CancellationError {
print("Task was cancelled")
} catch {
print("Error: \(error)")
}
}
}
func cancelLoading() {
loadTask?.cancel()
}
}
Task Value
let task = Task {
try await fetchUser(id: 1)
}
let user = try await task.value
TaskGroup
Parallel Execution
func fetchAllUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
Limiting Concurrency
func processImages(_ urls: [URL], maxConcurrent: Int = 4) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: UIImage.self) { group in
var images: [UIImage] = []
var iterator = urls.makeIterator()
for _ in 0..<maxConcurrent {
if let url = iterator.next() {
group.addTask { try await downloadImage(url) }
}
}
for try await image in group {
images.append(image)
if let url = iterator.next() {
group.addTask { try await downloadImage(url) }
}
}
return images
}
}
First Result Wins
func fetchFromFastestServer() async throws -> Data {
try await withThrowingTaskGroup(of: Data.self) { group in
let servers = ["server1.com", "server2.com", "server3.com"]
for server in servers {
group.addTask {
try await fetch(from: server)
}
}
guard let data = try await group.next() else {
throw NetworkError.noResponse
}
group.cancelAll()
return data
}
}
Actors
Basic Actor
actor BankAccount {
private var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
func withdraw(_ amount: Double) throws -> Double {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
return amount
}
func getBalance() -> Double {
balance
}
}
let account = BankAccount()
await account.deposit(100)
let balance = await account.getBalance()
Nonisolated
actor DataStore {
private var items: [Item] = []
nonisolated let id = UUID()
nonisolated var description: String {
"DataStore \(id)"
}
func addItem(_ item: Item) {
items.append(item)
}
}
Actor Reentrancy
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private var inProgress: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
if let cached = cache[url] {
return cached
}
if let task = inProgress[url] {
return try await task.value
}
let task = Task {
try await downloadImage(url)
}
inProgress[url] = task
do {
let image = try await task.value
cache[url] = image
inProgress[url] = nil
return image
} catch {
inProgress[url] = nil
throw error
}
}
}
MainActor
MainActor Annotation
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
func loadItems() async {
isLoading = true
defer { isLoading = false }
do {
items = try await fetchItems()
} catch {
}
}
}
MainActor.run
func processInBackground() async {
let result = await performHeavyWork()
await MainActor.run {
updateUI(with: result)
}
}
MainActor in SwiftUI
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
List(viewModel.items) { item in
Text(item.name)
}
.task {
await viewModel.loadItems()
}
}
}
Sendable
Sendable Types
struct UserData: Sendable {
let id: Int
let name: String
}
final class Counter: Sendable {
let count: Int
init(count: Int) {
self.count = count
}
}
final class ThreadSafeCounter: @unchecked Sendable {
private var lock = NSLock()
private var _count = 0
var count: Int {
lock.lock()
defer { lock.unlock() }
return _count
}
func increment() {
lock.lock()
defer { lock.unlock() }
_count += 1
}
}
Sendable Closures
func performAsync(_ work: @Sendable @escaping () async -> Void) {
Task {
await work()
}
}
performAsync { @Sendable in
await processData()
}
Continuations
Bridging Callbacks to Async
func fetchUser() async throws -> User {
try await withCheckedThrowingContinuation { continuation in
oldAPIWithCallback { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
Unsafe Continuation (Performance)
func fetchData() async -> Data {
await withUnsafeContinuation { continuation in
legacyFetch { data in
continuation.resume(returning: data)
}
}
}
async let
Concurrent Binding
func loadDashboard() async throws -> Dashboard {
async let user = fetchUser()
async let posts = fetchPosts()
async let notifications = fetchNotifications()
return try await Dashboard(
user: user,
posts: posts,
notifications: notifications
)
}
With Cancellation
func loadData() async throws -> (User, [Post]) {
async let user = fetchUser()
async let posts = fetchPosts()
return try await (user, posts)
}
Best Practices
Structured Concurrency
func loadData() async {
await withTaskGroup(of: Void.self) { group in
group.addTask { await loadUsers() }
group.addTask { await loadPosts() }
}
}
func loadData() {
Task { await loadUsers() }
Task { await loadPosts() }
}
Error Handling
func fetchWithRetry<T>(
maxAttempts: Int = 3,
operation: () async throws -> T
) async throws -> T {
var lastError: Error?
for attempt in 1...maxAttempts {
do {
return try await operation()
} catch {
lastError = error
if attempt < maxAttempts {
try await Task.sleep(nanoseconds: UInt64(attempt) * 1_000_000_000)
}
}
}
throw lastError!
}
Apple Documentation