Skip to content
AgileLogics
4 min read

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.