12 KiB
Concurrency Patterns
Modern Swift concurrency for responsive, safe macOS apps.
<async_await_basics> <simple_async>
// Basic async function
func fetchData() async throws -> [Item] {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Item].self, from: data)
}
// Call from view
struct ContentView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task {
do {
items = try await fetchData()
} catch {
// Handle error
}
}
}
}
</simple_async>
<task_modifier>
struct ItemListView: View {
@State private var items: [Item] = []
let category: Category
var body: some View {
List(items) { item in
Text(item.name)
}
// .task runs when view appears, cancels when disappears
.task {
await loadItems()
}
// .task(id:) re-runs when id changes
.task(id: category) {
await loadItems(for: category)
}
}
func loadItems(for category: Category? = nil) async {
// Automatically cancelled if view disappears
items = await dataService.fetchItems(category: category)
}
}
</task_modifier> </async_await_basics>
```swift // Actor for thread-safe state actor DataCache { private var cache: [String: Data] = [:]func get(_ key: String) -> Data? {
cache[key]
}
func set(_ key: String, data: Data) {
cache[key] = data
}
func clear() {
cache.removeAll()
}
}
// Usage (must await) let cache = DataCache() await cache.set("key", data: data) let cached = await cache.get("key")
</basic_actor>
<service_actor>
```swift
actor NetworkService {
private let session: URLSession
private var pendingRequests: [URL: Task<Data, Error>] = [:]
init(session: URLSession = .shared) {
self.session = session
}
func fetch(_ url: URL) async throws -> Data {
// Deduplicate concurrent requests for same URL
if let existing = pendingRequests[url] {
return try await existing.value
}
let task = Task {
let (data, _) = try await session.data(from: url)
return data
}
pendingRequests[url] = task
defer {
pendingRequests[url] = nil
}
return try await task.value
}
}
</service_actor>
```swift actor ImageProcessor { private var processedCount = 0// Synchronous access for non-isolated properties
nonisolated let maxConcurrent = 4
// Computed property that doesn't need isolation
nonisolated var identifier: String {
"ImageProcessor-\(ObjectIdentifier(self))"
}
func process(_ image: NSImage) async -> NSImage {
processedCount += 1
// Process image...
return processedImage
}
func getCount() -> Int {
processedCount
}
}
</nonisolated>
</actors>
<main_actor>
<ui_updates>
```swift
// Mark entire class as @MainActor
@MainActor
@Observable
class AppState {
var items: [Item] = []
var isLoading = false
var error: AppError?
func loadItems() async {
isLoading = true
defer { isLoading = false }
do {
// This call might be on background, result delivered on main
items = try await dataService.fetchAll()
} catch {
self.error = .loadFailed(error)
}
}
}
// Or mark specific functions
class DataProcessor {
@MainActor
func updateUI(with result: ProcessResult) {
// Safe to update UI here
}
func processInBackground() async -> ProcessResult {
// Heavy work here
let result = await heavyComputation()
// Update UI on main actor
await updateUI(with: result)
return result
}
}
</ui_updates>
<main_actor_dispatch>
// From async context
await MainActor.run {
self.items = newItems
}
// Assume main actor (when you know you're on main)
MainActor.assumeIsolated {
self.tableView.reloadData()
}
// Task on main actor
Task { @MainActor in
self.progress = 0.5
}
</main_actor_dispatch> </main_actor>
<structured_concurrency> <task_groups>
// Parallel execution with results
func loadAllCategories() async throws -> [Category: [Item]] {
let categories = try await fetchCategories()
return try await withThrowingTaskGroup(of: (Category, [Item]).self) { group in
for category in categories {
group.addTask {
let items = try await self.fetchItems(for: category)
return (category, items)
}
}
var results: [Category: [Item]] = [:]
for try await (category, items) in group {
results[category] = items
}
return results
}
}
</task_groups>
<limited_concurrency>
// Process with limited parallelism
func processImages(_ urls: [URL], maxConcurrent: Int = 4) async throws -> [ProcessedImage] {
var results: [ProcessedImage] = []
try await withThrowingTaskGroup(of: ProcessedImage.self) { group in
var iterator = urls.makeIterator()
// Start initial batch
for _ in 0..<min(maxConcurrent, urls.count) {
if let url = iterator.next() {
group.addTask {
try await self.processImage(at: url)
}
}
}
// As each completes, add another
for try await result in group {
results.append(result)
if let url = iterator.next() {
group.addTask {
try await self.processImage(at: url)
}
}
}
}
return results
}
</limited_concurrency>
<async_let>
// Concurrent bindings
func loadDashboard() async throws -> Dashboard {
async let user = fetchUser()
async let projects = fetchProjects()
async let notifications = fetchNotifications()
// All three run concurrently, await results together
return try await Dashboard(
user: user,
projects: projects,
notifications: notifications
)
}
</async_let> </structured_concurrency>
<async_sequences> <for_await>
// Iterate async sequence
func monitorChanges() async {
for await change in fileMonitor.changes {
await processChange(change)
}
}
// With notifications
func observeNotifications() async {
let notifications = NotificationCenter.default.notifications(named: .dataChanged)
for await notification in notifications {
guard !Task.isCancelled else { break }
await handleNotification(notification)
}
}
</for_await>
<custom_async_sequence>
struct CountdownSequence: AsyncSequence {
typealias Element = Int
let start: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
mutating func next() async -> Int? {
guard current > 0 else { return nil }
try? await Task.sleep(for: .seconds(1))
defer { current -= 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(current: start)
}
}
// Usage
for await count in CountdownSequence(start: 10) {
print(count)
}
</custom_async_sequence>
<async_stream>
// Bridge callback-based API
func fileChanges(at path: String) -> AsyncStream<FileChange> {
AsyncStream { continuation in
let monitor = FileMonitor(path: path) { change in
continuation.yield(change)
}
monitor.start()
continuation.onTermination = { _ in
monitor.stop()
}
}
}
// Throwing version
func networkEvents() -> AsyncThrowingStream<NetworkEvent, Error> {
AsyncThrowingStream { continuation in
let connection = NetworkConnection()
connection.onEvent = { event in
continuation.yield(event)
}
connection.onError = { error in
continuation.finish(throwing: error)
}
connection.onComplete = {
continuation.finish()
}
connection.start()
continuation.onTermination = { _ in
connection.cancel()
}
}
}
</async_stream> </async_sequences>
```swift func processLargeDataset(_ items: [Item]) async throws -> [Result] { var results: [Result] = []for item in items {
// Check for cancellation
try Task.checkCancellation()
// Or check without throwing
if Task.isCancelled {
break
}
let result = await process(item)
results.append(result)
}
return results
}
</checking_cancellation>
<cancellation_handlers>
```swift
func downloadFile(_ url: URL) async throws -> Data {
let task = URLSession.shared.dataTask(with: url)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
task.completionHandler = { data, _, error in
if let error = error {
continuation.resume(throwing: error)
} else if let data = data {
continuation.resume(returning: data)
}
}
task.resume()
}
} onCancel: {
task.cancel()
}
}
</cancellation_handlers>
<task_cancellation>
class ViewModel {
private var loadTask: Task<Void, Never>?
func load() {
// Cancel previous load
loadTask?.cancel()
loadTask = Task {
await performLoad()
}
}
func cancel() {
loadTask?.cancel()
loadTask = nil
}
deinit {
loadTask?.cancel()
}
}
</task_cancellation>
```swift // Value types are Sendable by default if all properties are Sendable struct Item: Sendable { let id: UUID let name: String let count: Int }// Classes must be explicitly Sendable final class ImmutableConfig: Sendable { let apiKey: String let baseURL: URL
init(apiKey: String, baseURL: URL) {
self.apiKey = apiKey
self.baseURL = baseURL
}
}
// Actors are automatically Sendable actor Counter: Sendable { var count = 0 }
// Mark as @unchecked Sendable when you manage thread safety yourself final class ThreadSafeCache: @unchecked Sendable { private let lock = NSLock() private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
}
</sendable_types>
<sending_closures>
```swift
// Closures that cross actor boundaries must be @Sendable
func processInBackground(work: @Sendable @escaping () async -> Void) {
Task.detached {
await work()
}
}
// Capture only Sendable values
let items = items // Must be Sendable
Task {
await process(items)
}
</sending_closures>
<best_practices>
- Use
.taskmodifier for view-related async work - Use actors for shared mutable state
- Mark UI-updating code with
@MainActor - Check
Task.isCancelledin long operations - Use structured concurrency (task groups, async let) over unstructured
- Cancel tasks when no longer needed