Swift Macros

Reviewed & published by Brayan K

Macros (Swift 5.9+) generate code at compile time. Learn freestanding (#) vs attached (@) macros, real examples like #Predicate and @Observable, what they expand to, @attached(member), SwiftSyntax, and hygiene — you consume far more than you write.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Freestanding Macros (#)

Freestanding macros are called with a leading # and act as an expression or declaration on their own. #warning, #file, and #Predicate are common ones.

// FREESTANDING macros are called with a leading # and stand on their own.
// They run at COMPILE TIME and expand into real source code.

// Built-in examples you have likely seen:
#warning("Refactor this before release")   // emits a compiler warning
let file = #file                            // expands to the file path
let line = #line                            // expands to the line number

// #Predicate (from SwiftData/Foundation) builds a type-checked query:
import Foundation
let isAdult = #Predicate<Int> { age in
    age >= 18
}
// The macro expands the closure into a Predicate the framework can run.

2️⃣ Attached Macros (@)

Attached macros use a leading @ and decorate a declaration, adding members or conformances. @Observable rewrites a class so SwiftUI can track its changes.

// ATTACHED macros use a leading @ and attach to a declaration.
// @Observable rewrites a class so SwiftUI can track its changes.
import Observation

@Observable
class Counter {
    var value = 0          // becomes observable automatically
    func tick() { value += 1 }
}

// What @Observable roughly EXPANDS to (conceptually):
//   class Counter: Observable {
//       @ObservationTracked var value = 0
//       ... synthesized observation registrar ...
//   }
//
// You write four lines; the macro generates the boilerplate.

3️⃣ How a Macro Is Declared

A macro declares its role, like @attached(member), and points to an implementation in a separate target that uses SwiftSyntax to read and emit code.

// A macro's role is declared with @attached/@freestanding roles.
// This sketch shows how a member macro is DECLARED (implementation omitted).

// @attached(member) adds new members INTO the type it decorates.
@attached(member, names: named(description))
macro Described() = #externalMacro(module: "MyMacros", type: "DescribedMacro")

// Using it generates a 'description' property for you:
@Described
struct Point { let x: Int; let y: Int }

// The real work lives in a SEPARATE macro target that uses SwiftSyntax
// to read the struct's stored properties and emit the new member.

Your turn. Fill in the freestanding warning macro and the attached observable macro.

// 🎯 YOUR TURN — fill in each ___ then read the expansion.

// 1) Use a FREESTANDING macro (leading symbol) to warn at compile time:
___("Remove this debug path")          // 👉 the freestanding warning macro

// 2) Mark a class with the ATTACHED macro that makes it observable:
___                                     // 👉 the attached macro (with its symbol)
class Model {
    var count = 0
}

// ✅ Expected: a compiler warning appears, and Model becomes observable.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

ConceptExampleKind
Freestanding#warning("...")# prefix
Freestanding#Predicate { ... }# prefix
Attached@Observable@ prefix
Role@attached(member)adds members
Toolingimport SwiftSyntaxbuilds code
Whencompile timeexpansion

Mini-Challenge: Spot the Macro Kinds

Classify each macro as freestanding or attached, then summarize what a macro expands into and when.

// 🎯 MINI-CHALLENGE: spot the macro kinds
// For each item, decide: FREESTANDING (#) or ATTACHED (@)?
//   1. #Predicate<User> { $0.isActive }
//   2. @Observable class Store { ... }
//   3. @attached(member) macro AddID()
//   4. #warning("todo")
//
// Then write one sentence: what does a macro expand into, and when?
//
// ✅ Answers: 1 freestanding, 2 attached, 3 attached, 4 freestanding.
//    A macro expands into generated source code at compile time.

// your notes here

Practice quiz

In which Swift version were macros introduced?

  • Swift 5.9
  • Swift 4.2
  • Swift 5.5
  • Swift 5.7

Answer: Swift 5.9. Swift macros arrived in Swift 5.9, alongside Xcode 15.

What syntax marks a freestanding macro at the call site?

  • A trailing !
  • A => arrow
  • A leading # symbol
  • A leading @ symbol

