If you've used SwiftUI's @Observable, SwiftData's @Model, or Swift Testing's #expect, you've already used Swift Macros — you just might not have realized the sheer amount of compile-time machinery working behind those annotations. Swift Macros landed in Swift 5.9 and have been evolving fast through Swift 6. They're hands down the most powerful metaprogramming tool the language has ever offered, letting you generate code at compile time, kill boilerplate, enforce patterns, and build expressive APIs that simply weren't possible before.
This guide takes you from the fundamentals all the way to building your own custom macros from scratch. We'll cover every macro role, dig into how SwiftSyntax powers the whole system, write real implementations, test them properly, and check out what the open-source community has been building. By the end, you'll be ready to create macros that save you hours of repetitive work across your projects.
Why Swift Macros Matter
Before Swift 5.9, there was no official way to generate code at compile time in Swift. Developers leaned on tools like Sourcery, or — let's be honest — just wrote mountains of boilerplate by hand. Want a custom CodingKeys enum for every Codable type? Write it yourself. Need auto-generated mocks for testing? Pull in a third-party tool. Compile-time URL validation? Nope, out of luck.
Swift Macros change that equation entirely.
They give you a first-class, type-safe, compiler-integrated mechanism for code generation. Here's what makes them special:
- Compile-time safety: Macro-generated code goes through the exact same type checking as hand-written code. If your macro spits out invalid Swift, the compiler catches it right away.
- Transparency: You can expand any macro in Xcode to see exactly what code it generates. No hidden magic — just Swift code generating more Swift code.
- Composability: Multiple macro roles can be combined on a single declaration, enabling rich, layered transformations.
- Performance: Since macros expand at compile time, there's zero runtime overhead. The generated code runs just as fast as if you'd written it yourself.
The Two Families of Swift Macros
Swift Macros come in two families: freestanding macros and attached macros. The distinction is straightforward but pretty fundamental to understanding how everything fits together.
Freestanding Macros
Freestanding macros use the # prefix and stand on their own as expressions or declarations in your code. They look a lot like function calls, but they're expanded at compile time.
// Expression macro — returns a value
let validURL = #URL("https://swiftcrafted.com")
// Declaration macro — generates new declarations
#warning("This feature is deprecated")
There are two freestanding macro roles:
@freestanding(expression)— Produces a value. This is the only macro role that can actually return something. Think of it as a compile-time function.@freestanding(declaration)— Generates one or more new declarations (functions, types, variables) right at the call site.
Attached Macros
Attached macros get applied to existing declarations using the @ prefix, just like attributes you're already familiar with.
@Observable
class UserProfile {
var name: String = ""
var email: String = ""
}
There are five attached macro roles, each targeting a different aspect of the declaration:
@attached(peer)— Adds new declarations alongside the one it's applied to.@attached(member)— Adds new declarations inside the type or extension.@attached(memberAttribute)— Adds attributes to existing members of the type.@attached(accessor)— Adds getters, setters, or other accessors to a property.@attached(extension)— Adds an extension with conformances or additional members.
And here's the really cool part — a single macro can combine multiple roles. @Observable, for instance, combines @attached(member), @attached(memberAttribute), and @attached(extension) to transform a class into a fully observable object. That's a lot of heavy lifting from one little annotation.
Understanding Macro Roles in Depth
Let's walk through each role with concrete examples so you can see exactly what each one does and when you'd actually reach for it.
Expression Macros
Expression macros are the simplest to wrap your head around. They take an expression as input and produce a new expression as output. The classic example is #stringify:
// Declaration
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
#externalMacro(module: "MyMacros", type: "StringifyMacro")
// Usage
let result = #stringify(2 + 3)
// Expands to: (2 + 3, "2 + 3")
But here's a more practical one — a compile-time URL validator:
@freestanding(expression)
public macro URL(_ string: String) -> URL =
#externalMacro(module: "MyMacros", type: "URLMacro")
// Valid — compiles fine
let url = #URL("https://swiftcrafted.com")
// Invalid — compile-time error!
let bad = #URL("not a url ::::")
This is genuinely one of my favorite use cases. No more force-unwrapping URL(string:) when you know the URL is valid — the macro validates it at compile time and hands you an unwrapped URL directly.
Declaration Macros
Declaration macros generate entirely new types, functions, or variables at the call site. They're handy for generating repetitive data structures:
@freestanding(declaration, names: arbitrary)
public macro generateEnum(name: String, cases: [String]) =
#externalMacro(module: "MyMacros", type: "GenerateEnumMacro")
// Usage
#generateEnum(name: "Direction", cases: ["north", "south", "east", "west"])
// Expands to:
// enum Direction {
// case north, south, east, west
// }
Member Macros
Member macros add new declarations inside a type. This is one of the most commonly used roles — and for good reason. For example, you might want to auto-generate a custom initializer:
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(module: "MyMacros", type: "AutoInitMacro")
@AutoInit
struct User {
let name: String
let age: Int
let email: String
}
// Expands to add inside the struct:
// init(name: String, age: Int, email: String) {
// self.name = name
// self.age = age
// self.email = email
// }
Peer Macros
Peer macros create new declarations that sit alongside the declaration they're attached to. A common use case is generating an async wrapper for a completion-handler-based function:
@attached(peer, names: overloaded)
public macro AddAsync() = #externalMacro(module: "MyMacros", type: "AddAsyncMacro")
@AddAsync
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
// existing callback-based implementation
}
// Generates alongside:
// func fetchUser(id: String) async throws -> User {
// try await withCheckedThrowingContinuation { continuation in
// fetchUser(id: id) { result in
// continuation.resume(with: result)
// }
// }
// }
If you're still maintaining a codebase that mixes completion handlers and async/await, this kind of macro can be a real lifesaver.
Accessor Macros
Accessor macros add computed property accessors (getters, setters, willSet, didSet) to a stored property. They enable property wrapper-like behavior:
@attached(accessor)
public macro Clamped(min: Int, max: Int) =
#externalMacro(module: "MyMacros", type: "ClampedMacro")
struct Settings {
@Clamped(min: 0, max: 100)
var volume: Int = 50
}
// Generates accessor that clamps value to the range
Extension Macros
Extension macros add an entire extension to the type, often providing protocol conformances along with the required members:
@attached(extension, conformances: Equatable, Hashable)
public macro AutoEquatable() =
#externalMacro(module: "MyMacros", type: "AutoEquatableMacro")
@AutoEquatable
struct Point {
var x: Double
var y: Double
}
// Generates:
// extension Point: Equatable, Hashable {
// static func == (lhs: Point, rhs: Point) -> Bool { ... }
// func hash(into hasher: inout Hasher) { ... }
// }
Setting Up a Macro Package
Swift Macros have to live in a Swift package with a specific structure. The macro implementation runs as a compiler plugin and depends on SwiftSyntax — Apple's library for parsing and manipulating Swift source code as a syntax tree.
Creating the Package
The fastest way to get started is through Xcode: go to File → New → Package and select Swift Macro. This generates the correct package structure automatically. You can also set one up manually:
// Package.swift
// swift-tools-version: 6.0
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "MyMacros",
platforms: [.macOS(.v10_15), .iOS(.v13)],
products: [
.library(
name: "MyMacros",
targets: ["MyMacros"]
),
],
dependencies: [
.package(
url: "https://github.com/swiftlang/swift-syntax.git",
from: "600.0.0"
),
],
targets: [
// The macro implementation — runs at compile time
.macro(
name: "MyMacrosPlugin",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
),
// The public API — what users import
.target(
name: "MyMacros",
dependencies: ["MyMacrosPlugin"]
),
// Tests
.testTarget(
name: "MyMacrosTests",
dependencies: [
"MyMacrosPlugin",
.product(name: "SwiftSyntaxMacrosTestSupport",
package: "swift-syntax"),
]
),
]
)
Understanding the Package Structure
There are three key targets in a macro package:
- The macro plugin target (
.macro) — Contains the actual macro implementations. This code runs at compile time inside the Swift compiler. It depends on SwiftSyntax for parsing and generating syntax trees. - The library target (
.target) — Contains the macro declarations that users see and import. These are the@freestandingand@attacheddeclarations that map to the implementation via#externalMacro. - The test target — Contains tests that verify your macros expand correctly. (Don't skip this one — seriously.)
Building Your First Macro: A Compile-Time URL Validator
Alright, let's build something real. We're going to create a #URL macro from scratch that validates URLs at compile time and returns an unwrapped URL value. No more force-unwrapping or fiddling with optionals for URLs you know are valid.
Step 1: Declare the Macro
In your library target, create the public declaration:
// Sources/MyMacros/Macros.swift
import Foundation
@freestanding(expression)
public macro URL(_ string: String) -> URL =
#externalMacro(module: "MyMacrosPlugin", type: "URLMacro")
Step 2: Implement the Macro
In your macro plugin target, write the implementation. This is where the actual work happens:
// Sources/MyMacrosPlugin/URLMacro.swift
import SwiftSyntax
import SwiftSyntaxMacros
import Foundation
public struct URLMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// Extract the string literal argument
guard let argument = node.arguments.first?.expression,
let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
segments.count == 1,
case .stringSegment(let literalSegment)? = segments.first
else {
throw MacroError.message(
"#URL requires a static string literal"
)
}
let urlString = literalSegment.content.text
// Validate the URL at compile time
guard let _ = Foundation.URL(string: urlString),
urlString.contains("://") || urlString.hasPrefix("/")
else {
throw MacroError.message(
"Invalid URL: \"\(urlString)\" is not a valid URL"
)
}
// Return an expression that creates the URL
return "URL(string: \(argument))!"
}
}
enum MacroError: Error, CustomStringConvertible {
case message(String)
var description: String {
switch self {
case .message(let text):
return text
}
}
}
Step 3: Register the Plugin
Every macro plugin needs an entry point that registers all its macros with the compiler:
// Sources/MyMacrosPlugin/Plugin.swift
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
URLMacro.self,
]
}
Step 4: Use the Macro
import MyMacros
// This compiles — the URL is valid
let homepage = #URL("https://swiftcrafted.com")
let api = #URL("https://api.example.com/v2/users")
// This produces a compile-time error:
// "Invalid URL: "not a valid url" is not a valid URL"
let bad = #URL("not a valid url")
Pretty satisfying, right? Four files and you've got compile-time URL validation that integrates perfectly with the rest of the language.
Building an Attached Macro: Auto-Generated CodingKeys
Now let's tackle something more advanced — an attached member macro that auto-generates a CodingKeys enum for Codable types, with support for custom key mappings through a helper attribute.
The Goal
@CustomCodable
struct Article: Codable {
@CodableKey("article_title")
var title: String
@CodableKey("author_name")
var author: String
var publishedDate: Date // Uses default key "publishedDate"
@CodableKey("is_featured")
var featured: Bool
}
// Generates:
// enum CodingKeys: String, CodingKey {
// case title = "article_title"
// case author = "author_name"
// case publishedDate
// case featured = "is_featured"
// }
If you've ever had to hand-write CodingKeys for a dozen models with snake_case API responses, you'll appreciate how much tedium this eliminates.
Declaring the Macros
// Sources/MyMacros/CodableMacros.swift
@attached(member, names: named(CodingKeys))
public macro CustomCodable() =
#externalMacro(module: "MyMacrosPlugin", type: "CustomCodableMacro")
@attached(peer)
public macro CodableKey(_ key: String) =
#externalMacro(module: "MyMacrosPlugin", type: "CodableKeyMacro")
Implementing CustomCodableMacro
// Sources/MyMacrosPlugin/CustomCodableMacro.swift
import SwiftSyntax
import SwiftSyntaxMacros
public struct CustomCodableMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Collect all stored properties
let members = declaration.memberBlock.members
var cases: [String] = []
for member in members {
guard let property = member.decl.as(VariableDeclSyntax.self),
let binding = property.bindings.first,
let identifier = binding.pattern
.as(IdentifierPatternSyntax.self)?
.identifier.text
else { continue }
// Check if property has @CodableKey attribute
let customKey = property.attributes.compactMap { attr -> String? in
guard let attribute = attr.as(AttributeSyntax.self),
let name = attribute.attributeName
.as(IdentifierTypeSyntax.self)?.name.text,
name == "CodableKey",
let args = attribute.arguments?
.as(LabeledExprListSyntax.self),
let firstArg = args.first?.expression
.as(StringLiteralExprSyntax.self)?
.segments.first?
.as(StringSegmentSyntax.self)?
.content.text
else { return nil }
return firstArg
}.first
if let key = customKey {
cases.append("case \(identifier) = \"\(key)\"")
} else {
cases.append("case \(identifier)")
}
}
let casesString = cases.joined(separator: "\n ")
let codingKeys: DeclSyntax = """
enum CodingKeys: String, CodingKey {
\(raw: casesString)
}
"""
return [codingKeys]
}
}
// CodableKeyMacro is a peer macro that does nothing —
// it exists solely as a marker for CustomCodableMacro to read
public struct CodableKeyMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
return [] // No peers generated — just a marker
}
}
This pattern of using a marker macro (CodableKey) that gets read by a parent macro (CustomCodable) is a common and powerful technique. You'll see it pop up all over the macro ecosystem once you start looking for it.
Working with SwiftSyntax: Essential Concepts
To write macros effectively, you need a working understanding of SwiftSyntax — the library that represents Swift source code as a structured syntax tree. You don't need to memorize every type (there are a lot of them), but grasping the core concepts will save you a ton of time.
The Syntax Tree
Every piece of Swift code gets represented as a tree of syntax nodes. Take a simple variable declaration:
var name: String = "Alice"
SwiftSyntax models this as a VariableDeclSyntax containing a PatternBindingSyntax with an IdentifierPatternSyntax (the name), a TypeAnnotationSyntax (the type), and an InitializerClauseSyntax (the default value). Each of those has further children down the tree. It's turtles all the way down, basically.
Key Types You'll Use Constantly
DeclGroupSyntax— Protocol for declaration groups (structs, classes, enums, actors). Use this to iterate over members.VariableDeclSyntax— Representsvarandletdeclarations. Access bindings for property names and types.FunctionDeclSyntax— Represents function declarations. Access parameters, return type, and body.AttributeSyntax— Represents an attribute like@CodableKey("key"). Extract arguments from here.ExprSyntax— Base type for all expressions. Your expression macros return this.DeclSyntax— Base type for all declarations. Your member and peer macros return arrays of this.
Navigating the Tree
SwiftSyntax uses a pattern of .as(SpecificType.self) to downcast syntax nodes. You'll write this a lot:
// Navigating from a member to a property name
for member in declaration.memberBlock.members {
guard let property = member.decl.as(VariableDeclSyntax.self),
let binding = property.bindings.first,
let name = binding.pattern.as(IdentifierPatternSyntax.self)?
.identifier.text
else { continue }
print("Found property: \(name)")
}
Generating Code with String Interpolation
SwiftSyntax has a powerful string interpolation system that lets you create syntax nodes from string literals. Honestly, this is the easiest way to generate code in most situations:
// Create a function declaration from a string
let funcDecl: DeclSyntax = """
func greet(_ name: String) -> String {
return "Hello, \\(name)!"
}
"""
// Embed syntax nodes in interpolation
let expr: ExprSyntax = "\\(argument).description"
The double backslash in the string literal generates an actual backslash in the output. Use \(raw: someString) when you want to inject raw text without SwiftSyntax escaping it.
Emitting Diagnostics from Macros
A well-built macro doesn't just generate code — it gives helpful error messages when things go wrong. SwiftSyntax provides a diagnostics system that lets your macros emit warnings, errors, and even fix-its directly in Xcode.
import SwiftDiagnostics
public struct MyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
guard let argument = node.arguments.first else {
// Emit a diagnostic with a fix-it suggestion
let diagnostic = Diagnostic(
node: Syntax(node),
message: SimpleDiagnosticMessage(
message: "Missing required argument",
diagnosticID: MessageID(
domain: "MyMacros",
id: "missingArgument"
),
severity: .error
)
)
context.diagnose(diagnostic)
return "\"\""
}
return "\(argument.expression)"
}
}
These diagnostics show up inline in Xcode, just like regular compiler errors and warnings. This is critical for creating macros that people actually want to use — when someone misuses your macro, they should get a clear, actionable error pointing to exactly where the problem is.
Testing Your Macros
Testing is essential for macros. Since they generate code that becomes part of your program, a bug in a macro can propagate to every single call site. SwiftSyntax ships with dedicated testing utilities, so there's really no excuse to skip this step.
Using assertMacroExpansion
import SwiftSyntaxMacrosTestSupport
import XCTest
final class URLMacroTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"URL": URLMacro.self,
]
func testValidURL() throws {
assertMacroExpansion(
#"""
#URL("https://swiftcrafted.com")
"""#,
expandedSource: #"""
URL(string: "https://swiftcrafted.com")!
"""#,
macros: testMacros
)
}
func testInvalidURL() throws {
assertMacroExpansion(
#"""
#URL("not a valid url")
"""#,
expandedSource: #"""
#URL("not a valid url")
"""#,
diagnostics: [
DiagnosticSpec(
message: #"Invalid URL: "not a valid url" is not a valid URL"#,
line: 1,
column: 1
)
],
macros: testMacros
)
}
}
Debugging Tips
Debugging macros can be a bit tricky since they run inside the compiler. Here are the strategies I've found most effective:
- Write tests first. Since macro expansion in tests runs in-process, you can set breakpoints inside your
expansionmethod and actually step through the code. - Use
po nodein the LLDB debugger to inspect the syntax tree passed to your macro. It's incredibly helpful for understanding what you're actually working with. - Print the description. Every syntax node has a
.descriptionproperty that returns the source code it represents. Sprinkleprint(node.description)liberally during development. - Expand in Xcode. Right-click any macro usage and select "Expand Macro" to see the generated code inline.
Body Macros: Swift 6's Game-Changing Addition
Swift 6 introduced the @attached(body) role, and honestly, it might be the most significant addition to the macro system yet. Body macros can synthesize or augment the body of a function — something no previous macro role could touch.
Two Flavors of Body Macros
BodyMacro— Replaces the entire function body. Great for generating implementations from the function signature alone.PreambleMacro— Inserts code at the beginning of the function body while keeping the rest intact. Perfect for logging, tracing, or validation.
Practical Example: Automatic Logging
@attached(body)
public macro Traced() = #externalMacro(module: "MyMacrosPlugin", type: "TracedMacro")
@Traced
func processOrder(id: String, items: [Item]) -> Receipt {
// Your implementation here
let total = items.reduce(0) { $0 + $1.price }
return Receipt(orderId: id, total: total)
}
// The macro inserts logging at the start of the body:
// func processOrder(id: String, items: [Item]) -> Receipt {
// print("[TRACE] processOrder(id: \(id), items: \(items))")
// let total = items.reduce(0) { $0 + $1.price }
// return Receipt(orderId: id, total: total)
// }
Body macros open up some seriously powerful use cases, like automatic RPC stub generation where the function signature defines the remote API contract and the macro fills in all the networking plumbing.
The @DebugDescription Macro
Swift 6 also brought @DebugDescription to the standard library. This macro processes your type's debugDescription property and translates string interpolations involving stored properties into LLDB type summaries. The practical result? Your custom descriptions show up in Xcode's variable inspector and po output without any extra LLDB configuration.
@DebugDescription
struct Temperature {
var celsius: Double
var debugDescription: String {
"\(celsius)°C"
}
}
// In LLDB, printing a Temperature now shows "23.5°C"
// instead of the default struct dump
It's a small quality-of-life improvement, but one you'll appreciate during every debugging session.
Real-World Macros in the Swift Ecosystem
The open-source community has built an impressive collection of macros that tackle real problems. Here are the ones worth knowing about:
Apple's Built-In Macros
@Observable— Transforms a class into an observable object for SwiftUI, replacing the oldObservableObject+@Publisheddance with something much cleaner.@Model— SwiftData's core macro that turns a class into a persistent model with automatic schema generation.#expectand#require— Swift Testing macros that capture rich expression data for detailed failure messages.#Predicate— Generates type-safe predicates for SwiftData queries and Foundation filtering.
Community Macros
- MemberwiseInit — Enhanced memberwise initializers with access control, default values, and property ignore capabilities. Point-Free uses it in production, and it reportedly deleted over 1,100 lines of boilerplate from their codebase.
- MetaCodable — A comprehensive macro collection for
Codablecustomization: custom keys, nested key paths, default values, flattened decoding — the works. - Mockable — Generates mock implementations of protocols for testing, so you can stop hand-writing those tedious mock objects.
- SafeDecoding — Implements failable decoding with automatic fallbacks. One corrupt field in a JSON response won't crash your entire decode operation anymore.
- PropertyTracer — Traces which methods access which properties. Invaluable when you're debugging complex state management issues.
Combining Multiple Macro Roles
One of the most powerful aspects of Swift Macros is combining multiple roles into a single macro. This is exactly how Apple's @Observable works, and you can do the same for your own macros.
@attached(member, names: named(init), named(validate))
@attached(extension, conformances: Validatable)
public macro Validated() =
#externalMacro(module: "MyMacrosPlugin", type: "ValidatedMacro")
@Validated
struct RegistrationForm {
@Range(1...50)
var username: String
@Pattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
var email: String
@Range(8...128)
var password: String
}
// Generates:
// 1. A Validatable conformance via extension
// 2. A validate() method that checks all constraints
// 3. A validating initializer that throws on invalid input
When combining roles, your macro plugin struct conforms to multiple protocols, and you implement each expansion method separately. The compiler calls each one during the appropriate phase of expansion.
Best Practices and Common Pitfalls
After spending a lot of time building macros and studying the ecosystem, here are the practices that separate solid macros from fragile ones.
Do
- Use
makeUniqueName()fromMacroExpansionContextwhen generating variable names. This guarantees no naming collisions with user code or other macro expansions. - Emit diagnostics instead of throwing fatal errors. A
fatalErrorin a macro crashes the compiler (not great for your team's morale). Usecontext.diagnose()for recoverable errors instead. - Write comprehensive tests. Test the happy path, edge cases, error cases, and diagnostic messages. Macros can be subtle, and regression tests are your safety net.
- Keep macro output readable. Since users can expand macros in Xcode, the generated code should be well-formatted and understandable.
- Document what your macro generates. Users shouldn't have to expand the macro every time just to figure out what it does.
Don't
- Don't perform I/O in macros. Macros run inside the compiler sandbox. Network requests, file reads, and other I/O operations will fail or behave unpredictably.
- Don't generate excessive code. Macro expansion adds to compile time. Keep the generated code minimal and focused.
- Don't create macros for trivial transformations. If a simple function or protocol extension would do the job, prefer that. Macros add complexity to the build system that isn't always worth it.
- Don't ignore trivia. SwiftSyntax includes whitespace and comments as "trivia" on syntax nodes. Be mindful of formatting when generating code so the output is clean.
- Don't rely on runtime state. Macros expand at compile time. They can't access runtime values, environment variables, or any dynamic state.
Performance Considerations
Macro expansion is a compile-time operation, and it does add to your build times. Here's how to keep things fast:
- Macro expansion involves serialization overhead. The compiler serializes parts of the syntax tree and sends them to the macro plugin process. This is why macros are only expanded once per use site — the cost is non-trivial.
- Keep SwiftSyntax tree traversal shallow. Deep tree navigation in your
expansionmethod adds up when the macro is applied to many declarations. - Cache intermediate results. If you're iterating over members multiple times, collect everything you need in a single pass.
- Profile with Instruments. For macro-heavy projects, use the Swift compiler's build timing diagnostics to identify slow macros.
Looking Ahead
The Swift macro system continues to evolve quickly. The 2026 GSoC project to reimplement property wrappers using macros tells you a lot about the long-term direction — macros will become the foundational mechanism for many of Swift's own features. Body macros in Swift 6 already opened new doors, and future proposals are exploring even more roles and capabilities.
If you haven't started building your own macros yet, now is a great time. The ecosystem is mature enough for production use, the tooling is solid, and the community has established clear patterns to follow. Start with a simple expression macro to eliminate one piece of boilerplate in your project, then work your way up to attached macros that transform your data models.
Swift Macros represent a fundamental shift in how we write Swift code. They let us teach the compiler about our domain, enforce conventions at compile time, and build APIs that are both expressive and type-safe. Master them, and you'll end up writing less code while building more powerful applications. That's a trade I'll take any day.