| name | Swift Concurrency |
| user-invocable | false |
| description | Use when swift's modern concurrency model including async/await, actors, task groups, structured concurrency, and async sequences for building safe, performant concurrent code without data races or callback pyramids. |
| allowed-tools | [] |
Swift Concurrency
Introduction
Swift's modern concurrency model provides structured, safe concurrent
programming through async/await syntax, actors for data isolation, and task
management primitives. This system eliminates common concurrency bugs like data
races and callback hell while improving code readability and maintainability.
Introduced in Swift 5.5, the concurrency model integrates with the language's
type system to enforce safety at compile time. Actors protect mutable state,
async/await makes asynchronous code look synchronous, and structured
concurrency ensures tasks are properly managed and cancelled.
This skill covers async functions, actors, task groups, cancellation, async
sequences, and patterns for migrating from completion handlers to modern
concurrency.
Async/Await Fundamentals
Async/await syntax enables writing asynchronous code that reads like synchronous
code, without nested callbacks or complex error handling chains.
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 loadUserProfile() async {
do {
let user = try await fetchUser(id: 42)
print("Loaded user: \(user.name)")
} catch {
print("Failed to load user: \(error)")
}
}
func loadFullProfile() async throws -> Profile {
let user = try await fetchUser(id: 42)
let posts = try await fetchPosts(userId: user.id)
let comments = try await fetchComments(userId: user.id)
return Profile(user: user, posts: posts, comments: comments)
}
func loadProfileParallel() async throws -> Profile {
async let user = fetchUser(id: 42)
async let posts = fetchPosts(userId: 42)
async let comments = fetchComments(userId: 42)
return try await Profile(
user: user,
posts: posts,
comments: comments
)
}
struct UserRepository {
var currentUser: User {
get async throws {
return try await fetchUser(id: getCurrentUserId())
}
}
}
func displayUser(repo: UserRepository) async {
do {
let user = try await repo.currentUser
print(user.name)
} catch {
print("Error: \(error)")
}
}
class DataManager {
let data: Data
init() async throws {
let url = URL(string: "https://api.example.com/config")!
let (data, _) = try await URLSession.shared.data(from: url)
self.data = data
}
}
Async functions suspend execution at await points, allowing other work to
proceed without blocking threads. The runtime manages suspension and resumption
efficiently.
Actors for Safe Concurrency
Actors protect mutable state from concurrent access, preventing data races by
ensuring only one task can access actor-isolated state at a time.
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
func useCounter() async {
let counter = Counter()
await counter.increment()
let value = await counter.getValue()
print("Counter: \(value)")
}
actor ImageCache {
private var cache: [URL: Image] = [:]
func image(for url: URL) async throws -> Image {
if let cached = cache[url] {
return cached
}
let (data, _) = try .shared.data(from: url)
image (data: data) {
.invalidData
}
cache[url] image
image
}
() {
cache.removeAll()
}
}
: {
invalidData
}
{
(: ) {
}
}
: {
users: [] []
() {
{
fetchedUsers fetchAllUsers()
users fetchedUsers
} {
()
}
}
}
() -> [] {
[]
}
{
connection: ?
( : ) -> {
query.trimmingCharacters(in: .whitespaces)
}
( : ) {
connection connection {
.notConnected
}
}
}
{}
: {
notConnected
}
{
shared ()
}
{
table:
() -> {
}
}
Actors automatically serialize access to their state, eliminating data races
while maintaining code clarity and avoiding manual lock management.
Structured Concurrency with Task Groups
Task groups enable spawning multiple concurrent tasks with automatic lifecycle
management and result collection.
func fetchMultipleUsers(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
}
}
func loadImages(urls: [URL]) async -> [Image] {
await withTaskGroup(of: Image?.self) { group in
for url in urls {
group.addTask {
try? await downloadImage(from: url)
}
}
var images: [Image] = []
for await image in group {
if let image = image {
images.append(image)
}
}
images
}
}
( : ) -> {
(data, ) .shared.data(from: url)
image (data: data) {
.invalidData
}
image
}
<>(
: [],
: ,
: () ->
) {
withThrowingTaskGroup(of: .) { group
index
(maxConcurrent, items.count) {
item items[index]
group.addTask {
process(item)
}
index
}
index items.count {
group.next()
item items[index]
group.addTask {
process(item)
}
index
}
group.waitForAll()
}
}
(: []) -> [: ] {
withThrowingTaskGroup(
of: (, ).
) { group
id userIds {
group.addTask {
data loadUserData(id: id)
(id, data)
}
}
results: [: ] [:]
(id, data) group {
results[id] data
}
results
}
}
{}
(: ) -> {
()
}
(: []) -> ? {
withTaskGroup(of: ?.) { group
item items {
group.addTask {
checkMatch(item)
}
}
result group {
match result {
group.cancelAll()
match
}
}
}
}
( : ) -> ? {
item.count item :
}
Task groups provide structured concurrency: all child tasks complete or are
cancelled before the group exits, preventing task leaks.
Task Management and Cancellation
Tasks represent units of asynchronous work with lifecycle management,
cancellation support, and priority configuration.
func backgroundUpdate() {
Task.detached(priority: .background) {
await performHeavyComputation()
}
}
func performHeavyComputation() async {
}
class DataLoader {
private var loadTask: Task<Data, Error>?
func startLoading() {
loadTask = Task {
try await loadData()
}
}
func cancelLoading() {
loadTask?.cancel()
loadTask = nil
}
func loadData() async throws -> Data {
let url = URL(string: "https://api.example.com/data")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
func processLargeDataset(items: [Item]) {
item items {
.checkCancellation()
process(item)
}
}
{}
( : ) {
}
(: ) -> {
(bytes, response) .shared.bytes(from: url)
data ()
byte bytes {
.isCancelled {
()
}
data.append(byte)
}
data
}
{
high, medium, low
priority: {
{
.high: .high
.medium: .medium
.low: .low
}
}
}
(: , : () -> ) {
(priority: priority.priority) {
work()
}
}
() -> {
task <, > {
fetchUser(id: )
}
task.value
}
{
() {
{
data fetchData()
updateUI(with: data)
}
}
() -> {
()
}
( : ) {
}
}
Tasks automatically inherit priority, task-local values, and actor context from
their creation site, ensuring proper execution environment.
Async Sequences
Async sequences provide asynchronous iteration over values that arrive over
time, enabling clean handling of streams, events, and paginated data.
struct AsyncCountdown: AsyncSequence {
typealias Element = Int
let start: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
mutating func next() async -> Int? {
guard current >= 0 else { return nil }
let value = current
current -= 1
try? await Task.sleep(nanoseconds: 1_000_000_000)
return value
}
}
func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(current: start)
}
}
func countDown() async {
let countdown = AsyncCountdown(start: 5)
for await number in countdown {
(number)
}
}
(: []) {
(bytes, ) .shared.bytes(
from: urls[]
)
byte bytes {
processByte(byte)
}
}
( : ) {
}
() -> <> {
{ continuation
sensor ()
sensor.onReading { temperature
continuation.yield(temperature)
}
continuation.onTermination {
sensor.stop()
}
sensor.start()
}
}
{
onReading: (() -> )
() {}
() {}
}
() {
temp temperatures() {
()
temp {
}
}
}
() {
temps temperatures()
temp temps temp {
()
}
}
() -> <, > {
{ continuation
monitor ()
monitor.onEvent { event
continuation.yield(event)
}
monitor.onError { error
continuation.finish(throwing: error)
}
monitor.start()
}
}
{}
{
onEvent: (() -> )
onError: (() -> )
() {}
}
Async sequences integrate with for-await-in loops and support transformation,
filtering, and composition like synchronous sequences.
Best Practices
-
Use async/await instead of completion handlers to improve readability and
avoid callback pyramids in new code
-
Protect mutable state with actors rather than manual locks to prevent
data races with compile-time guarantees
-
Prefer structured concurrency with task groups over detached tasks to
ensure proper lifecycle management and cancellation
-
Check Task.isCancelled in long-running operations to enable cooperative
cancellation and resource cleanup
-
Use MainActor for UI code to ensure UI updates happen on the main thread
without explicit dispatch calls
-
Leverage async let for parallel execution when multiple independent async
operations can run concurrently
-
Employ async sequences for streams instead of callbacks or delegates when
handling values that arrive over time
-
Mark nonisolated methods appropriately on actors when they don't access
isolated state to avoid unnecessary awaits
-
Set task priorities explicitly for background work to prevent priority
inversion and ensure responsive UI
-
Use withTaskCancellationHandler to clean up resources immediately when
tasks are cancelled rather than waiting for checkpoints
Common Pitfalls
-
Blocking main thread with await in synchronous contexts causes hangs;
create Task wrappers to bridge to async code
-
Not checking for cancellation in long operations wastes resources and
delays response to user actions
-
Creating retain cycles with unowned/weak incorrectly in async closures
can cause crashes or memory leaks
-
Mixing async/await with completion handlers improperly creates race
conditions and difficult-to-debug behavior
-
Overusing detached tasks loses structured concurrency benefits like
automatic cancellation and priority inheritance
-
Forgetting await on actor methods causes compilation errors since actor
isolation requires suspension points
-
Not using throwing variants of task APIs when errors are possible leads
to silent failures and unhandled errors
-
Accessing actor state without isolation by making properties public
defeats the purpose of actor protection
-
Creating too many concurrent tasks without limits exhausts system
resources and degrades performance
-
Assuming immediate execution after await; other tasks may run before
continuation, breaking assumptions about state
When to Use This Skill
Use Swift concurrency when building iOS 15+, macOS 12+, watchOS 8+, or tvOS 15+
applications that perform asynchronous operations like networking, file I/O, or
background processing.
Apply async/await when working with URLSession, Core Data async methods, or any
API that supports modern concurrency instead of completion handlers.
Employ actors when managing shared mutable state accessed from multiple
concurrent contexts, especially in data managers, caches, or repositories.
Leverage task groups when performing multiple independent async operations that
should be coordinated, like fetching data for multiple users or processing a
batch of items concurrently.
Use async sequences when handling streams of data from sensors, network
connections, file reading, or any source that produces values over time.
Resources