How to Create Swift Macros with Xcode 15
Tags: swift, ios, development, apple
Published: 2023-06-21 06:36:53

[Here](http://blog.leonifrancesco.com/articles/swift-macros) you can find this articles and many more about iOS and macOS Development.


## 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. 
```swift
#caseDetection // Freestanding Macro
```
* **Attached** macros are used as attributes on declarations in your code. They start with an **@** sign.
```swift
@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](https://github.com/apple/swift-syntax.git).

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:
### @freestanding(expression)
Creates a piece of code that returns a value.
#### Protocol
`ExpressionMacro`
#### Declaration
```swift
@freestanding(expression)
```
 
### @freestanding(declaration)
Creates one or more declarations. Like `struct`, `function`, `variable` or `type`.
#### Protocol
`DeclarationMacro`
#### Declaration
```swift
@freestanding (declaration, names: arbitrary)
```
 
### @attached(peer)
Adds new declarations alongside the declaration it's applied to.
#### Protocol
`PeerMacro`
#### Declaration
```swift
@attached(peer, names: overloaded)
```
 
### @attached(accessor)
Adds accessors to a property. Eg. adds `get` and `set` to a `var`. For example the `@State` in SwiftUI.
#### Protocol
`AccessorMacro`
#### Declaration
```swift
@attached(accessor)
```
 
### @attached (memberAttribute)
Adds attributes to the declarations in the type/extension it's applied to.
#### Protocol
`MamberAttributeMacro`
#### Declaration
```swift
@attached(memberAttribute)
```
 
### @attached (member)
Adds new declarations inside the type/extension it's applied to. Eg. adds a custom `init()` inside a `struct`.
#### Protocol
`MemberMacro`
#### Declaration
```swift
@attached(member, names: named(init()))
```
 
### @attached(conformance)
Adds conformances to protocols.
#### Protocol
`ConformanceMacro`
#### Declaration
```swift
@attached(conformance)
```

## Build Macro
### Signature
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.
```swift
@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.
### Implementation
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.
```swift
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:
```swift
// Signature
@attached(accessor)
@attached(memberAttribute)
@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.
```swift
@main
struct AddAsyncMacroPlugin: CompilerPlugin {
  let providingMacros: [SwiftSyntaxMacros.Macro.Type] = [
    AddAsyncMacro.self
  ]
}
```
### 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:
```swift
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:
```swift
func test_AddAsync() {
    assertMacroExpansion(
        """
        @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:
```terminal
FunctionDeclSyntax
├─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:
```swift
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.
```swift
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.
```swift
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:
```swift
struct AsyncFunctions {
  
  @AddAsync
  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.
!WARNING 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.
!WARNING The `Expand Macro` seems to not work always in Xcode 15.0 beta (15A5160n).
## Breakpoint
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.
## Conclusion
Congratulations! You just created your first Macro.
Check out [here](https://github.com/francescoleoni98/Swift-Macros-AddAsync-example) 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