Swift Macros Cheat Sheet
Covers Swift 5.9+ macro types (freestanding/attached), writing macro declarations, SwiftSyntax expansion basics, and built-in macros like #Preview.
Declaring a Macro
Macro declarations live in your app target; expansion logic lives in a separate compiler plugin target.
// In your app/library target:@freestanding(expression)macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacros", type: "StringifyMacro")@attached(member, names: named(init))macro AddInit() = #externalMacro(module: "MyMacros", type: "AddInitMacro")// Usage:let (result, code) = #stringify(2 + 3)// result == 5, code == "2 + 3"
Implementing a Macro (Compiler Plugin)
Macros are implemented with SwiftSyntax against a `CompilerPlugin` target declared in Package.swift.
import SwiftSyntaximport SwiftSyntaxMacrospublic struct StringifyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax { guard let arg = node.arguments.first?.expression else { throw MacroError.missingArgument } return "(\(arg), \(literal: arg.description))" }}@mainstruct MyMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [StringifyMacro.self]}
Package.swift for a Macro Target
Macros require a `.macro` target plus a `CompilerPlugin` build product.
let package = Package( name: "MyMacros", targets: [ .macro( name: "MyMacrosImpl", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), ] ), .target(name: "MyMacros", dependencies: ["MyMacrosImpl"]), ])
Common Built-in Macros
Macros shipped with Swift/SwiftUI you'll use daily.
- #Preview- declares a SwiftUI preview without a PreviewProvider boilerplate
- #warning / #error- emit a compiler diagnostic at that source location
- #file, #line, #function- expression macros giving source location info
- @Observable- attached macro that generates observation tracking (replaces ObservableObject)
- @Model- SwiftData attached macro that turns a class into a persistent model
- #stringify- example freestanding macro pattern for expr + source text pairs
Attached Accessor Macro
An `@attached(accessor)` macro injects get/set logic into a stored property, the mechanism behind property wrappers like @Observable.
@attached(accessor)macro Clamped<T: Comparable>(_ range: ClosedRange<T>) = #externalMacro(module: "MyMacros", type: "ClampedMacro")struct Volume { @Clamped(0...100) var level: Int = 50}// Expansion generates:// var level: Int {// get { _level }// set { _level = min(max(newValue, 0), 100) }// }
Emitting Custom Diagnostics from a Macro
Macros can attach warnings or errors at the exact call-site node using `Diagnostic` and `context.diagnose`.
public struct RequireNonEmptyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax { guard let arg = node.arguments.first?.expression, let str = arg.as(StringLiteralExprSyntax.self), !str.segments.isEmpty else { let diagnostic = Diagnostic( node: Syntax(node), message: SimpleDiagnosticMessage( message: "Argument must be a non-empty string literal", diagnosticID: MessageID(domain: "MyMacros", id: "emptyArg"), severity: .error ) ) context.diagnose(diagnostic) return "\"\"" } return arg }}
Attached Member + Extension Macro Combo
Many real macros need both `@attached(member)` and `@attached(extension)` to add stored state and a protocol conformance together.
@attached(member, names: named(id))@attached(extension, conformances: Identifiable)macro AutoIdentifiable() = #externalMacro(module: "MyMacros", type: "AutoIdentifiableMacro")@AutoIdentifiablestruct Todo { var title: String}// Expands roughly to:// struct Todo {// var title: String// let id = UUID()// }// extension Todo: Identifiable {}
Unit Testing a Macro Expansion
SwiftSyntaxMacrosTestSupport's `assertMacroExpansion` compares source-to-source without running the compiled binary.
import SwiftSyntaxMacrosTestSupportimport XCTestfinal class StringifyMacroTests: XCTestCase { func testExpansion() { assertMacroExpansion( "#stringify(2 + 3)", expandedSource: "(2 + 3, \"2 + 3\")", macros: ["stringify": StringifyMacro.self] ) }}
Macro Roles Reference
The full set of attachment roles beyond the basics, and what each is allowed to generate.
- @attached(peer)- adds a new declaration alongside the one it's attached to, without modifying it
- @attached(memberAttribute)- attaches further attributes/macros onto existing members of a type
- @attached(conformance)- (superseded by extension role) adds a protocol conformance with no members
- @attached(extension)- generates a full extension block, optionally with conformances and members
- @freestanding(declaration)- expands to one or more top-level declarations, e.g. #Codable-style generation
- names: arbitrary- opt-out of naming every generated symbol upfront when names can't be known statically
Attached macros can only ADD code (members, accessors, conformances) — they can't remove or rewrite existing declarations, so design your macro's API assuming it's purely additive.