Swift Macro Guideline
Macros, we usually use in C/C++ codes for simplifying redundant code we should write by hand. Swift macros are similar, but significantly more powerful. Compared to C macros, Swift macros have some features helping us writing more complex and safer code.
- Swift macros run in the build phase. Actually, the real macro parser is an individual program running inside a sandbox and communicate with the Xcode.
- Swift macros are similar as normal functions rather than simple string replacements. That means they are type-safe guaranteed by the Swift compiler.
- Swift macros respectively have multiple different types in different circumstances.
- Macros add new code, but they never delete or modify existing code.
Building your fist macro
First, we create a simple macro to learn how to declare a macro and what file architectures it has.
Open Xcode 15 and click the menu File -> New -> Package
to create a Swift Macro
package.


Then we will get an example macro project generated by Xcode. The macro project has a build-in macro stringify
producing both a value and a string containing the source code that generated the value. For example,
1 | let x = 100, y = 200 |
Look up the Package.swift
, there are four targets about our macro project
1 | import PackageDescription |
To create a Swift macro, we need to dependency the swift-syntax
package. The swift-syntax
is a syntax tool provided by Apple for parsing Swift source code to AST or generating source code from structured syntax.
There is a new target type macro
used to claim the target is a Xcode plugin for expanding macros. The target MyMacrosMacros
isn’t used to declare new macros, it contains the implementation of macros declared in the target MyMacros
.
1 | // MyMacros.swift |
As you can see, we use public macro
to declare a macro stringify
that receives any value and returns a tuple. Declaring a macro is similar to declare a function. The @freestanding(expression)
is used to specify the macro type of the stringify
. There are two main categories of macros, freestanding
and attached
, we will learn them in subsequent chapters.
In above code, the macro stringify
was assign to the value #externalMacro(module: "MyMacrosMacros", type: "StringifyMacro")
. The #externalMacro
refers the macros implementation to the module MyMacrosMacros
and the specified type StringifyMacro
. Let’s see detail code in the MyMacrosMacro.swift
.
1 | public struct StringifyMacro: ExpressionMacro { |
The code declare a struct StringifyMacro
conforming to the ExpressionMacro
protocol means the StringifyMacro
is an expression macro. Every macro type has to implement an expansion
function that takes several params containing source code and code context and returns a Swift syntax or throws a parsing error.
As said above, Macros are individual applications running in a special sandbox and communicates with Xcode during complication. So we need create a struct conforming to the CompilerPlugin
protocol to be the entry of the macro plugin. It requires a property providingMacros
where we list all types of macros we want to use in other modules.
Note
Only macros declared in the property providingMacros
can be accessed by outer modules.
After writing macros, we need crate an unite test to ensure the macro behaves as we expect. The Swift provides us with some new asserts
to help testing macro expansions.
1 | #if canImport(MyMacrosMacros) |
We passed source code and expected expanded code to the assertMacroExpansion
function, the function will match all macros the testMacros
contains in the source code we passed in and compare it with the expandedSource
.
The testMacros
map's keys are string names of macros we use in source code.
Beside in unit tests, we can quickly expand macros in our source code to check generated code actually compiled by the Swift compiler.


ShortCut
Xcode 15 has a new shortcut cmd+shift+A
to open the Quick Actions
sheet view.
Kinds of macros
Swift has two kinds of macros:
Freestanding
macros appears on their own, without being attached to a declaration.Attached
macros modify the declaration that they’re attached to.
Call a freestanding macro, we write a number sign #
before its name. If calling an attached macro, we use an at sign @
.
Every kind of macros has multiple roles. We should select appropriate roles for our custom macros. Most roles indicate available code positions where we can use the macro.
Before learning various macros, there are some limitations to macros we should know. Not any code we can generate by macros.
import
declarations can never be produced by a macro.- A type with the
@main
attribute cannot be produced by a macro. - extension declarations can never be produced by a macro.
- operator and precedencegroup declarations can never be produced by a macro .
- macro declarations can never be produced by a macro.
- Top-level default literal type overrides, including IntegerLiteralType, FloatLiteralType, BooleanLiteralType, ExtendedGraphemeClusterLiteralType, UnicodeScalarLiteralType, and StringLiteralType, can never be produced by a macro.
Note
Details about why code above we can not generate with macros are there.
freestanding(expression)
Describes a macro that is explicitly expanded as an expression.
1 | public protocol ExpressionMacro: FreestandingMacro { |
Example stringify

1
2
3// Declaration
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacrosMacros", type: "StringifyMacro")
1
2
3
4
5
6
7
8
9
10
11
12
13// Implement
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}
return "((argument), (literal: argument.description))"
}
}
freestanding(declaration)
Describes a macro that forms declarations.
1 | public protocol DeclarationMacro: FreestandingMacro { |
Example myError
Trigger a custom compiler error.
1
2
3// Declaration
@freestanding(declaration)
public macro myError(_ message: Any) = #externalMacro(module: "MyMacrosMacros", type: "ErrorMacro")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// Implementation
struct ErrorMacro: DeclarationMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let firstElement = node.argumentList.first,
let stringLiteral = firstElement.expression
.as(StringLiteralExprSyntax.self),
stringLiteral.segments.count == 1,
case let .stringSegment(messageString) = stringLiteral.segments.first
else {
throw MacroExpansionErrorMessage("#error macro requires a string literal")
}
context.diagnose(
Diagnostic(
node: Syntax(node),
message: MacroExpansionErrorMessage(messageString.content.description)
)
)
return []
}
}
freestanding(codeItem)
Describes a macro that forms code items in a function or closure body.
1 | public protocol CodeItemMacro: FreestandingMacro { |
Experimental feature
Until to Xcode 15.0, CodeItem macros are an experimental feature that is not enabled.
attached(peer)
These macros produce new declarations in the same scope as the declaration that the macro is attached to. For example, applying a peer macro to a method of a structure can define additional methods and properties on that structure.
1 | public protocol PeerMacro: AttachedMacro { |
Example getter
Generate getter methods for properties.
1
2
3// Declaration
@attached(peer, names: arbitrary)
public macro getter() = #externalMacro(module: "MyMacrosMacros", type: "GetterMacro")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// Implementation
public struct GetterMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard
let variable = declaration.as(VariableDeclSyntax.self),
let binding = variable.bindings.first,
let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self),
let typeAnnotationType = binding.typeAnnotation?.type
else {
throw MacroExpansionErrorMessage("Must attached to variables with explicit type declarations")
}
let identifierName = identifierPattern.identifier.text
let newFunction = try FunctionDeclSyntax("public func get(raw: identifierName.prefix(1).capitalized + identifierName.dropFirst())() -> (typeAnnotationType)", bodyBuilder: {
ExprSyntax(stringLiteral: identifierName)
})
return [DeclSyntax(newFunction)]
}
}
attached(member)
These macros produce new declarations that are members of the type or extension that the macro is attached to. For example, applying a member macro to a structure declaration can define additional methods and properties on that structure.
1 | // Describes a macro that can add members to the declaration it's attached to. |
Example deinitLog
Add a deinit method to the attached class.
1
2
3// Declaration
@attached(member, names: arbitrary)
public macro deinitLog() = #externalMacro(module: "MyMacrosMacros", type: "DeinitLogMacro")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// Implementation
public struct DeinitLogMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard declaration.as(ClassDeclSyntax.self) != nil else {
throw MacroExpansionErrorMessage("Can only declare classes.")
}
return [
#"deinit { print("(self) was deinited.") }"#
]
}
}
attached(memberAttribute)
These macros add attributes to members of the type or extension that the macro is attached to.
1 | /// Describes a macro that can add attributes to the members inside the |
Example getterMembers
Add a macro @getter for every property of the attached type.
1
2
3// Declaration
@attached(memberAttribute)
public macro getterMembers() = #externalMacro(module: "MyMacrosMacros", type: "GetterMembersMacro")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// Implementation
public struct GetterMembersMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
guard
let variable = member.as(VariableDeclSyntax.self),
let binding = variable.bindings.first,
binding.typeAnnotation?.type != nil
else {
return []
}
return ["@getter"]
}
}
attached(accessor)
These macros add accessors to the stored property they’re attached to, turning it into a computed property.
1 | /// Describes a macro that adds accessors to a given declaration. |
Example @storageBacked
Convert a stored property of optional types to a computed property that saves or gets values from an map _storage.
1
2
3
4// Declaration
@attached(accessor, names: arbitrary)
public macro storageBacked() = #externalMacro(module: "MyMacrosMacros", type: "StorageBackedMacro")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// Implementation
public struct StorageBackedMacro: AccessorMacro {
public static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
guard
let variable = declaration.as(VariableDeclSyntax.self),
let binding = variable.bindings.first,
let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self),
let typeAnnotationType = binding.typeAnnotation?.type.as(OptionalTypeSyntax.self),
let typeIdentifier = typeAnnotationType.wrappedType.as(IdentifierTypeSyntax.self)?.name
else {
throw MacroExpansionErrorMessage("Must attached to variables with explicit optional type declarations")
}
let identifierName = identifierPattern.identifier.text
return [
#"""
set {
_storage["#(raw: identifierName)"] = newValue
}
get {
_storage["#(raw: identifierName)"] as? #(typeIdentifier)
}
"""#
]
}
}
attached(extension)
These macros add protocol conformance to the type they’re attached to.
1 | /// Describes a macro that can add extensions to the declaration it's |
Example @errorType
Extend the type the macro attached to conform to the Error protocol.
1
2
3// Declaration
@attached(extension, conformances: Error)
public macro errorType() = #externalMacro(module: "MyMacrosMacros", type: "ErrorTypeMacro")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// Implementation
public struct ErrorTypeMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let errorExtension: DeclSyntax =
"""
extension (type.trimmed): Error {}
"""
guard let extensionDecl = errorExtension.as(ExtensionDeclSyntax.self) else {
return []
}
return [extensionDecl]
}
}
The peer
, member
, and accessor
macro roles require a names argument, listing the names of the symbols that the macro generates. The freestanding macro role declaration
can also set up names for new generated symbols.
There are five kinds of names we can use.
named(<#name#>)
where name is that fixed symbol name, for a name that’s known in advance.overloaded
for a name that’s the same as an existing symbol.prefixed(<#prefix#>)
where prefix is prepended to the symbol name, for a name that starts with a fixed string.suffixed(<#suffix#>)
where suffix is appended to the symbol name, for a name that ends with a fixed string.arbitrary
for a name that can’t be determined until macro expansion.
Tip
As a special case, you can write prefixed($) for a macro that behaves similar to a property wrapper.
Restrictions on arbitrary names
Macros with arbitrary names cannot be used at top-level scope. Detail
Nested macros
If we make a macro expression as a parameter of another macro, nested macro-expansion expressions expand from the outside in. In code the outer macro expands first and the unexpanded inner macro appears in the abstract syntax tree that outer macro receives as its input.
For example, we define a macro stringConnect
that connects multiple strings to one with the separator “+”.
1 | @freestanding(expression) |
See code above, the stringConnect
macro receives a variable parameter values
and returns a string as the result. The implementation of the macro is also simple, we just connect every string expression with the char +
.
Let’s write a simple nested macro expression.
1 | let str = #stringConnect("hello", "world", #stringConnect("hello", "macro")) |
If we try to run the code above, the compiler will throw an error: Recursive expansion of macro 'stringConnect'
You can see that the outer macro was expanded firstly, and the inner macro kept the original expression. Actually, the code above won’t trigger any recursive expansion. That maybe some limitations that Apple doesn’t resolve.
Warning
We cannot use two same macros in a nested macro expression, that would produce recursive expansions.
For running the expression successfully, we can define a new macro anotherStringConnect
having the same implementation from the StringConnect
1 | @freestanding(expression) |
Then we rewrite the code above to:
1 | let str = #stringConnect("hello", "world", #anotherStringConnect("hello", "macro")) |

As shown above, the nested macro expression expanded twice.
Attached macros with multiple roles
Since a role of macros can only expand a part of code in some special positions, Swift allows us declaring one macro with multiple roles for expanding its functions and available areas.
There is an important rule that every role can only receive original code as its input. The sort of multiple roles doesn’t influence any paring result. For example, a member macro adds some properties to a struct, another memberAccessor macro can only get original properties the struct defined, without properties the member macro extended.
References
Online Tool: Swift AST explorer
SE-0389: attached macros
A possible vision for macros in Swift
Macro-Expansion Expression
SE-0389: restrictions on arbitrary names
Using init-accessors
in Swift macros
Import Swift macros without SPM
In the next blog, I will introduce how to integrate Swift Macros into our projects without SPM, most projects usually use CocoaPods.