← All lessonsCheat sheet · Expansions
Cheat sheet

Expansions

What each piece of syntax really is, written out

SwiftUI and Swift concurrency are full of syntax that stands for something longer: a property wrapper for a hidden property and an accessor, await for a function cut in two, @concurrent for a hop to the thread pool and back. The expansion is the definition. This sheet collects every one the course teaches, each shown twice — as you write it, and as the compiler makes it — so that there is one place to come back to. Where the expansion is Xcode's own, it was read from the compiler's macro expansion; where it is written out by hand, a flash card runs the hand-written version against the sugar and checks they do the same thing.

1 · from lesson 2

A property wrapper

As you write it
struct Pet {
    @ClampedToTen var food: Int
}

Written out: the wrapper is stored in a hidden property named with an underscore, and the property you declared becomes a computed property over it.

What the compiler makes of it
struct HandWrittenPet {
    private var _food = ClampedToTen()        // the hidden storage: the wrapper itself

    var food: Int {
        get {
            _food.wrappedValue
        }
        set {
            _food.wrappedValue = newValue
        }
    }
}

@ClampedToTen var food is _food plus food. $food, where a wrapper offers one, is _food.projectedValue. Lesson 2 builds the wrapper, and this expansion, by hand.

2 · from lesson 2

@State

As you write it: @State and @Binding in a view
import SwiftUI

struct Counter: View {
    @State private var count = 0
    @Binding var name: String

    var body: some View {
        Text("\(name): \(count)")
    }
}

Since Xcode 27 @State is a macro, so Xcode can show you what it writes (Editor ▸ Expand Macro). Shortened:

Glance.swift — from Xcode’s expansion of the macro (shortened), not code to type
struct Counter: View {
    @State private var count = 0
    {
        get {
            __count.wrappedValue
        }
        nonmutating set {
            __count.wrappedValue = newValue
        }
    }

    private var __count = SwiftUICore.State._makeStorage({ 0 })

    @SwiftUICore._StateProjectedValue
    private var $count = SwiftUICore.State(initialValue: 0).projectedValue
    {
        get {
            __count.projectedValue
        }
    }

    // …and `_count`, and a private static that holds the starting value.

    @Binding var name: String

    var body: some View {
        Text("\(name): \(count)")
    }
}

The same shape as the hand-written wrapper above: hidden storage, a computed property over it with a nonmutating setter (that is how a view, an immutable struct, can change its state — the state lives outside it), and $count as the storage's projected value, which is a Binding. What the macro adds, and ClampedToTen could not, is that the storage tells SwiftUI when the value changes.

3 · from lesson 2

@Binding and $

As you write it
import SwiftUI

struct Parent: View {
    @State private var name = "Pip"

    var body: some View {
        Child(name: $name)
    }
}

struct Child: View {
    @Binding var name: String

    var body: some View {
        Button("Shout") {
            name = name.uppercased()
        }
    }
}

Written out: a binding is two closures — a getter and a setter — passed together. Lesson 2 calls the hand-written version Ref:

What the compiler makes of it: the type
struct Ref<Value> {
    let get: () -> Value
    let set: (Value) -> Void

    var value: Value {
        get {
            get()
        }
        nonmutating set {
            set(newValue)
        }
    }
}
What the compiler makes of it: what $name and name = … do in Child
// $name is a Ref: a getter and a setter, passed together. Child writes through it.
let name = Ref(get: { parent.name }, set: { parent.name = $0 })
name.value = name.value.uppercased()

Apple's Binding has exactly these parts: a wrappedValue with a nonmutating set, and a projectedValue that is the binding itself, so $name on a @Binding property passes it further down.

4 · from lesson 2

@AppStorage

As you write it
import SwiftUI

struct Settings: View {
    @AppStorage("petName") private var petName = "Pip"

    var body: some View {
        Text(petName)
    }
}

Written out: a wrapper whose storage is UserDefaults — the getter reads the key, the nonmutating setter writes it. Lesson 2 builds it as Stored:

What the compiler makes of it
import Foundation

@propertyWrapper
struct Stored<Value> {
    private let key: String
    private let defaultValue: Value

    init(wrappedValue: Value, _ key: String) {
        self.key = key
        self.defaultValue = wrappedValue
    }