Answer: A leading # symbol. Freestanding macros are invoked with a leading # such as #warning or #Predicate.

What syntax marks an attached macro?

  • A leading #
  • A leading @ symbol
  • A trailing ?
  • Curly braces

Answer: A leading @ symbol. Attached macros use a leading @ and attach to a declaration, like @Observable.

What does a Swift macro fundamentally do?

  • Downloads code
  • Runs at app launch
  • Replaces the runtime
  • Generates and inserts source code at compile time

Answer: Generates and inserts source code at compile time. A macro expands into additional source code that the compiler inserts before type-checking.

The @Observable macro is an example of which kind of macro?

  • Attached
  • Freestanding expression
  • Standalone script
  • Runtime reflection

Answer: Attached. @Observable is an attached macro that adds observation conformances to a class.

Which library do you use to read and build syntax trees in a macro implementation?

  • Combine
  • SwiftSyntax
  • UIKit
  • Foundation

Answer: SwiftSyntax. Macro implementations operate on syntax trees using the SwiftSyntax library.

What does @attached(member) generate?

  • A new file
  • A protocol
  • A runtime hook
  • New members inside the declaration it is attached to

Answer: New members inside the declaration it is attached to. @attached(member) adds new members, such as properties or methods, to the declaration.

Why is macro hygiene important?

  • It is decorative
  • It speeds up compiling
  • Generated names will not accidentally clash with the user's identifiers
  • It hides code

Answer: Generated names will not accidentally clash with the user's identifiers. Hygiene ensures introduced identifiers do not collide with names in the surrounding code.

Where does a macro's implementation typically live?

  • In the same file inline
  • In a separate compiler-plugin package target
  • In Info.plist
  • In a storyboard

Answer: In a separate compiler-plugin package target. Macro implementations live in a separate macro (compiler-plugin) target, often its own package.

For most developers, the typical relationship with macros is that they...

  • Consume far more than they author
  • Write many and use few
  • Never encounter them
  • Only write them

Answer: Consume far more than they author. Most developers use built-in macros like #Predicate and @Observable far more than they write their own.

Continue this course

Frequently asked questions

What are Swift macros and when were they added?

Swift macros, introduced in Swift 5.9 with Xcode 15, let you generate source code automatically at compile time. Instead of writing repetitive boilerplate by hand, you apply a macro and the compiler expands it into real Swift code before type-checking. Macros are not a runtime feature and they cannot do anything you could not write by hand; they simply save you from writing and maintaining that code yourself, while keeping everything type-checked and debuggable.

What is the difference between freestanding and attached macros?

Freestanding macros are invoked with a leading hash symbol, like #warning("...") or #Predicate { ... }, and they stand alone as an expression or declaration. Attached macros are invoked with a leading at symbol and attach to a declaration, like @Observable on a class, adding or modifying members, conformances, or accessors for that declaration. The symbol at the call site tells you which kind it is: hash for freestanding, at for attached.

What does a macro actually expand into?

A macro expands into ordinary Swift source code that the compiler splices in at the call site during compilation. For example, @Observable expands a class to add the observation machinery SwiftUI needs, and #Predicate expands a closure into a structured Predicate value the framework can evaluate safely. Because the expansion is plain code, you can right-click and inspect it in Xcode, set breakpoints in it, and trust that it was type-checked just like code you wrote.

How are macros implemented, and what is SwiftSyntax?

A macro has two parts: a declaration that defines its name and roles, such as @attached(member), and a separate implementation that lives in its own compiler-plugin target, usually a Swift package. The implementation receives the syntax tree of the code the macro is applied to and produces new syntax to insert, using the SwiftSyntax library to read and build that tree. Roles like @attached(member), @attached(peer), and @freestanding(expression) tell the compiler what the macro is allowed to generate.

Do I need to write my own macros?

Usually not. Most developers consume far more macros than they author, relying on built-in and framework macros such as #Predicate, #Preview, and @Observable to remove boilerplate. Writing a custom macro makes sense when a team has a repetitive, mechanical code pattern across many types that is worth automating. Even then it is an advanced task involving SwiftSyntax and a plugin target, so the common and recommended path is to understand macros well enough to use them confidently.