What are Macros?

Swift Macros allow you to generate repetitive code at compile time, making your app’s codebase more easier to read and less tedious to write.

There are two types of macros:

  • Freestanding macros stand in place of something else in your code. They always start with a hashtag (#) sign.

#caseDetection // Freestanding Macro

  • Attached macros are used as attributes on declarations in your code. They start with an @ sign.

@CaseDetection // Attached Macro

Create new Macro

Macros need to be created in a special Package that depends on swift-syntax library.


SwiftSyntax is a set of Swift libraries for parsing, inspecting, generating, and transforming Swift source code. Here is the GitHub repo.

To create a new Macro go to New -> Package and select Swift Macro.

Type the name of your Macro and create the Package.


Type only the actual name of the macro, without Macro suffix. Eg. for a Macro named AddAsync, type AddAsync not AddAsyncMacro.

Macro Package structure

Inside the newly created Package you will find some auto-generated files:

  • [Macro name].swift where you declare the signature of your Macro
  • main.swift where you can test the behaviour of the Macro
  • [Macro name]Macro.swift where you write the actual implementation of the Macro
  • [Macro name]Tests.swift where you write the tests of the Macro implementation

Macro roles

A single Macro can have multiple roles that will define its behaviour.

The available roles are:


Creates a piece of code that returns a value.






Creates one or more declarations. Like struct, function, variable or type.




@freestanding (declaration, names: arbitrary)


Adds new declarations alongside the declaration it’s applied to.




@attached(peer, names: overloaded)


Adds accessors to a property. Eg. adds get and set to a var. For example the @State in SwiftUI.





@attached (memberAttribute)

Adds attributes to the declarations in the type/extension it’s applied to.





@attached (member)

Adds new declarations inside the type/extension it’s applied to. Eg. adds a custom init() inside a struct.




@attached(member, names: named(init()))


Adds conformances to protocols.





Build Macro


In this guide we will create a Macro that creates an async function off of a completion one.

To start building this Macro we need to create the Macro signature.

To do this, go to [Macro name].swift file and add.

@attached(peer, names: overloaded)

public macro AddAsync() = #externalMacro(module: "AddAsyncMacros", type: "AddAsyncMacro")

Here you declare the name of the Macro (AddAsync), then in the #externalMacro you specify the module it is in and the type of the Macro.


Then to implement the actual Macro, go to the [Macro name]Macro.swift file.

Create a public struct named accordingly with the name of the Macro and add conformance to protocols based on the signature you specified in the Macro signature.

So, inside newly created struct and add the required method.

public struct AddAsyncMacro: PeerMacro {

public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {

// Implement macro



If your Macro signature has more than one role you need to add conformance to each role, for example:

// Signature



@attached(member, names: named(init()))

public macro // ...

// Implementation

public struct MyMacro: AccessorMacro, MamberAttributeMacro, MemberMacro { }


To know the corresponding protocols see Macro roles section.

Exporting the Macro

Inside [Macro name]Macro.swift file add or edit this piece of code with to newly created Macro.


struct AddAsyncMacroPlugin: CompilerPlugin {

let providingMacros: [SwiftSyntaxMacros.Macro.Type] = [




Expansion method

The expansion method is responsible for generating the hidden code.

Here the piece of code the Macro (declaration) is attached on, is broken into pieces (TokenSyntax) and manipulated to generate the desired additional code.

To do this we have to cast the declaration to the desired syntax.

Eg. If the Macro can be attached to a struct we will cast it to StructDeclSyntax.

In this case the Macro can only be attached to a function so we will cast it to FunctionDeclSyntax.

So, inside the expansion method add:

guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {

// TODO: Throw error


return []

Now, before we continue, we need to write a test that checks whether the implementation of the Macro generates the code we expect.

So, in [Macro name]Tests.swift file add:

func test_AddAsync() {




func test(arg1: String, completion: (String?) -> Void) {



expandedSource: """

func test(arg1: String, completion: (String?) -> Void) {


func test(arg1: String) async -> String? {

await withCheckedContinuation { continuation in

self.test(arg1: arg1) { object in

continuation.resume(returning: object)





macros: testMacros



Once that is in place, let’s add a breakpoint at return [] inside the expansion method and run the test.

Once we hit the breakpoint, run po functionDecl inside the debug console to get this long description:


├─attributes: AttributeListSyntax

│ ╰─[0]: AttributeSyntax

│ ├─atSignToken: atSign

│ ╰─attributeName: SimpleTypeIdentifierSyntax

│ ╰─name: identifier("AddAsync")

├─funcKeyword: keyword(SwiftSyntax.Keyword.func)

├─identifier: identifier("test")

├─signature: FunctionSignatureSyntax

│ ╰─input: ParameterClauseSyntax

│ ├─leftParen: leftParen

│ ├─parameterList: FunctionParameterListSyntax

│ │ ├─[0]: FunctionParameterSyntax

│ │ │ ├─firstName: identifier("arg1")

│ │ │ ├─colon: colon

│ │ │ ├─type: SimpleTypeIdentifierSyntax

│ │ │ │ ╰─name: identifier("String")

│ │ │ ╰─trailingComma: comma

│ │ ╰─[1]: FunctionParameterSyntax

│ │ ├─firstName: identifier("completion")

│ │ ├─colon: colon

│ │ ╰─type: FunctionTypeSyntax

│ │ ├─leftParen: leftParen

│ │ ├─arguments: TupleTypeElementListSyntax

│ │ │ ╰─[0]: TupleTypeElementSyntax

│ │ │ ╰─type: OptionalTypeSyntax

│ │ │ ├─wrappedType: SimpleTypeIdentifierSyntax

│ │ │ │ ╰─name: identifier("String")

│ │ │ ╰─questionMark: postfixQuestionMark

│ │ ├─rightParen: rightParen

│ │ ╰─output: ReturnClauseSyntax

│ │ ├─arrow: arrow

│ │ ╰─returnType: SimpleTypeIdentifierSyntax

│ │ ╰─name: identifier("Void")

│ ╰─rightParen: rightParen

╰─body: CodeBlockSyntax

├─leftBrace: leftBrace

├─statements: CodeBlockItemListSyntax

╰─rightBrace: rightBrace

Here you can see every component of the function declaration.

And now you can pick the individual piece you need and use it to create you Macro-generated code.

Retrieve first argument name

For example, if you need to retrieve the first argument name of the function, you will write:

let signature = functionDecl.signature.as(FunctionSignatureSyntax.self)

let parameters = signature?.input.parameterList

let firstParameter = parameters?.first

let parameterName = firstParameter.firstName // -> arg1

This is quite a long code just to retrieve a single string and it will be even more complex if you need to handle multiple function variations.

I think that Apple will improve this in the future, but for now let’s stick with this.

Complete the implementation

Now, let’s complete the AddAsync implementation.

if let signature = functionDecl.signature.as(FunctionSignatureSyntax.self) {

let parameters = signature.input.parameterList

// 1.

if let completion = parameters.last,

let completionType = completion.type.as(FunctionTypeSyntax.self)?.arguments.first,

let remainPara = FunctionParameterListSyntax(parameters.removingLast()) {

// 2. returns "arg1: String"

let functionArgs = remainPara.map { parameter -> String in

guard let paraType = parameter.type.as(SimpleTypeIdentifierSyntax.self)?.name else { return "" }

return "\(parameter.firstName): \(paraType)"

}.joined(separator: ", ")

// 3. returns "arg1: arg1"

let calledArgs = remainPara.map { "\($0.firstName): \($0.firstName)" }.joined(separator: ", ")

// 4.

return [


func \(functionDecl.identifier)(\(raw: functionArgs)) async -> \(completionType) {

await withCheckedContinuation { continuation in

self.\(functionDecl.identifier)(\(raw: calledArgs)) { object in

continuation.resume(returning: object)







In this block of code we:

  1. Retrieve the completion argument from the function signature
  2. Parse the function arguments except the completion
  3. Create the arguments that get passed into the called function
  4. Compose the async function

Show custom errors

Macros allow you to show custom errors to the user.

For example, in case the user placed the macro on a struct but that macro can only be used with functions.

In this case, you can throw an error and it will be automatically shown in Xcode.

enum AsyncError: Error, CustomStringConvertible {

case onlyFunction

var description: String {

switch self {

case .onlyFunction:

return "@AddAsync can be attached only to functions."




// Inside expansion method.

guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {

throw AsyncError.onlyFunction // <- Error thrown here


Test Macro usage

To test the behaviour of the AddAsync macro.

Go to the main.swift file and add:

struct AsyncFunctions {


func test(arg1: String, completion: (String) -> Void) {



func testing() async {

let result = await AsyncFunctions().test(arg1: "Blob")


As you can see the build completes with success.


The autocompletion may not show the generated async function.

Show Macro generated code

To expand a Macro in code and see the automatically generated code, right click on the Macro and choose Expand Macro from the menu.


The Expand Macro seems to not work always in Xcode 15.0 beta (15A5160n).


Code generated by Macros can be debugged by adding breakpoints as you normally would.

To do this, right click on a Macro and choose Expand Macro from the menu.

Then add a breakpoints at the line you wish to debug.


Congratulations! You just created your first Macro.

Check out here the complete code.

As you saw, so far Macro implementations can be quite long event to perform a simple task.

But once you wrap your head around, they can be really useful and they can save you a lot of boilerplate code.

Macros are still in Beta so I think Apple will improve them by the time they will be available publicly.

Thank you for reading

Source link