Macros

Macros let Swift generate code for you at compile time. Instead of hand-writing repetitive boilerplate, you write a short marker like #Preview or @Observable and the compiler expands it into real, type-checked Swift. They power some of the most-used features in modern SwiftUI and SwiftData — and you can build your own with SwiftSyntax.

What macros are

A macro is a compile-time transformation: it takes the code you wrote and produces additional code, which is then compiled normally. Crucially, macros are not text substitution like C's #define. They operate on the program's syntax tree, the result must be valid Swift, and everything they emit is fully type-checked. Macros never hide control flow — they only add code; they can't delete or silently mutate what you wrote.

Two kinds of macros

Swift macros come in two syntactic flavors, distinguished by their sigil:

KindSigilUsed asExample
Freestanding#An expression or declaration on its own#Preview, #warning, #URL
Attached@An attribute on a declaration@Observable, @Model, @AddCompletionHandler

Freestanding macros (#)

A freestanding macro appears on its own and expands into an expression or a set of declarations. You've already met #Preview. Another is the standard library's #warning, which emits a compiler warning, and #function/#line for source location.

func ship() {
    #warning("Replace the stub before release")   // shows up in the Issue navigator
    print("Called from \(#function) at line \(#line)")
}

Freestanding macros are invoked with a leading #.

Attached macros (@)

An attached macro is written as an attribute on a declaration and transforms that declaration — adding members, conformances, accessors, or peers. Because it's attached to something, it has access to that declaration's structure.

@Observable
final class Counter {
    var value = 0          // the macro adds the observation plumbing
}

Attached macros are written as @ attributes on declarations.

Attached macros are categorized by what they add — for example member macros add new members, extension macros add a conformance, accessor macros add getters/setters, and peer macros add declarations alongside the original. A single macro like @Observable can combine several of these roles.

How expansion works

When the compiler reaches a macro, it parses your code into a syntax tree, hands the relevant piece to the macro's implementation, and splices the returned syntax back in. Compilation then continues on the expanded code. Three properties make this safe and predictable:

  1. Syntax in, syntax out

    The macro receives a structured representation of your code (not raw text) and returns new syntax nodes. It can't reach outside the code it was given.

  2. Additive only

    A macro can only add code. It never deletes or rewrites your existing declarations, so you can always reason about what you wrote.

  3. Fully checked

    The generated code is type-checked like any other Swift. If a macro produces something invalid, you get a normal compiler error pointing at the macro.

Conceptually, @Observable on the Counter above expands to something like this — observation tracking added around each stored property, plus an Observable conformance:

final class Counter: Observable {
    @ObservationTracked var value = 0

    private let _$observationRegistrar = ObservationRegistrar()
    // ... access() / withMutation() calls wired into value's get/set ...
}

A simplified view of what the macro generates — use Xcode's "Expand Macro" to see the real output.

Tip: In Xcode, right-click any macro use and choose Expand Macro to read the generated code inline. It's the best way to understand what a macro actually does.

Using common macros

You already use macros constantly in iOS development. Two of the most important come from SwiftUI and SwiftData.

#Preview

The freestanding #Preview macro registers a preview for the Xcode canvas. It expands into the type Xcode looks for, so you write one clean line instead of a verbose preview provider.

#Preview("Filled counter") {
    CounterView(counter: Counter())
}

#Preview generates the preview registration behind the scenes.

@Observable

The attached @Observable macro (from the Observation framework) makes a class observable so SwiftUI re-renders when its properties change — replacing the older ObservableObject / @Published pattern. In a view you just hold it with @State.

@Observable
final class Counter {
    var value = 0
}

struct CounterView: View {
    @State var counter = Counter()
    var body: some View {
        Button("Count: \(counter.value)") {
            counter.value += 1   // view updates automatically
        }
    }
}

@Observable + @State is the modern way to wire model changes to the UI.

@Model

SwiftData's @Model macro turns a plain class into a persisted entity — generating the storage, change tracking, and schema metadata so you can save and query objects without writing a Core Data stack.

import SwiftData

@Model
final class Task {
    var title: String
    var isDone: Bool
    init(title: String, isDone: Bool = false) {
        self.title = title
        self.isDone = isDone
    }
}

@Model generates the persistence layer for a SwiftData type.

Declaring and implementing macros (SwiftSyntax)

You can write your own macros. A macro has two halves: a declaration in your app or package that gives it a name and signature, and an implementation in a separate compiler-plugin target that does the work using the SwiftSyntax library.

The declaration uses the macro keyword and the @freestanding or @attached attribute to say what kind it is and which target implements it:

// Declaration — names the macro and points at its implementation type
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
    #externalMacro(module: "MyMacrosPlugin", type: "StringifyMacro")

A freestanding expression macro that returns a value and its source text.

The implementation conforms to a role protocol (here ExpressionMacro) and builds the replacement syntax from the arguments it receives:

import SwiftSyntax
import SwiftSyntaxMacros

public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.arguments.first?.expression else {
            return "()"
        }
        // build "(argument, \"argument source text\")"
        return "(\(argument), \(literal: argument.description))"
    }
}

The implementation transforms the input syntax into the output expression.

Using it then looks like an ordinary call, and expands at compile time:

let (result, code) = #stringify(2 + 3)
// result == 5, code == "2 + 3"

Note: Macro implementations run inside the compiler as a separate plugin process. That's why they live in their own target and depend on the swift-syntax package — they manipulate syntax trees, not your app's runtime objects.

Watch out: Reach for a macro only when simpler tools — functions, generics, protocol extensions, property wrappers — won't do. Macros add a build dependency and indirection; the payoff has to be real boilerplate elimination, like @Observable or @Model.

Wrapping up

Macros are compile-time code generators that work on syntax, add code without rewriting yours, and produce fully type-checked Swift. They come as freestanding (#) expressions/declarations and attached (@) attributes. You already rely on #Preview, @Observable, and @Model every day, and when you have genuine repetition to eliminate, you can implement your own with SwiftSyntax.