Adding a boilerplate property to every model, an id or a CodingKeys enum, used to mean either hand-writing it everywhere or reaching for a code generation script that runs outside the build. A macro does the same expansion, but as part of compilation, with full type-checker visibility into the result.
#The new way
The expansion logic lives in a separate compiler-plugin target, built against SwiftSyntax:
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxMacros
public struct AddIdentifiableMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
["let id = UUID()"]
}
}
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
AddIdentifiableMacro.self,
]
}
The client-facing package only exposes the macro's declaration, pointing at that plugin by name:
@attached(member, names: named(id))
public macro AddIdentifiable() = #externalMacro(module: "MyMacrosPlugin", type: "AddIdentifiableMacro")
Call sites just apply the attribute:
import Foundation
@AddIdentifiable
struct Order {
let total: Double
}
let order = Order(total: 42.0)
print(order.id)
#Why it matters
providingMembersOfgets the full syntax tree ofOrder, so the macro could inspect existing properties instead of blindly adding one@attached(member, names: named(id))declares up front exactly which names the macro can introduce, so Xcode can offer completion foridbefore expansion even runs- Expansion happens at compile time and produces real Swift source the type checker validates. A malformed expansion is a build error, not a runtime surprise
- The macro implementation lives in a separate SwiftSyntax-based target, kept out of the app's runtime binary entirely
#Gotcha
You debug macro expansions by reading the generated source, not by stepping through the macro implementation at the call site. Xcode's "Expand Macro" action (or swift build with the expansion flags) shows exactly what code a given annotation produced. A macro that compiles fine in isolation can still fail at a specific call site if the declaration it's attached to doesn't match what the macro assumed (wrong property count, missing type annotation), and that failure surfaces as a diagnostic on the macro attribute, not inside the macro's own implementation file.