Write asynchronous code that reads sequentially with async/await, protect shared state with actors, and understand how ARC manages memory — including the weak references that break retain cycles.
Modern Swift concurrency lets you write asynchronous code that reads top to bottom instead of nested callbacks. An async function can suspend at an await without blocking a thread, and callers await its result. This replaces completion-handler pyramids with linear code, and it is how you make network calls in current iOS apps.
func fetchUser() async throws -> String {
// suspends here without blocking; resumes when the data arrives
let (data, _) = try await URLSession.shared.data(from: url)
return String(decoding: data, as: UTF8.self)
}
func load() async {
do {
let user = try await fetchUser() // reads sequentially, non-blocking
print("Got \(user)")
} catch {
print("Failed: \(error)")
}
}
// no callbacks, no nesting — async code that reads like sync code.Swift runs async work in Tasks, and structured concurrency ties their lifetimes together — using async let, two operations run concurrently and you await both. A Task can be cancelled, and child tasks cancel with their parent, preventing leaked work. This gives you parallelism and clean cancellation without manual thread management.
func loadProfile() async -> String { "profile" }
func loadPosts() async -> String { "posts" }
func loadAll() async -> String {
// 'async let' starts both CONCURRENTLY, then awaits both
async let profile = loadProfile()
async let posts = loadPosts()
return await "\(profile) + \(posts)" // ~max(the two), not the sum
}
// a Task bridges sync -> async (e.g. from a button tap):
Task {
let result = await loadAll()
print(result)
} // cancelling the Task cancels its child workData races — two tasks touching the same mutable state at once — are a classic concurrency bug. An actor serializes access to its state: only one task runs its code at a time, so its properties are safe to mutate concurrently. You interact with an actor using await. Actors let you share mutable state across tasks without manual locks.
actor BankAccount {
private var balance = 0
func deposit(_ amount: Int) { balance += amount } // isolated — one at a time
func getBalance() -> Int { balance }
}
let account = BankAccount()
Task {
await account.deposit(100) // 'await' to touch actor state
print(await account.getBalance()) // 100 — no data race possible
}
// the actor serializes access to 'balance' — safe concurrent mutation, no locks.Swift manages memory with Automatic Reference Counting: an object lives while something holds a strong reference and is freed when the count hits zero. The pitfall is a retain cycle — two objects (or a closure and its owner) holding each other strongly, so neither is freed. You break cycles by making one reference weak (or unowned), the single most important memory concept in iOS.
Break cycles with weak required — Closures capturing self strongly are the most common leak in iOS. Use [weak self] in escaping closures that self owns, then unwrap self, to avoid a retain cycle.class ViewModel {
var onDone: (() -> Void)?
func setup() {
// BUG: the closure captures self strongly; self holds the closure -> cycle
// onDone = { self.finish() }
// FIX: capture self weakly so the cycle can't form
onDone = { [weak self] in
self?.finish()
}
}
func finish() { print("done") }
}
// ARC frees objects at refcount 0. A strong cycle keeps them alive forever
// (a leak). [weak self] in escaping closures is the everyday fix.