@Observable, #Preview, #expect and @Model are macros; reading their expansions is essential for debugging.
Macros (Swift 5.9+) generate code at compile time. Unlike C macros they operate on the syntax tree, are type-checked, and only add code — they never delete or rewrite what you wrote.
Two families:
- Freestanding macros start with
#and produce an expression or declarations:#Preview { ... },#expect(a == b),#URL("https://...")-style validators. - Attached macros start with
@and add to the declaration they're attached to. Roles include@attached(member)(add members),peer(add sibling declarations),accessor(add get/set),memberAttribute, andextension(add conformances).@Observablecombines several roles to add observation storage and accessors to a class.
In Xcode, right-click a macro use and choose Expand Macro to see the generated code. This is the first thing to do when a macro-based API behaves unexpectedly.
A macro is declared in a library with macro and implemented in a separate compiler plug-in target built with SwiftSyntax. The implementation is a type conforming to a protocol such as ExpressionMacro or MemberMacro, receiving the syntax node and returning new syntax. It runs in a sandbox, can't access the network or files, and should emit clear diagnostics instead of generating broken code. Package templates (swift package init --type macro) set this all up, including tests using assertMacroExpansion.
Costs: SwiftSyntax adds build time, so prefer ordinary functions, generics or property wrappers when they suffice.
Example
import Observation
@Observable
final class Cart {
var items: [String] = []
var total: Double = 0
}
// Expanded (simplified): the macro adds an ObservationRegistrar,
// rewrites `items` and `total` with tracking accessors and adds
// `extension Cart: Observable {}`.
let cart = Cart()
withObservationTracking {
print("count:", cart.items.count) // count: 0 (reads are tracked)
} onChange: {
print("items will change")
}
cart.items.append("apple") // items will change
// Declaring a macro in a library (implementation lives in a plug-in target):
// @freestanding(expression)
// public macro stringify<T>(_ value: T) -> (T, String) =
// #externalMacro(module: "MyMacros", type: "StringifyMacro")Remember
- Freestanding
#macromakes expressions/declarations; attached@Macroadds to a declaration - Macros only add type-checked code; use Expand Macro to read it
- Implementations are SwiftSyntax plug-ins running in a sandbox
Watch out for
- Writing a macro for something a generic function or property wrapper already handles, paying build-time cost for nothing.