Macros

Overview

Swift macros "allow you to generate repetitive code at compile-time".

Swift macros are: :

Macro implementations should be pure functions, using only the information provided to them by the compiler. They run in a sandbox to prevent certain operations. For xample, they cannot interact with the file system or the network.

Resources

Provided Macros

The following macros are provided by Swift and SwiftUI:

Third Party Macros

The following macros are not provided by Apple:

Defining Macros

The steps to define a new macro are:

  1. Open Xcode.
  2. Select New ... File ... Package
  3. Select the "Swift Macro" template.
  4. Click the "Next" button.
  5. Enter a package name.
  6. Select the directory where it will be saved.
  7. Click the "Create" button.

The generated file structure will contain the following:

Macro definitions are similar to function definitions. They begin with the macro keyword and are followed by a name, parameter list, and return type.

Macro Types

The value passed to these is its role. Each role has a corresponding protocol. Macro implementations must conform to the protocol for each of the roles it supports. Often a macro only supports a single role, but they can support multiple roles

All the methods required by these role protocols are static

Freestanding Macros

Freestanding macros are invoked with the # character. They can only use data passed to them.

Freestanding macro definitions begin with @freestanding(expression) and can be applied to any expression.

The source code of the expression is made available to the macro in an abstract syntax tree (AST).

The macro can transform the AST to provide the result AST which is used to generate source code that is then passed to the compiler.

An implementation of a freestanding macro can play only play one of the roles expression and declaration.

Attached Macros

Attached macros are attached to a declaration such as a variable, function, type, or property. They can use data passed to them and data in the declaration to which they are attached. They are invoked with the @ character.

An implementation of an attached macro can play one or more of the roles peer, accessor, memberAttribute, member, and conformance. The role used is determined by where the macro is applied

Generics in Macros

Macros can be declared to use generic types in order to make them usable in more scenarios.

Debugging Macros

To view the AST made available to a custom macro, set a breakpoint in the macro implementation and run a test that uses the macro. In the debugger, use the po command to pretty-print a variable that holds the AST.

Using Custom Macros

To use custom macros in a Swift project:

In each file that wishes to use the macros:

To see learn about a given macro, option-click an invocation to open a dialog containing the documentation.

To see the code generated by a given macro, right-click its invocation and select "Expand Macro".

Invoking Macros

A macro is invoked by preceding its name with a pound sign (#).

Unit Tests

Macro unit tests typically call the assertMacroExpansion function. That function takes a diagnostics argument whose value is an array of diagnostic messages that should be emitted when the macro is invoked incorrectly, such as when an attached macro is applied to the wrong kind of declaration.

Macros typically throw specific cases of a custom enum that conforms to the CustomStringConvertible and Error protocols. For example:

enum MyCustomError: CustomStringConvertible, Error {
case badThing1
case badThing2

var description: String {
switch self {
case .badThing1: return "first bad thing happened"
case .badThing2: return "second bad thing happened"
}
}
}

When non-test code uses a macro in a way that throws an error, the error message will appear in Xcode next to the offending line just like other compiler generated errors.

To run the tests, select Product ... Test or press cmd-u.

Examples