iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Tasks

Task is Swift Concurrency’s unit of asynchronous work — like a goroutine or future, with structured concurrency built in. Use Task to bridge sync → async, TaskGroup to fan out, Task.detached sparingly, and trust the cooperative scheduler to keep main-thread work off the UI.

Task, group, cancellation, MainActor

EXAMPLE
// 1) Launch a Task — bridge sync to async
import Foundation

func onTap() {
    Task {
        do {
            let user = try await fetchUser(id: 42)
            print(user)
        } catch {
            print("error: \\(error)")
        }
    }
}

// Tasks created inside an async context INHERIT the actor + priority.
// Tasks at the top level run on the default executor (cooperative thread pool).

// 2) Task with priority
Task(priority: .userInitiated) {
    let data = try await loadHeavyAsset()
    await present(data)
}

// Priorities: .background, .utility, .medium (default), .userInitiated, .high

// 3) Awaiting a result later
let task = Task<Int, Error> {
    try await compute()
}

let result = try await task.value         // wait for completion
let maybe  = await task.result            // Result<Int, Error>

// 4) Cancellation — cooperative
let long = Task {
    for i in 0..<1_000 {
        try Task.checkCancellation()      // throws if cancelled
        try await Task.sleep(for: .milliseconds(10))
    }
}
long.cancel()                              // signal
// The task must CHECK for cancellation; it doesn't kill the work forcibly.

// Inside a task: Task.isCancelled — non-throwing check
for item in items {
    if Task.isCancelled { break }
    await process(item)
}

// 5) Sleep + delay
try await Task.sleep(for: .seconds(1))
try await Task.sleep(for: .milliseconds(250))
try await Task.sleep(nanoseconds: 100_000_000)

// 6) TaskGroup — fan out N parallel tasks
func fetchAll(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 u in group {
            users.append(u)
        }
        return users
    }
}

// Group cancels remaining tasks when an error propagates.

// 7) Bounded TaskGroup — limit concurrency
func fetchBounded(ids: [Int], maxInFlight: Int = 4) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        var idx = 0
        for _ in 0..<min(maxInFlight, ids.count) {
            let id = ids[idx]; idx += 1
            group.addTask { try await fetchUser(id: id) }
        }
        var results: [User] = []
        for try await u in group {
            results.append(u)
            if idx < ids.count {
                let id = ids[idx]; idx += 1
                group.addTask { try await fetchUser(id: id) }
            }
        }
        return results
    }
}

// 8) async let — fixed parallelism with simpler syntax
func loadProfile() async throws -> (User, [Post]) {
    async let user  = fetchUser(id: 1)
    async let posts = fetchPosts(for: 1)
    return try await (user, posts)
}

// async let starts the work immediately; await collects all values.

// 9) Task.detached — escape current actor / inherit nothing
let imageTask = Task.detached(priority: .userInitiated) {
    let data = try await downloadImage(url)
    return UIImage(data: data)
}

// detached tasks DON'T inherit the actor; useful for background CPU work.
// Default Task inherits; usually preferred for UI flows.

// 10) MainActor — UI updates
@MainActor
class ViewModel: ObservableObject {
    @Published var users: [User] = []

    func load() async {
        do {
            users = try await fetchAll(ids: [1, 2, 3])
        } catch {
            // handle
        }
    }
}

// All methods of a @MainActor class run on the main thread; the framework hops as needed.

// 11) Race patterns — fastest of N
func fastest<T>(_ tasks: [@Sendable () async throws -> T]) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        for t in tasks { group.addTask { try await t() } }
        guard let first = try await group.next() else { throw URLError(.badServerResponse) }
        group.cancelAll()
        return first
    }
}

// 12) Timeout — group with a sleep
func withTimeout<T>(seconds: Double, _ work: @escaping @Sendable () async throws -> T) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask { try await work() }
        group.addTask {
            try await Task.sleep(for: .seconds(seconds))
            throw URLError(.timedOut)
        }
        let v = try await group.next()!
        group.cancelAll()
        return v
    }
}

// 13) Sendable — types crossing concurrency domains
// Tasks capture values; they must be Sendable to cross actor boundaries.
//   • Struct/enum with Sendable fields — automatic
//   • final class with immutable fields — Sendable
//   • Actors — Sendable
//   • Classes with mutable state — not Sendable; use actor

struct OrderID: Sendable { let value: String }
final class ImmutableConfig: Sendable { let baseURL: URL; init(_ u: URL) { baseURL = u } }

// 14) Combining with SwiftUI
struct ContentView: View {
    @StateObject private var vm = ViewModel()
    var body: some View {
        List(vm.users, id: \.id) { Text($0.name) }
            .task { await vm.load() }                  // SwiftUI starts + cancels the task with view lifecycle
    }
}

// 15) Task local values (Swift 5.5+)
enum RequestID { @TaskLocal static var current: UUID = UUID() }

await RequestID.$current.withValue(UUID()) {
    await doWork()                                       // anywhere inside, RequestID.current is this UUID
}

// 16) Common bugs
// • Forgot await → 'expression is async but not awaited'
// • Forgot Task wrapper at the boundary of sync code → 'cannot find … async'
// • Holding a Mutex across await → deadlock; use actor or @MainActor
// • Capturing self strongly in a long-lived Task → memory leak; [weak self]
// • Not checking cancellation in tight loops → tasks run after .cancel()
// • Detached Task using main-actor types — runs off main; data race; mark @MainActor or copy first
// • TaskGroup not awaited or no addTask called → cancels at scope end
// • Mixing async / completion handlers in the same flow → confusion; pick one
// • Setting Sendable on classes manually that aren't actually thread-safe → @unchecked Sendable is a footgun

Why it matters

Task launches structured async work that inherits the calling actor and priority. Fan out with TaskGroup or async let, bound concurrency manually, check cancellation in long loops, and prefer regular Task over Task.detached so UI flows stay on the main actor. Reach for timeouts and races via TaskGroup + cancelAll.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
let task = Task {
    return await compute()
}
let result = await task.value
Try it Yourself »

Discussion

Loading…