SWIFT

Swift Concurrency: Syntax, Usage, and Examples

Swift concurrency lets you write asynchronous code that is safer, more readable, and more efficient. By leveraging async/await, structured concurrency, and task management, Swift makes it easier to handle background operations without blocking the main thread.

How to Use Swift Concurrency

The async and await keywords are at the heart of Swift concurrency. They allow functions to run asynchronously, avoiding UI freezes and improving performance.

Declaring an Async Function

func fetchData() async -> String {
    return "Data received"
}

Task {
    let result = await fetchData()
    print(result)  // Output: Data received
}

In this example, fetchData() is an asynchronous function. The Task block lets you call it without blocking the main thread.

Using Async/Await with Networking

func fetchUserData() async throws -> String {
    let url = URL(string: "https://jsonplaceholder.typicode.com/users/1")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(decoding: data, as: UTF8.self)
}

Task {
    do {
        let userData = try await fetchUserData()
        print(userData)
    } catch {
        print("Error fetching data:", error)
    }
}

The await keyword pauses execution until the network request completes, preventing blocking.

When to Use Swift Concurrency

Running Heavy Tasks in the Background

If a task takes time, running it asynchronously prevents the app from freezing. For example, fetching data, processing images, or interacting with a database should run in the background.

func processLargeFile() async {
    print("Processing started")
    try? await Task.sleep(nanoseconds: 2_000_000_000) // Simulates a delay
    print("Processing completed")
}

Task {
    await processLargeFile()
}

UI Updates in SwiftUI

Swift concurrency works seamlessly with SwiftUI, allowing UI updates to run safely on the main thread.

import SwiftUI

struct ContentView: View {
    @State private var message = "Loading..."

    var body: some View {
        VStack {
            Text(message)
                .padding()
            Button("Fetch Data") {
                Task {
                    message = await fetchData()
                }
            }
        }
    }
}

Handling Multiple Concurrent Tasks

You can perform multiple tasks at once using async let, which runs them concurrently instead of sequentially.

async let user = fetchUserData()
async let posts = fetchPosts()

let (userData, postData) = await (user, posts)
print(userData, postData)

Examples of Swift Concurrency

Using Task for Structured Concurrency

The Task API provides a way to run background tasks without blocking execution.

Task {
    print("Start task")
    try? await Task.sleep(nanoseconds: 1_000_000_000) // Simulates delay
    print("End task")
}

Task Cancellation

Swift tasks support cancellation, letting you stop long-running operations when needed.

func performTask() async {
    for i in 1...5 {
        if Task.isCancelled { return } // Exit if cancelled
        print("Task running: \(i)")
        try? await Task.sleep(nanoseconds: 1_000_000_000)
    }
}

let task = Task {
    await performTask()
}

Task {
    try? await Task.sleep(nanoseconds: 2_000_000_000)
    task.cancel() // Cancels the task after 2 seconds
}

Using Actor for Data Safety

Actors protect shared data from race conditions, making concurrency safer.

actor BankAccount {
    private var balance = 0

    func deposit(amount: Int) {
        balance += amount
    }

    func getBalance() -> Int {
        return balance
    }
}

let account = BankAccount()
Task {
    await account.deposit(amount: 100)
    print(await account.getBalance()) // Output: 100
}

Learn More About Swift Concurrency

Swift Concurrency vs Combine

Swift concurrency and combine both handle async programming but serve different use cases.

  • Swift concurrency is easier to read, using async/await syntax for structured concurrency.
  • Combine uses publishers and subscribers, making it more suitable for reactive programming.

Swift Strict Concurrency

Strict concurrency in Swift prevents race conditions by enforcing thread-safe data access. Using actors and isolated data structures ensures safer code.

actor Counter {
    private var count = 0

    func increment() {
        count += 1
    }

    func getCount() -> Int {
        return count
    }
}

let counter = Counter()
Task {
    await counter.increment()
    print(await counter.getCount())  // Output: 1
}

Swift Concurrency Timeout

You can set a timeout for tasks to prevent long-running operations from delaying execution.

func slowFunction() async throws -> String {
    try await Task.sleep(nanoseconds: 5_000_000_000) // Simulates a long delay
    return "Done"
}

Task {
    do {
        let result = try await withTimeout(seconds: 3) {
            try await slowFunction()
        }
        print(result)
    } catch {
        print("Task timed out")
    }
}

func withTimeout<T>(seconds: UInt64, operation: @escaping @Sendable () async throws -> T) async throws -> T {
    try await withThrowingTaskGroup(of: T?.self) { group in
        group.addTask {
            try await operation()
        }
        group.addTask {
            try await Task.sleep(nanoseconds: seconds * 1_000_000_000)
            return nil
        }
        guard let result = try await group.next() else {
            throw NSError(domain: "Timeout", code: 1)
        }
        return result
    }
}

This example cancels the task if it exceeds 3 seconds.

Structured Concurrency in Swift

Structured concurrency ensures that tasks complete before the enclosing scope ends.

func downloadImages() async {
    async let image1 = loadImage(url: "https://example.com/image1.jpg")
    async let image2 = loadImage(url: "https://example.com/image2.jpg")

    let images = await [image1, image2]
    print(images)
}

Error Handling in Async Functions

You can handle errors using do-catch inside async functions.

func fetchData() async throws -> String {
    throw URLError(.badServerResponse)
}

Task {
    do {
        let result = try await fetchData()
        print(result)
    } catch {
        print("Error:", error)
    }
}

Using MainActor for UI Updates

Marking a function with @MainActor ensures UI updates happen on the main thread.

@MainActor
func updateUI() {
    print("Updating UI")
}

Task {
    await updateUI()
}

Best Practices for Swift Concurrency

  • Use async let to perform multiple tasks concurrently.
  • Use Task.sleep() for artificial delays in async functions.
  • Use Task.cancel() to cancel long-running operations.
  • Prefer actors for thread-safe data access.
  • Use @MainActor when modifying UI elements.
  • Always handle errors in async functions using do-catch.
Learn to Code in Swift for Free
Start learning now
button icon
To advance beyond this tutorial and learn Swift by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH