Macros
Prerequisites: Property wrappers and result builders
Macros (Swift 5.9+) generate code at compile time. They remove boilerplate while staying type-checked — the generated code is ordinary Swift that the compiler checks like anything you wrote.
Two kinds
- Freestanding macros start with
#and produce a value or declaration where they appear:
import Testing
@Test func addition() {
#expect(2 + 2 == 4)
}
#expect captures the expression's source code and values. If you
wrote #expect(2 + 2 == 5), the failure would report
2 + 2 → 4 == 5 rather than a bare "false".
- Attached macros start with
@and add code to a declaration:
import Observation
@Observable
final class Settings {
var theme = "light"
var fontSize = 14
}
@Observable rewrites the class so SwiftUI can track exactly which
properties each view reads, and refresh only those views when they
change.
Other macros you'll meet: #Preview (SwiftUI previews), @Model
(SwiftData), and #URL-style validation macros from packages.
Seeing the generated code
In Xcode, right-click a macro and choose Expand Macro to see exactly
what it generates. Doing this for @Observable is a good way to
demystify it.
Writing macros
A macro is implemented in a separate package target using the
SwiftSyntax library, which lets it read and generate Swift source
code. swift package init --type macro creates a template. Macros run
in a sandbox and can't access the network or file system.
Writing macros is an advanced, library-author task. Before writing one, check whether a function, generic, or property wrapper solves the problem more simply.