Welcome to Chapter 9! So far, we’ve learned how to build user interfaces, manage state, and even connect to the internet for data. But what happens when that internet connection is slow, or you have a complicated calculation to make? If your app freezes while it’s waiting, users will get frustrated and might even leave your app! This is where concurrency comes in.
In this chapter, we’re going to dive deep into making your iOS applications responsive and efficient. We’ll explore how to perform multiple tasks seemingly at the same time, without freezing your user interface. We’ll start with Grand Central Dispatch (GCD), Apple’s foundational technology for managing concurrent operations, and then transition to the modern, elegant, and safer approach introduced in Swift 5.5 and fully embraced in Swift 6: structured concurrency with async/await. By the end, you’ll be able to design and implement concurrent operations that keep your app snappy and delightful to use.
This chapter builds upon your understanding of basic Swift syntax, functions, and the idea of fetching data (even if simulated). Get ready to unlock a new level of power and responsiveness for your iOS apps!
What in the World is Concurrency?
Imagine you’re a chef in a busy restaurant. You can’t just cook one dish from start to finish while all other orders pile up. You need to chop vegetables for one dish, start boiling water for another, and check on a roasting chicken, all at roughly the same time. This is concurrency!
In app terms, concurrency means your app can manage and execute multiple tasks in an overlapping manner, giving the appearance that they’re happening simultaneously. It’s about efficiently utilizing your device’s resources.
Why is this so important for iOS apps?
- Responsive UI: Your app’s interface (buttons, scrolling, animations) should always respond instantly to user input. Long-running tasks, like downloading a large image or processing complex data, should never block the UI.
- Efficiency: Modern iPhones have powerful multi-core processors. Concurrency allows you to spread out computationally intensive work across these cores, making your app faster.
- Better User Experience: A smooth, responsive app feels professional and keeps users happy.
It’s crucial to distinguish concurrency from parallelism. Parallelism means tasks are actually executing at the exact same instant on different processor cores. Concurrency is more about managing tasks efficiently, whether they run truly in parallel or take turns on a single core very quickly. On a multi-core device, concurrent tasks can often run in parallel, but the goal of concurrency is broader: effective task management.
The Main Thread vs. Background Threads: A Critical Distinction
Every iOS app starts with a single thread of execution, known as the main thread (or UI thread). This thread is responsible for:
- Drawing and updating your app’s user interface.
- Responding to user interactions (taps, scrolls, gestures).
- Handling app lifecycle events.
The Golden Rule: Never perform long-running or blocking operations on the main thread. If you do, your UI will freeze, become unresponsive, and eventually, iOS might even terminate your app because it appears frozen.
To avoid blocking the main thread, we move time-consuming tasks to background threads. These are separate threads of execution that run alongside the main thread. Once a background task is complete, its results can be passed back to the main thread to update the UI safely.
This diagram illustrates the fundamental flow: user interaction on the main thread, heavy lifting on a background thread, and then back to the main thread for UI updates.
Grand Central Dispatch (GCD): The Foundation
Grand Central Dispatch (GCD) is a low-level API provided by Apple (and available on all Apple platforms) that helps you manage concurrent operations by dispatching tasks to queues. It’s built into the operating system and is highly optimized. Even modern async/await concurrency in Swift uses GCD queues under the hood for scheduling.
Dispatch Queues: Where Tasks Wait
GCD works with dispatch queues. Think of a dispatch queue as a line where tasks wait their turn to be executed.
There are two main types of dispatch queues:
Serial Queues: Tasks in a serial queue execute one at a time, in the order they were added. They guarantee that only one task runs at any given moment on that specific queue.
- The Main Queue:
DispatchQueue.mainis a special serial queue. All UI updates must happen on this queue. - Custom Serial Queues: You can create your own serial queues for specific purposes, ensuring certain operations don’t interfere with each other.
- The Main Queue:
Concurrent Queues: Tasks in a concurrent queue can execute simultaneously, potentially on different threads. The order of completion is not guaranteed, but the order of start is typically preserved.
- Global Queues:
DispatchQueue.global()provides several system-managed concurrent queues with different Quality of Service (QoS) levels. QoS helps the system prioritize tasks:.userInteractive: For tasks that the user is actively waiting for (e.g., animations)..userInitiated: For tasks initiated by the user that require immediate results (e.g., loading content for UI)..utility: For long-running tasks that don’t need immediate results (e.g., downloading large files)..background: For tasks that run in the background and don’t require user interaction (e.g., syncing data).
- Global Queues:
How to Use GCD (Briefly)
You typically use DispatchQueue.async to schedule work.
import Foundation
import UIKit // For DispatchQueue.main
// Simulate a long-running background task
func performHeavyCalculation() {
print("Performing heavy calculation on background thread...")
Thread.sleep(forTimeInterval: 2) // Simulate work
print("Heavy calculation complete!")
}
// Example usage
func loadDataWithGCD() {
print("Starting data load...")
// 1. Perform heavy work on a global background queue (concurrent)
DispatchQueue.global(qos: .userInitiated).async {
performHeavyCalculation()
let result = "Data Loaded!"
print("Got result: \(result)")
// 2. Update the UI on the main queue (serial)
DispatchQueue.main.async {
// This is where you would update a UILabel, UIImageView, etc.
print("UI Updated on main thread with: \(result)")
}
}
print("Data load initiated (app remains responsive)...")
}
// Call this from your app's entry point or a button action
// loadDataWithGCD()
Explanation:
- We define
performHeavyCalculation()to simulate work.Thread.sleepis generally bad in production but useful for demonstration. DispatchQueue.global(qos: .userInitiated).async { ... }moves theperformHeavyCalculation()to a background thread. The app continues running immediately after this line.- Inside the background block, after the work is done,
DispatchQueue.main.async { ... }ensures that the UI update happens back on the main thread, which is safe.
GCD is powerful but can lead to deeply nested “callback hell” if not managed carefully, especially with multiple asynchronous operations depending on each other. This is exactly why Swift introduced async/await.
Structured Concurrency with Async/Await: The Modern Way
Swift 5.5 introduced a revolutionary new approach to concurrency: structured concurrency using async/await. With Swift 6, this system is now fully mature and provides compile-time data-race safety, making it the preferred way to handle concurrency in modern iOS development.
Why async/await?
- Readability: Code written with
async/awaitlooks and flows like synchronous code, making it much easier to understand and maintain than nested closures or callbacks. - Safety (Swift 6): Swift 6’s concurrency model, built around
async/await,Actors, andSendabletypes, provides strong compile-time guarantees against common concurrency bugs like data races. - Error Handling: It integrates seamlessly with Swift’s existing
do-catcherror handling, avoiding separate error callbacks. - Cancellation: Structured concurrency makes it easier to manage and cancel ongoing tasks.
The Building Blocks of async/await
asyncFunctions:- You mark a function or method with the
asynckeyword to indicate that it can perform asynchronous work and potentially suspend its execution. - An
asyncfunction implicitly returns immediately to its caller, allowing the caller to continue with other work. - Example:
func fetchData() async -> Data { ... }
- You mark a function or method with the
awaitExpressions:- Inside an
asyncfunction, you use theawaitkeyword before calling anotherasyncfunction. awaitpauses the execution of the currentasyncfunction until the awaitedasyncfunction completes and returns a result. Whileawaitis waiting, the underlying thread is not blocked; it’s free to do other work, ensuring responsiveness.- Example:
let data = await fetchData()
- Inside an
Task:- A
Taskis the fundamental unit of work in Swift’s structured concurrency. It represents an asynchronous operation. - You create a
Taskto start anasyncoperation from a synchronous context (e.g., a button tap,onAppearin SwiftUI). Tasks are hierarchical: a task can create child tasks, and when the parent task is cancelled, its children are automatically cancelled. This is the “structured” part.- Example:
Task { await someAsyncFunction() }
- A
async let:- For performing multiple
asyncoperations concurrently and waiting for all of them to complete. It’s a more concise way to achieve parallel execution thanTaskGroupfor a fixed number of tasks. - Example:
async let firstResult = fetchFirstData() async let secondResult = fetchSecondData() let combined = await (firstResult, secondResult) // Waits for both
- For performing multiple
TaskGroup:- When you need to perform a dynamic or unknown number of
asyncoperations concurrently and collect their results. - More flexible than
async let.
- When you need to perform a dynamic or unknown number of
Actor:- A crucial concept for preventing data races in concurrent programming.
- An
Actoris a reference type that ensures its mutable state is accessed by only one task at a time. All access to anActor’s mutable properties or methods is implicitlyawaited, guaranteeing isolation. - This is a cornerstone of Swift 6’s data-race safety.
- Example:
actor DataStore { private var cache: [String: Data] = [:] func store(data: Data, forKey key: String) async { cache[key] = data } func retrieve(forKey key: String) async -> Data? { return cache[key] } }
SendableProtocol:- The
Sendableprotocol is a marker protocol in Swift 6’s concurrency model. It indicates that a type can be safely passed across concurrency domains (e.g., from oneTaskto another, or into anActor). - Value types (structs, enums) are implicitly
Sendableif their members areSendable. - Reference types (classes) are generally not
Sendableunless specifically designed to be immutable or use internal synchronization.Actors help manage mutable state safely. - Swift 6 enforces
Sendableat compile time, catching potential data races before your app even runs.
- The
Error Handling with async/await
async/await integrates beautifully with Swift’s existing throws and do-catch error handling. An async function that can throw an error is marked async throws. When you await such a function, you wrap the call in a do-catch block.
enum DataError: Error {
case networkFailed
case decodingFailed
}
func fetchRemoteData() async throws -> String {
print("Fetching remote data...")
try await Task.sleep(for: .seconds(1)) // Simulate network delay
let shouldFail = Bool.random()
if shouldFail {
throw DataError.networkFailed // Simulate network error
}
return "Hello from the server!"
}
func processData() async {
do {
let data = try await fetchRemoteData()
print("Successfully fetched: \(data)")
} catch {
print("Failed to fetch data: \(error)")
}
}
Step-by-Step Implementation: Fetching Data with Async/Await (SwiftUI)
Let’s build a simple SwiftUI app that fetches a message asynchronously and displays it. We’ll simulate a network request.
Prerequisites: Xcode 16 (or later) for Swift 6 support. Create a new “iOS” > “App” project, choose “SwiftUI” for Interface and “Swift” for Language. Name it ConcurrencyDemo.
Step 1: Basic SwiftUI View Setup
Open ContentView.swift. We’ll start with a simple UI that has a text label and a button.
// ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var message: String = "Tap the button to load data!"
@State private var isLoading: Bool = false
var body: some View {
VStack {
Text(message)
.font(.title2)
.padding()
if isLoading {
ProgressView() // Shows a loading spinner
}
Button("Load Async Data") {
// We'll add our async task here soon!
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
.disabled(isLoading) // Disable button while loading
}
}
}
#Preview {
ContentView()
}
Explanation:
@State private var message: This will hold the text we display to the user.@State private var isLoading: A boolean to control ourProgressViewand button state.VStack: Arranges our elements vertically.ProgressView(): A standard SwiftUI loading indicator.Button: Our trigger for the asynchronous operation. It’s disabled whenisLoadingis true.
Step 2: Create an async Function to Simulate Data Fetching
Now, let’s create a function that mimics fetching data from a network. We’ll make it async and throws to demonstrate error handling.
Add this enum and async function outside of the ContentView struct, perhaps as a top-level function or inside a dedicated data manager class (which we might explore in later chapters). For now, a top-level function is fine for demonstration.
// Add this above or below your ContentView struct
enum NetworkError: Error, LocalizedError {
case invalidURL
case dataCorrupted
case unknown(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "The URL provided was invalid."
case .dataCorrupted: return "The data received was corrupted."
case .unknown(let error): return "An unknown error occurred: \(error.localizedDescription)"
}
}
}
func fetchGreetingMessage() async throws -> String {
print("Beginning fetchGreetingMessage...")
// Simulate a network delay
try await Task.sleep(for: .seconds(2)) // Suspends execution for 2 seconds
// Simulate potential network failure
let shouldFail = Int.random(in: 1...10) == 7 // 10% chance to fail
if shouldFail {
print("Simulating network failure!")
throw NetworkError.networkFailed
}
print("Fetch complete!")
return "Hello from Swift 6 Concurrency!"
}
Explanation:
enum NetworkError: A custom error type to make our error handling more specific. It conforms toLocalizedErrorfor user-friendly messages.func fetchGreetingMessage() async throws -> String:async: This function will perform asynchronous work.throws: This function can throw errors.try await Task.sleep(for: .seconds(2)): This line is super important! It simulates a delay without blocking the main thread. Theawaitkeyword tells Swift to pause this function’s execution here until thesleepis done, but the thread it was running on is free to do other things.try await: SinceTask.sleepcan throwCancellationError, we needtry.- The
shouldFaillogic gives us a chance to see error handling in action.
Step 3: Call the async Function from a Task
Now, let’s connect our async function to the button in ContentView.
Modify the Button’s action closure:
// ContentView.swift - inside ContentView struct, modify the Button
Button("Load Async Data") {
// Start a new Task to run our async function
Task {
isLoading = true // Show loading indicator
do {
let fetchedMessage = try await fetchGreetingMessage() // Await the result
message = fetchedMessage // Update UI with result
} catch {
message = "Error: \(error.localizedDescription)" // Show error message
}
isLoading = false // Hide loading indicator
}
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
.disabled(isLoading)
Explanation:
Task { ... }: This creates a new asynchronous task. The code inside thisTaskblock will run on a background thread managed by Swift’s concurrency system.isLoading = true: We immediately setisLoadingto true. BecauseisLoadingis a@Statevariable, SwiftUI will automatically re-render the view, showing theProgressViewand disabling the button.do { ... } catch { ... }: We use ado-catchblock becausefetchGreetingMessage()is markedthrows.let fetchedMessage = try await fetchGreetingMessage(): This is the magic! We call ourasync throwsfunction. Theawaitkeyword means this line will pause untilfetchGreetingMessage()returns (or throws). While it’s paused, the UI remains fully responsive!message = fetchedMessage: Once the data is fetched successfully, we update our@Statevariable. SwiftUI updates theTextview automatically.isLoading = false: Finally, we hide the loading indicator.
Run your app! Tap the “Load Async Data” button. You’ll see the loading spinner, the button will disable, and after 2 seconds, either the success message or an error message will appear. Your UI remains responsive throughout!
Step 4: Using async let for Parallel Fetching
What if you need to fetch multiple pieces of data independently and then combine them? async let is perfect for this. Let’s add another async function and use async let.
First, add another async function (e.g., for fetching a number) outside ContentView:
// Add this above or below your ContentView struct
func fetchLuckyNumber() async throws -> Int {
print("Beginning fetchLuckyNumber...")
try await Task.sleep(for: .seconds(1.5)) // Simulate another delay
let shouldFail = Int.random(in: 1...10) == 3
if shouldFail {
print("Simulating number fetch failure!")
throw NetworkError.dataCorrupted
}
print("Number fetch complete!")
return Int.random(in: 100...999)
}
Now, modify ContentView to use async let when the button is tapped:
// ContentView.swift - inside ContentView struct, modify the Button
Button("Load Async Data") {
Task {
isLoading = true
do {
// Start both fetches concurrently!
async let greeting = fetchGreetingMessage()
async let number = fetchLuckyNumber()
// Await both results. This line will wait until BOTH 'greeting' and 'number' are available.
let fetchedGreeting = try await greeting
let fetchedNumber = try await number
message = "\(fetchedGreeting) Your lucky number is: \(fetchedNumber)"
} catch {
message = "Error fetching combined data: \(error.localizedDescription)"
}
isLoading = false
}
}
.padding()
.background(Color.green) // Changed color for distinction
.foregroundColor(.white)
.cornerRadius(8)
.disabled(isLoading)
Explanation:
async let greeting = fetchGreetingMessage(): This starts thefetchGreetingMessage()operation immediately in the background without waiting.async let number = fetchLuckyNumber(): This startsfetchLuckyNumber()immediately as well, concurrently with the greeting fetch.let fetchedGreeting = try await greeting: This line awaits the result of thegreetingtask. Ifgreetingis already done, it proceeds immediately. If not, it waits.let fetchedNumber = try await number: This line awaits the result of thenumbertask.- Because both
async letoperations started concurrently, the total time taken will be roughly the duration of the longest running task, not the sum of their durations. This is the power of parallel execution!
Run the app again. Notice how both operations run in parallel. If one fails, the entire do block will throw an error.
Step 5: Introducing an Actor for Data-Race Safety (Swift 6)
Actors are essential for safely managing mutable state shared across concurrent tasks, preventing data races. Let’s create a simple cache Actor.
Create a new Swift file named MessageCache.swift (File > New > File… > Swift File).
// MessageCache.swift
import Foundation
actor MessageCache {
private var cachedMessage: String?
private var cachedNumber: Int?
func store(message: String, number: Int) {
self.cachedMessage = message
self.cachedNumber = number
print("Cache: Stored message and number.")
}
func retrieve() -> (message: String?, number: Int?) {
print("Cache: Retrieving message and number.")
return (cachedMessage, cachedNumber)
}
func clear() {
cachedMessage = nil
cachedNumber = nil
print("Cache: Cleared.")
}
}
Explanation:
actor MessageCache: By declaring this as anactor, Swift guarantees that any access to its mutable properties (cachedMessage,cachedNumber) or methods (store,retrieve,clear) will be isolated.- When you call a method on an
actorfrom outside its isolation domain, you mustawaitit. This allows theactorto process one request at a time, preventing multiple tasks from modifying its internal state simultaneously.
Now, let’s integrate this MessageCache into our ContentView.
// ContentView.swift - inside ContentView struct
struct ContentView: View {
@State private var message: String = "Tap to load or retrieve from cache!"
@State private var isLoading: Bool = false
// Create an instance of our actor
private let messageCache = MessageCache() // Actors are reference types
var body: some View {
VStack {
Text(message)
.font(.title2)
.padding()
if isLoading {
ProgressView()
}
// Load data button
Button("Load & Cache Data") {
Task {
isLoading = true
do {
async let greeting = fetchGreetingMessage()
async let number = fetchLuckyNumber()
let fetchedGreeting = try await greeting
let fetchedNumber = try await number
// Store in the actor's cache (await is implicit here if called from a non-actor context)
await messageCache.store(message: fetchedGreeting, number: fetchedNumber) // Must await actor calls
message = "Loaded & Cached: \(fetchedGreeting) | Lucky: \(fetchedNumber)"
} catch {
message = "Error loading & caching: \(error.localizedDescription)"
}
isLoading = false
}
}
.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(8)
.disabled(isLoading)
// Button to retrieve from cache
Button("Retrieve from Cache") {
Task {
// Accessing actor methods requires await
let (cachedMsg, cachedNum) = await messageCache.retrieve()
if let msg = cachedMsg, let num = cachedNum {
message = "From Cache: \(msg) | Lucky: \(num)"
} else {
message = "Cache is empty!"
}
}
}
.padding()
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(8)
.disabled(isLoading) // Still disable if other tasks are loading
Button("Clear Cache") {
Task {
await messageCache.clear() // Clear cache
message = "Cache cleared!"
}
}
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(8)
.disabled(isLoading)
}
}
}
Explanation:
private let messageCache = MessageCache(): We instantiate ouractor.await messageCache.store(...): When calling methods on anactorfrom outside its ownactorcontext (like from ourContentView’sTask), you must useawait. This is Swift’s way of ensuring that access to theactor’s state is serialized and safe.- Similarly,
await messageCache.retrieve()andawait messageCache.clear()ensure safe access.
Run the app. First, tap “Load & Cache Data”. Then, tap “Retrieve from Cache”. You’ll see the cached data appear instantly without the 2-second delay. Tap “Clear Cache” to empty it. This demonstrates how Actors provide a safe, structured way to manage shared mutable state in a concurrent environment.
Mini-Challenge: Concurrent Image Download with TaskGroup
Challenge: Modify the ContentView to download three placeholder images from different URLs concurrently using a TaskGroup. Display these images in an HStack once all of them have finished downloading. If any download fails, display an error message.
Hints:
- You’ll need
URLSession.shared.data(from:)which is anasync throwsfunction. - You’ll need
UIImage(fromUIKit) andImage(fromSwiftUI) to display the images. - A
TaskGroupis created withawait withTaskGroup(of: Data.self) { group in ... }. - Inside the
TaskGroup, usegroup.addTask { ... }for each image download. - Collect results with
for await data in group { ... }. - Remember to update
@Statevariables on the main actor.
What to observe/learn: How TaskGroup allows you to manage a dynamic number of concurrent tasks and collect their results efficiently, waiting for all to complete before proceeding.
Stuck? Click for a hint!
You'll need an `@State` array of `Image?` to store the downloaded images. Remember to convert `Data` to `UIImage` and then to `Image(uiImage: ...)`.
Common Pitfalls & Troubleshooting
Blocking the Main Thread:
- Pitfall: Performing any long-running operation (network requests, heavy computations, disk I/O) directly on the main thread.
- Symptom: Your UI freezes, animations stop, buttons don’t respond. Xcode’s
Main Thread Checker(enabled by default) will often warn you about this. - Fix: Always offload such tasks to background threads using
Task { ... }orDispatchQueue.global().async { ... }. Ensure UI updates are explicitly dispatched back to the main actor usingawait MainActor.run { ... }(orDispatchQueue.main.async { ... }for GCD).
Data Races (Swift 6’s Superpower):
- Pitfall: Multiple threads or tasks trying to access and modify the same piece of mutable shared data simultaneously without proper synchronization.
- Symptom: Unpredictable crashes, incorrect data, subtle bugs that are hard to reproduce. In Swift 6, the compiler will often prevent these at compile time!
- Fix:
- Use
Actors to encapsulate and protect mutable state. All access to an actor’s state is serialized. - Ensure types passed between tasks are
Sendable(value types are often implicitlySendable). - Minimize shared mutable state. Prefer immutable data structures.
- Use
Forgetting
awaitforActorCalls:- Pitfall: Calling a method on an
actorinstance from a non-actor context without usingawait. - Symptom: Compile-time error in Swift 6: “Call to actor-isolated instance method ‘…’ in a non-isolated context; consider using ‘await’”.
- Fix: Simply add
awaitbefore the actor method call, e.g.,await myActor.doSomething().
- Pitfall: Calling a method on an
Ignoring Task Cancellation:
- Pitfall: Starting long-running tasks but not handling their cancellation. If a user navigates away, the task might continue running and wasting resources.
- Symptom: Wasted CPU/network, potential crashes if a task tries to update a UI element that no longer exists.
- Fix:
async/awaittasks are cooperatively cancellable. Inside yourasyncfunctions, periodically checkTask.isCancelledor calltry Task.checkCancellation()if your task performs cancellable work (likeTask.sleeporURLSessiondata tasks). When a parentTaskis cancelled, its children are also cancelled.
Summary
Congratulations! You’ve navigated the exciting world of concurrency in iOS development. Here’s a quick recap of the key takeaways:
- Concurrency is vital for building responsive, efficient, and user-friendly iOS applications by performing multiple tasks without freezing the UI.
- The main thread is for UI updates and user interaction; never block it with long-running operations.
- Grand Central Dispatch (GCD) is Apple’s foundational framework for managing concurrent tasks using
DispatchQueues (serial and concurrent). - Structured concurrency with
async/await(Swift 5.5+, matured in Swift 6) is the modern, preferred way to write asynchronous code in Swift. It offers improved readability, safety, and error handling. - Key
async/awaitcomponents include:asyncfunctions to declare asynchronous work.awaitto pause execution until anasyncfunction completes.Taskas the fundamental unit of asynchronous work.async letfor running a fixed number of tasks in parallel.TaskGroupfor managing a dynamic number of concurrent tasks.Actors for safely managing mutable shared state, preventing data races.Sendableprotocol to mark types safe for concurrent sharing, strictly enforced by Swift 6.
- Error handling with
async/awaitseamlessly uses Swift’sdo-catchblocks. - Always be mindful of common pitfalls like blocking the main thread and data races, leveraging Swift 6’s powerful compiler checks to catch issues early.
You now have the tools to make your apps not just functional, but also incredibly smooth and responsive. This skill is critical for any professional iOS developer!
Next, we’ll continue our journey into more advanced topics, potentially looking into advanced networking patterns or deeper into app architecture, where these concurrency patterns will be crucial.
References
- Concurrency in Swift - Apple Developer Documentation
- Meet async/await in Swift - WWDC21
- Actors in Swift - Apple Developer Documentation
- Grand Central Dispatch - Apple Developer Documentation
- Swift Evolution Proposal SE-0302: Sendable and @Sendable closures
- Swift Evolution Proposal SE-0338: Clarify the Execution of Non-Actor Asynchronous Functions
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.