    var wrappedValue: Value {
        get {
            UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
        }
        nonmutating set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

One thing the hand-written version lacks, and Apple's has: when the value changes, @AppStorage tells the view to draw again. Stored saves the name and the screen does not change until the next launch; @AppStorage does both.

5 · from lesson 16

@Observable

As you write it
import Observation

/// What the feeder has seen today. On the main actor: the screen reads it.
@MainActor
@Observable
final class Sightings {
    var robins = 0
    var sparrows = 0
}

A macro, so Xcode shows its expansion. For every stored property, shortened:

Sightings.swift — from Xcode’s expansion of the macro (shortened), not code to type
@MainActor
@Observable
final class Sightings {
    @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()

    internal nonisolated func access<Member>(
      keyPath: KeyPath<Sightings, Member>
    ) {
      _$observationRegistrar.access(self, keyPath: keyPath)
    }

    internal nonisolated func withMutation<Member, MutationResult>(
      keyPath: KeyPath<Sightings, Member>,
      _ mutation: () throws -> MutationResult
    ) rethrows -> MutationResult {
      try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
    }

    // …and four private overloads of shouldNotifyObservers, which the setter calls.

    @ObservationIgnored
    private var _robins = 0

    @ObservationTracked
    var robins = 0
    {
        @storageRestrictions(initializes: _robins)
        init(initialValue) {
            _robins = initialValue
        }
        get {
            access(keyPath: \.robins)
            return _robins
        }
        set {
            guard shouldNotifyObservers(_robins, newValue) else {
                _robins = newValue
                return
            }
            withMutation(keyPath: \.robins) {
                _robins = newValue
            }
        }
        _modify {
            access(keyPath: \.robins)
            _$observationRegistrar.willSet(self, keyPath: \.robins)
            defer {
                _$observationRegistrar.didSet(self, keyPath: \.robins)
            }
            yield &_robins
        }
    }

    // `sparrows` gets the same storage and accessors.
}

extension Sightings: nonisolated Observation.Observable {
}

A registrar is added to the class; each property moves into an underscored stored property; and the property you wrote becomes accessors that tell the registrar on every read (access) and every write (withMutation, or _modify for a change made in place). That is how a view knows which properties its body read, and is redrawn when one of those — and only those — changes. Lesson 16 writes the same accessors by hand first, and then reads these.

6 · from lesson 4

A result builder: @ViewBuilder

As you write it: a block of statements, each a widget
let card = stack {
    Line("Seattle")
    Line("5 nights")
}

A result builder block is rewritten into calls on the builder, one per statement. Lesson 4's WidgetBuilder — a miniature @ViewBuilder — has two:

The builder
@resultBuilder
enum WidgetBuilder {
    static func buildPartialBlock<W: Widget>(first: W) -> W {
        first
    }

    static func buildPartialBlock<A: Widget, B: Widget>(accumulated: A, next: B) -> Column<A, B> {
        Column(accumulated, next)
    }
}
What the compiler makes of it
// the block, as the compiler rewrites it: one call on the builder per statement
let card = WidgetBuilder.buildPartialBlock(
    accumulated: WidgetBuilder.buildPartialBlock(first: Line("Seattle")),
    next: Line("5 nights")
)

A SwiftUI body is the same trick: VStack { Text("a"); Text("b") } is buildBlock(Text("a"), Text("b")), an if is buildEither, an if without else is buildOptional. The result is a nested type that spells out the whole tree — which is why body returns some View and not a named type.

7 · from lesson 37

async and await

As you write it
/// Ada flies to a flower and comes back with what she found. The flight is an `await`.
/// `@MainActor`: she leaves from the queen's actor — on purpose, to see what the wait does to it.
@MainActor func fly() async throws -> Int {
    print("Ada leaves from \(whereAmI())")
    let (data, _) = try await URLSession.shared.data(from: meadow)
    print("Ada is back on \(whereAmI())")
    return data.count
}

Written out: the compiler cuts the function in two at the await. The first half runs up to the wait and returns, so its thread is free; when the result arrives the second half runs, with the locals it needs handed along, back on the actor the first half was on.

What the compiler makes of it
import Foundation

// fly(), as the compiler builds it: two ordinary functions, and the request's completion handler between them.
// You never write this. It is what `await` means.
@MainActor func flyFirstHalf(then: @escaping @Sendable (Int) -> Void) {
    // everything before the await
    print("Ada leaves from \(whereAmI())")
    // the wait. The closure runs when the data has arrived, on some other thread —
    // so it hops back to the actor the first half was on, and runs the second half there
    URLSession.shared.dataTask(with: meadow) { data, _, _ in
        Task { @MainActor in
            flySecondHalf(data ?? Data(), then: then)
        }
    }.resume()
    // and the first half returns here: the thread is free
}

@MainActor func flySecondHalf(_ data: Data, then: (Int) -> Void) {
    // everything after the await
    print("Ada is back on \(whereAmI())")
    then(data.count)
}

A completion handler, the rest of the function inside it, and a hop back — what a 2016 iOS developer wrote by hand for every request. await is the compiler writing it. From it: a thread is never kept waiting at an await; the locals live in a frame off the thread; and the code after an await is on the actor it left.

8 · from lesson 37

@concurrent

As you write it
/// Counting every grain of pollen in the meadow: a long computation, with nothing to wait for —
/// now on the shared thread pool, so that the queen is free while it runs.
@concurrent func countPollen() async -> Int {
    var grains = 0
    for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {
        grains += 1
    }
    print("counted on \(whereAmI())")
    return grains
}

Written out: hand the body to the shared thread pool and wait for it. The caller's actor is free while you wait.

What the compiler makes of it
import Foundation

// @concurrent, written out as the Swift you could write before it existed: hand the body to the shared pool,
// and wait for it, where you are. The queen is free while you wait.
nonisolated(nonsending) func countPollenByHand() async -> Int {
    // a new task, on the shared pool, on no actor — and .value: suspend here until it is done
    await Task.detached {
        var grains = 0
        for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {
            grains += 1
        }
        print("counted on \(whereAmI())")
        return grains
    }.value
}

With one difference, which is the reason the word exists. Task.detached makes a new task — its own priority, its own cancellation, nothing of the caller's. @concurrent moves the same task to the pool and back: cancel the caller, and a @concurrent function sees it; a detached task does not. And @concurrent moves only the body you wrote: anything it calls that is main-actor code pulls it straight back to the main actor.

9 · from lesson 37

async let

async let is a declaration, and it can only be written inside an async function or closure. So the smallest complete thing that compiles is a function with the line in it, and that is the whole program here. The compiler charges every instruction it emits to one line of your source; below, each line is followed by exactly the instructions charged to it, as the compiler printed them, with only its bookkeeping removed.

As you write it
func f() async -> Int {  // stands for anything you could call
    1
}

func t() async {
    async let x = f()
}
PartWhat can go there
xWhat can go thereA new constant — async var is an error. It may carry a type, async let x: Int = f(); be a tuple, async let (n, s) = pair(); be _; or be several in one line, async let a = f(), b = g().
f()What can go thereAny expression with a value: a call to an async function, to a throwing one (try is optional here), to a plain function, a closure call, arithmetic, even 5. An await inside is allowed and not needed. It cannot be left out.
func t() async {What can go thereThe line can only sit inside an async function or closure — a Task { } body counts. In a plain function it is an error, and so is an async let stored property.
await xWhat can go thereEvery read is await x — try await x if f() can throw. The first read waits only if the slot is still empty; later reads take the value again. A read without await is a warning in the Swift 5 mode and an error in Swift 6.

The compiler's words for the ones that are not allowed:

func t() async { — what the block compiles to
%0 = metatype $@thick MainActor.Type
// function_ref static MainActor.shared.getter
%1 = function_ref @$sScM6sharedScMvgZ : $@convention(method) (@thick MainActor.Type) -> @owned MainActor
%2 = apply %1(%0) : $@convention(method) (@thick MainActor.Type) -> @owned MainActor
hop_to_executor %2

A function in a new project is main-actor code, async or not, so the block begins by fetching the main actor and hopping onto it. Every wait inside the function will hop back here. That is all the block adds.

async let x = f() — what the line compiles to
%4 = alloc_stack $Int
%5 = address_to_pointer %4 to $Builtin.RawPointer
%6 = enum $Optional<Builtin.RawPointer>, #Optional.none!enumelt
// function_ref implicit closure #1 in t()
%7 = function_ref @$s7program1tyyYaFSiyYaYbcfu_ : $@convention(thin) @Sendable @async @substituted <τ_0_0> () -> (@out τ_0_0, @error any Error) for <Int>
%8 = convert_function %7 to $@convention(thin) @async @substituted <τ_0_0> () -> (@out τ_0_0, @error any Error) for <Int>
%9 = thin_to_thick_function %8 to $@noescape @async @callee_guaranteed @substituted <τ_0_0> () -> (@out τ_0_0, @error any Error) for <Int>
%10 = builtin "startAsyncLetWithLocalBuffer"<Int>(%6, %9, %5) : $Builtin.RawPointer

Three things. alloc_stack $Int: a slot for the result, in t()'s own frame. implicit closure #1 in t(): the right-hand side, f(), has been turned into a function of its own, shown next. startAsyncLetWithLocalBuffer: start a child task of the current task running that function (%9) and writing into that slot (%5), with no options (%6), and carry on — one level further down it is a single call into the Swift runtime, swift_asyncLet_begin. Nothing here waits, and x is not an Int: it is the slot and the child. That is why the line has no await.

f() — the right-hand side, as the function it became
%2 = enum $Optional<any Actor>, #Optional.none!enumelt
// function_ref f()
%3 = function_ref @$s7program1fSiyYaF : $@convention(thin) @async () -> Int
%4 = apply %3() : $@convention(thin) @async () -> Int
hop_to_executor %2
store %4 to %0
%7 = tuple ()
return %7

This is what the child runs: on no actor — the Optional<any Actor>.none it hops to is the shared pool — call f(); f is main-actor code, so that call goes to the main actor and comes back; hop back to no actor; store the answer in the slot. So async let does not by itself move work off the main actor. The expression runs wherever the things it calls live.

} — what the closing brace compiles to
%11 = builtin "finishAsyncLet"(%10, %5) : $()
hop_to_executor %2
strong_release %2
%14 = builtin "endAsyncLetLifetime"(%10) : $()
dealloc_stack %4
%16 = tuple ()
return %16

The brace carries the rest. finishAsyncLet: if x was never read, cancel the child; wait for it to end; discard the result. A hop back onto the main actor, because that wait was a suspension. endAsyncLetLifetime, and the slot is freed. Nothing you can write puts instructions on a closing brace, which is why async let is a keyword and not a function.

So the one line is: a slot, the right-hand side as a function, start it now. The read, await x, is another line. The cancel-and-wait is the brace's.

10 · from lesson 37

An actor method with an await in it

As you write it
extension Jar {
    /// A slow pour: look at the jar, go and get the honey, come back and put one drop on top of what you saw.
    func pourSlowly() async {
        let seen = drops
        try? await Task.sleep(for: .milliseconds(10))
        drops = seen + 1
    }
}

Written out: the method is cut at the await into two jobs for the actor, and between them the actor belongs to nobody. Another task's first job can run there and see the same drops you saw.

What the compiler makes of it
// pourSlowly(), as the compiler builds it: the method is cut at the await into two jobs for the jar.
// Between them the jar is free — so another bee's first job can run, and see the same `drops` you saw.
extension Jar {
    func pourSlowlyByHand() async {
        let seen = startPour()                           // job 1, in the jar
        try? await Task.sleep(for: .milliseconds(10))    // not in the jar: anyone may pour now
        finishPour(seen)                                 // job 2, in the jar again
    }

    func startPour() -> Int {
        drops
    }

    func finishPour(_ seen: Int) {
        drops = seen + 1
    }
}

This is reentrancy, and the one place the GCD picture breaks: a block holds its serial queue to the end, an actor method holds the actor only until its next await. Lesson 37 measures it — 100 slow pours leave 1 drop. Keep a read-modify-write on one side of an await.

11 · from lesson 37

isolated and #isolation

As you write it
import Foundation

/// The actor this call is on: "MainActor", the name of an actor of yours, or "no actor". From lesson 1.
func whereAmI(_ isolation: isolated (any Actor)? = #isolation) -> String {
    isolation.map { "\(type(of: $0))" } ?? "no actor"
}

/// Whether this code is on the main thread. Only a synchronous function may ask — in `async` code the answer can
/// change at every `await`, and Swift 6 refuses the question there. On no actor: it answers for whoever calls it.
func onMainThread() -> Bool {
    Thread.isMainThread
}

Written out: #isolation is the caller's own actor, put into the argument at the call site; isolated on the parameter means the function runs on whatever actor it is handed.

What the compiler makes of it, at three call sites
// #isolation, written out: the compiler puts the caller's own actor into the argument.

/// whereAmI(), called from a view or any main-actor code.
@MainActor func whereIsHereByHand() -> String {
    whereAmI(MainActor.shared)
}

/// whereAmI(), called from a @concurrent function, as whereIsThePool() does.
@concurrent func whereIsThePoolByHand() async -> String {
    whereAmI(nil)
}

extension Jar {
    /// whereAmI(), called from inside one of the jar's methods, as whereIsTheJar() does.
    func whereIsTheJarByHand() -> String {
        whereAmI(self)
    }
}

So whereAmI() with no argument is always answered for whoever called it — and passing a different actor really runs the function there.

12 · from lesson 42

.task { }

As you write it
import SwiftUI

struct Watcher: View {
    var body: some View {
        Text("watching")
            .task {
                await watch("the .task modifier")
            }
    }
}

Written out: a task started when the view appears, kept in state, and cancelled when the view disappears.

What the compiler makes of it
import SwiftUI

struct WatcherByHand: View {
    @State private var watching: Task<Void, Never>?

    var body: some View {
        Text("watching, by hand")
            .onAppear {
                watching = Task {
                    await watch("onAppear and onDisappear")
                }
            }
            .onDisappear {
                watching?.cancel()
            }
    }
}

The modifier does the keeping and the cancelling for you, and runs the task on the main actor like the view. Task { } in onAppear without the onDisappear is the common mistake: an unstructured task that outlives its view.

13 · from lesson 37

Task { }

As you write it, in main-actor code
Task {
    print("Task { }: \(whereAmI())")
}

Written out: a new task on the shared pool that first hops to the actor of the code that created it, and keeps that code's priority. That inherited hop is why a Task { } in a view may touch @State.

What the compiler makes of it
// Task { } is a new task on the shared pool that first hops to the actor of the code that made it —
// here the main actor — and keeps that code's priority.
Task.detached(priority: Task.currentPriority) {
    await MainActor.run {
        print("Task { } by hand: \(whereAmI())")
    }
}

Task.detached { } is the version without the hop and without the inheritance: no actor, default priority, no task-local values — and, like Task { }, no parent to be cancelled by.

14 · from lesson 37

A main-actor call from the pool

As you write it: @concurrent code calls a main-actor function — the compiler makes you write await
@concurrent func fromThePool() async {
    await report()
}

Written out: that await is a hop. Run the function on the main actor and come back.

What the compiler makes of it
@concurrent func fromThePoolByHand() async {
    await MainActor.run {
        report()
    }
}

The compiler inserts the hop wherever code on one actor calls code on another; MainActor.run is the same hop spelled out. In GCD this was DispatchQueue.main.async { } — written by you, every time, and with no hop back.

Flash cards

14 cards, one for each expansion above. Each is also in the deck of the lesson it comes from. Answer in your head first, then tap the card.

Syntax · Property wrappersWhat does @ClampedToTen var food: Int expand to?
struct HandWrittenPet {
    private var _food = ClampedToTen()        // the hidden storage: the wrapper itself

    var food: Int {
        get {
            _food.wrappedValue
        }
        set {
            _food.wrappedValue = newValue
        }
    }
}

A hidden property _food holding the wrapper, and food as a computed property over its wrappedValue; $food would be _food.projectedValue. Set to 42, the sugar and the hand-written struct both printed 10 10.

Syntax · StateWhat does @State private var count = 0 expand to?

From Xcode’s expansion of the macro (shortened) — not code to type

struct Counter: View {
    @State private var count = 0
    {
        get {
            __count.wrappedValue
        }
        nonmutating set {
            __count.wrappedValue = newValue
        }
    }

    private var __count = SwiftUICore.State._makeStorage({ 0 })

    @SwiftUICore._StateProjectedValue
    private var $count = SwiftUICore.State(initialValue: 0).projectedValue
    {
        get {
            __count.projectedValue
        }
    }

    // …and `_count`, and a private static that holds the starting value.

    @Binding var name: String

    var body: some View {
        Text("\(name): \(count)")
    }
}

Xcode's own expansion of the macro: hidden storage __count, made with State._makeStorage; count as a computed property over it with a nonmutating set; and $count as the storage's projectedValue — a Binding. The storage also tells SwiftUI when the value changes, which no hand-written wrapper can.

Syntax · BindingWhat is a Binding, written out?
struct Ref<Value> {
    let get: () -> Value
    let set: (Value) -> Void

    var value: Value {
        get {
            get()
        }
        nonmutating set {
            set(newValue)
        }
    }
}

A getter and a setter, passed together: access to a value that lives somewhere else. Writing through the hand-written Ref changed the original — it printed PIP. $name on a @State is one of these, and a @Binding property's own $name is itself.

Syntax · AppStorageWhat does @AppStorage("petName") var petName = "Pip" expand to?
    var wrappedValue: Value {
        get {
            UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
        }
        nonmutating set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }

A wrapper whose storage is UserDefaults: the getter reads the key (or the default), the nonmutating setter writes it. A second Preferences read what the first had set: Biscuit. Apple's version adds the one thing this lacks — it tells the view to draw again.

Syntax · ObservationWhat does @Observable write into a class?
import Observation

/// What the feeder has seen today. On the main actor: the screen reads it.
@MainActor
@Observable
final class Sightings {
    var robins = 0
    var sparrows = 0
}

From Xcode’s expansion of the macro (shortened) — not code to type

@MainActor
@Observable
final class Sightings {
    @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()

    internal nonisolated func access<Member>(
      keyPath: KeyPath<Sightings, Member>
    ) {
      _$observationRegistrar.access(self, keyPath: keyPath)
    }

    internal nonisolated func withMutation<Member, MutationResult>(
      keyPath: KeyPath<Sightings, Member>,
      _ mutation: () throws -> MutationResult
    ) rethrows -> MutationResult {
      try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
    }

    // …and four private overloads of shouldNotifyObservers, which the setter calls.

    @ObservationIgnored
    private var _robins = 0

    @ObservationTracked
    var robins = 0
    {
        @storageRestrictions(initializes: _robins)
        init(initialValue) {
            _robins = initialValue
        }
        get {
            access(keyPath: \.robins)
            return _robins
        }
        set {
            guard shouldNotifyObservers(_robins, newValue) else {
                _robins = newValue
                return
            }
            withMutation(keyPath: \.robins) {
                _robins = newValue
            }
        }
        _modify {
            access(keyPath: \.robins)
            _$observationRegistrar.willSet(self, keyPath: \.robins)
            defer {
                _$observationRegistrar.didSet(self, keyPath: \.robins)
            }
            yield &_robins
        }
    }

    // `sparrows` gets the same storage and accessors.
}

extension Sightings: nonisolated Observation.Observable {
}

Xcode's own expansion: an ObservationRegistrar; each stored property moved into an underscored one; and the property you wrote turned into accessors that tell the registrar on every read (access) and every write (withMutation). A view's body is tracked through those reads, and redrawn when one of the properties it read changes.

Syntax · Result buildersWhat does a @ViewBuilder block — two statements — expand to?
let byHand = WidgetBuilder.buildPartialBlock(
    accumulated: WidgetBuilder.buildPartialBlock(first: Line("Seattle")),
    next: Line("5 nights")
)

Calls on the builder, one per statement, nested: the block's result is the type those calls return — here a Column<Line, Line>. The sugar and the hand-written calls gave the same lines the same type. In SwiftUI, buildBlock, buildEither and buildOptional do the same for a body.

Syntax · awaitWhat does the compiler do with an async function at its await?
func flyFirstHalf(then: @escaping @Sendable (Int) -> Void) {
    // everything before the await
    print("Ada leaves from \(whereAmI())")
    // the wait. The closure runs when the data has arrived, on some other thread —
    // so it hops back to the actor the first half was on, and runs the second half there
    URLSession.shared.dataTask(with: meadow) { data, _, _ in
        Task { @MainActor in
            flySecondHalf(data ?? Data(), then: then)
        }
    }.resume()
    // and the first half returns here: the thread is free
}

@MainActor

It cuts the function in two. The first half runs to the await and returns — the thread is free; the second half runs when the result is ready, with the locals handed along, back on the actor the first half was on. Run side by side, the sugar and the two hand-written halves printed: Ada leaves from MainActor Ada is back on MainActor Ada leaves from MainActor Ada is back on MainActor both brought nectar, both back on the main actor

Syntax · @concurrentWhat is @concurrent, written out — and what is the one thing the written-out version gets wrong?
func countPollenByHand() async -> Int {
    // a new task, on the shared pool, on no actor — and .value: suspend here until it is done
    await Task.detached {
        var grains = 0
        for i in 0..<60_000_000 where sin(Double(i)) > 0.999 {
            grains += 1
        }
        print("counted on \(whereAmI())")
        return grains
    }.value
}

await Task.detached { body }.value: the body on the pool, the caller's actor free meanwhile. Both ran on no actor and returned to the main actor: counted on no actor counted on no actor the same grains and back on MainActor. The difference: Task.detached is a new task, so the caller's cancellation never reaches it; @concurrent moves the same task, and sees it.

Syntax · Tasksasync let x = f() — what does that one line do, by itself?
/// A one-second errand that reports whether it was cancelled.
func errand() async -> String {
    do {
        try await Task.sleep(for: .seconds(1))
        return "done"
    } catch {
        print("errand: cancelled")
        return "cancelled"
    }
}

/// The line, then a read of it.
func startThenRead() async -> (lineTook: TimeInterval, readTook: TimeInterval, value: String) {
    let before = Date()
    async let x = errand()
    let lineTook = Date().timeIntervalSince(before)
    let value = await x
    return (lineTook, Date().timeIntervalSince(before) - lineTook, value)
}

/// The line, and never a read: the closing brace cancels the child, then waits for her.
func startAndLeave() async {
    async let _ = errand()
}

It starts f() now, as a child task of the task you are in, and makes x the name of the slot its result will land in. The line does not wait: in the compiler's output it is a slot in your frame, the right-hand side turned into a function of its own, and one startAsyncLetWithLocalBuffer. Reading is another line, await x, which waits only while the slot is empty. The closing brace is where a child nobody read is cancelled and then waited for. The run shows all three: the line costing nothing, the read waiting a second, and startAndLeave returning only after its cancelled errand had ended.

Syntax · ActorsWhat does an actor method with an await inside it expand to?
    func pourSlowlyByHand() async {
        let seen = startPour()                           // job 1, in the jar
        try? await Task.sleep(for: .milliseconds(10))    // not in the jar: anyone may pour now
        finishPour(seen)                                 // job 2, in the jar again
    }

Two jobs for the actor, with the actor free between them — so another task's first job runs there and sees the same value. 100 tasks each reading, waiting and writing: both lost the drops: 1 and 1 of 100. Keep a read-modify-write on one side of an await.

Syntax · IsolationWhat does #isolation expand to at a call site?
func whereAmI(_ isolation: isolated (any Actor)? = #isolation) -> String {
    isolation.map { "\(type(of: $0))" } ?? "no actor"
}

The caller's own actor, as the argument: from main-actor code whereAmI() is whereAmI(MainActor.shared), from a @concurrent function it is whereAmI(nil). Both comparisons printed true true. isolated on the parameter makes the function run on whatever actor it is handed.

Syntax · .taskWhat does the .task { } modifier expand to?
            .onAppear {
                watching = Task {
                    await watch("onAppear and onDisappear")
                }
            }
            .onDisappear {
                watching?.cancel()
            }
Screenshot of this code running
Screenshot · iPhone 17, iOS 27.0

A task started in onAppear, kept in state, and cancelled in onDisappear — on the main actor, like the view. Hiding both views printed: onAppear and onDisappear: cancelled the .task modifier: cancelled. The modifier does the keeping and the cancelling; Task { } in onAppear alone leaves a task running after its view is gone.

Syntax · TasksWhat does Task { } expand to, created from main-actor code?
Task.detached(priority: Task.currentPriority) {
    await MainActor.run {
        print("Task { } by hand: \(whereAmI())")
    }
}

A new task on the pool that first hops to its creator's actor and keeps its creator's priority — which is why a Task { } in a view may touch @State. Without the hop it is Task.detached: Task { }: MainActor Task { } by hand: MainActor Task.detached: no actor. Neither has a parent to be cancelled by; keep the Task value if you need to cancel it.

Syntax · Main actor@concurrent code calls a main-actor function, and the compiler makes you write await. What is that await, written out?
@concurrent func fromThePoolByHand() async {
    await MainActor.run {
        report()
    }
}

A hop to the main actor and back: await MainActor.run { report() }. Both versions ran the report on the main actor: report on MainActor report on MainActor. In GCD this was DispatchQueue.main.async { }, written by hand every time and with no way back.

Further reading