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.

image image

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
2
3
let x = 100, y = 200
print(#stringify(x + y))
// (300, "x + y")

Look up the Package.swift, there are four targets about our macro project

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "MyMacros",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MyMacros",
targets: ["MyMacros"]
),
.executable(
name: "MyMacrosClient",
targets: ["MyMacrosClient"]
),
],
dependencies: [
// Depend on the Swift 5.9 release of SwiftSyntax
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
// Macro implementation that performs the source transformation of a macro.
.macro(
name: "MyMacrosMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),

// Library that exposes a macro as part of its API, which is used in client programs.
.target(name: "MyMacros", dependencies: ["MyMacrosMacros"]),

// A client of the library, which is able to use the macro in its own code.
.executableTarget(name: "MyMacrosClient", dependencies: ["MyMacros"]),

// A test target used to develop the macro implementation.
.testTarget(
name: "MyMacrosTests",
dependencies: [
"MyMacrosMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)

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
2
3
4
// MyMacros.swift

@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacrosMacros", type: "StringifyMacro")

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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))"
}
}

@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
]
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#if canImport(MyMacrosMacros)
import MyMacrosMacros

let testMacros: [String: Macro.Type] = [
"stringify": StringifyMacro.self,
]
#endif

final class MyMacrosTests: XCTestCase {
func testMacro() throws {
#if canImport(MyMacrosMacros)
assertMacroExpansion(
"""
#stringify(a + b)
""",
expandedSource: """
(a + b, "a + b")
""",
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}
}

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.

image image

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
2
3
4
5
6
7
8
public protocol ExpressionMacro: FreestandingMacro {
/// Expand a macro described by the given freestanding macro expansion
/// within the given context to produce a replacement expression.
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax
}

Example stringify

image

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public protocol DeclarationMacro: FreestandingMacro {
/// Expand a macro described by the given freestanding macro expansion
/// declaration within the given context to produce a set of declarations.
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]

/// Whether to copy attributes on the expansion syntax to expanded declarations,
/// 'true' by default.
static var propagateFreestandingMacroAttributes: Bool { get }
/// Whether to copy modifiers on the expansion syntax to expanded declarations,
/// 'true' by default.
static var propagateFreestandingMacroModifiers: Bool { get }
}

public extension DeclarationMacro {
static var propagateFreestandingMacroAttributes: Bool { true }
static var propagateFreestandingMacroModifiers: Bool { true }
}

Example myError

Trigger a custom compiler error. image


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
2
3
4
5
6
7
8
public protocol CodeItemMacro: FreestandingMacro {
/// Expand a macro described by the given freestanding macro expansion
/// declaration within the given context to produce a set of declarations.
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [CodeBlockItemSyntax]
}

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
2
3
4
5
6
7
8
9
10
11
12
13
public protocol PeerMacro: AttachedMacro {
/// Expand a macro described by the given custom attribute and
/// attached to the given declaration and evaluated within a
/// particular expansion context.
///
/// The macro expansion can introduce "peer" declarations that sit alongside
/// the given declaration.
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}

Example getter

Generate getter methods for properties. image


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Describes a macro that can add members to the declaration it's attached to.
public protocol MemberMacro: AttachedMacro {
/// Expand an attached declaration macro to produce a set of members.
///
/// - Parameters:
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of member declarations introduced by this macro, which
/// are nested inside the `attachedTo` declaration.
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}

Example deinitLog

Add a deinit method to the attached class. image


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// Describes a macro that can add attributes to the members inside the
/// declaration it's attached to.
public protocol MemberAttributeMacro: AttachedMacro {
/// Expand an attached declaration macro to produce an attribute list for
/// a given member.
///
/// - Parameters:
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - member: The member declaration to attach the resulting attributes to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of attributes to apply to the given member.
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax]
}

Example getterMembers

Add a macro @getter for every property of the attached type. image


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
2
3
4
5
6
7
8
9
10
11
/// Describes a macro that adds accessors to a given declaration.
public protocol AccessorMacro: AttachedMacro {
/// Expand a macro that's expressed as a custom attribute attached to
/// the given declaration. The result is a set of accessors for the
/// declaration.
static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax]
}

Example @storageBacked

Convert a stored property of optional types to a computed property that saves or gets values from an map _storage. image


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// Describes a macro that can add extensions to the declaration it's
/// attached to.
public protocol ExtensionMacro: AttachedMacro {
/// Expand an attached extension macro to produce a set of extensions.
///
/// - Parameters:
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - type: The type to provide extensions of.
/// - protocols: The list of protocols to add conformances to. These will
/// always be protocols that `type` does not already state a conformance
/// to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of extension declarations introduced by the macro,
/// which are always inserted at top-level scope. Each extension must extend
/// the `type` parameter.
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax]
}

Example @errorType

Extend the type the macro attached to conform to the Error protocol. image


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@freestanding(expression)
public macro stringConnect(_ values: String...) -> String = #externalMacro(module: "MyMacrosMacros", type: "StringConnect")

public struct StringConnect: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
let result = node.argumentList.map({
"\($0.expression)"
}).joined(separator: #"+"+"+"#)

return .init(stringLiteral: result)
}
}

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
2
let str = #stringConnect("hello", "world", #stringConnect("hello", "macro"))
print(str)

If we try to run the code above, the compiler will throw an error: Recursive expansion of macro 'stringConnect'
image
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
2
@freestanding(expression)
public macro anotherStringConnect(_ values: String...) -> String = #externalMacro(module: "MyMacrosMacros", type: "StringConnect")

Then we rewrite the code above to:

1
2
3
4
let str = #stringConnect("hello", "world", #anotherStringConnect("hello", "macro"))
print(str)

// hello+world+hello+macro
image

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.

Import Swift macros without SPM

After having learned how to develop Swift macros, the next question is how we can import them to our projects. If your project is based on Swift package manager, you are lucky that the Apple official recommends to integrate Macros with SPM. But majority of iOS projects are managed through CocoaPods. We need to dive deeper to seek a way for integrating Macros to our CocoaPods projects.

There are two ways that we can import Macros to our exist projects without using SPM.
The first way is to directly import the macro binary to our main project. This way is adaptable to any projects whatever they are managed by.
The second way is making your macro an individual pods framework. You can reintegrate it to other pods projects. If your project is multi-component architecture, that would be the best way.

Integrate Macro to Main Project

As you can see, the Macro is an executable file that runs during code compilation. We need to copy the executable file to our project for executing macro parsing.

Run swift build -c release in your macro project directory, the command would build the project and output an .build/release folder. You can find a executable file named MyMacrosMacros. The name is same as the .macro target in your Package.swift file.

Copying the file to your project root directory. Then searching the Other Swift Flags settings under the targets’ Build Settings. Add two string, -load-plugin-executable and MyMacrosMacros#MyMacrosMacros.
image

Creating a file for declaring these macros, you are now ready to use those macros in you project.

1
2
3
4
5
6
7
8
@attached(extension, conformances: Error)
macro errorType() = #externalMacro(module: "MyMacrosMacros", type: "ErrorTypeMacro")

@errorType
struct Person {
}

let myError: Error = Person()

Integrate Macro as an Individual Pod

Creating a Swift Macro Library

Using command swift package init --name JYMacros --type macro to quickly create a Macro example.

I have written an example for integrating a SwiftMacro Pod to projects.

Creating Podspec and Makefile

First, we need crate a JYMacros.podspec file as the pods’ configuration file.
We make a directory named macros to save our released binary executable file.
For easily building, a Makefile can be the best choice.
Then, our project directory looks like this:

1
2
3
4
5
6
7
bin = "JYMacrosMacros"
build:
swift build -c release --disable-sandbox
cp ".build/release/JYMacrosMacros" "./macros/JYMacrosMacros"
clean:
rm -rf .build
.PHONY: build clean

The Makefile defines two commands build and clean. We can directly invoke make build to build the macro with release mode and copy its building products to the macros directory created before.
Invoking make clean is to clean the old .build folder.

If you want to rebuild the macro every times before the integrating project building, you can write some shell in the project’s Podfile to execute the make build command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Pod::Spec.new do |s|
s.name = 'JYMacros'
s.version = '0.0.1'
s.summary = 'Some Swift macros.'
s.description = <<-DESC
A proof of concept macro to show they can work with cocoapods.
DESC
s.homepage = 'https://github.com/SSBun'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'caishilin' => 'caishilin@yahoo.com' }
s.source = { :git => 'https://github.com/SSBun/demo_swift_macro_pod', :tag => s.version.to_s }
s.ios.deployment_target = '16.0'
s.source_files = ['Sources/JYMacros/**/*']
s.swift_version = "5.9"
s.preserve_paths = ["macros/JYMacrosMacros"]
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-load-plugin-executable ${PODS_ROOT}/JYMacros/macros/JYMacrosMacros#JYMacrosMacros'
}
s.user_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-load-plugin-executable ${PODS_ROOT}/JYMacros/macros/JYMacrosMacros#JYMacrosMacros'
}
end
  • source: Using SPM to create a Swift Macro library, macro declarations is in the Sources/JYMacros folder.
  • preserve_paths: The executable file is the key to assist XCode to expand our macros. We need to carry it as part of our pod.
  • pod_start_xcconfig & user_target_xcconfig: Loading our macro executable file as xcode plugins.

Integrating to Projects

Just as normal cocoaPods libraries, add pod 'JYMacros' to you project’s Podfile file.

1
2
3
4
5
6
7
8
9
10
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'MacroClient' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!

#pod 'JYMacros', path: "./../.."
pod 'JYMacros', git: "https://github.com/SSBun/demo_swift_macro_pod.git", tag: '0.0.6'
end

import JYMacros and then you can use the #stringify macro in your code.

Recompiling Macros while building project

In our above cases, we need to compile the macro binary manually before integrating it to our project. If we want to recompile the macro every time we build the project, we can add a shell script in the Podspec file.

Wanting to compile the macro, we need to preserve the macro source code in the preserve_paths. The old binary folder macros isn’t required and will be created at every project building.

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
31
32
33
34
35
36
37
s.preserve_paths = [
"Sources/JYMacrosMacros",
"Resources",
"Package.swift",
"Makefile",
]


if Dir.pwd.start_with?('/Users')
makeFilePath = Dir.pwd
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => "-load-plugin-executable #{Dir.pwd}/macros/JYMacrosMacros#JYMacrosMacros"
}
s.user_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => "-load-plugin-executable #{Dir.pwd}/macros/JYMacrosMacros#JYMacrosMacros"
}
else
makeFilePath = '${PODS_ROOT}/JYMacros'
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-load-plugin-executable ${PODS_ROOT}/JYMacros/macros/JYMacrosMacros#JYMacrosMacros'
}
s.user_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-load-plugin-executable ${PODS_ROOT}/JYMacros/macros/JYMacrosMacros#JYMacrosMacros'
}
end

# A script printing current path
script = <<~SCRIPT
env -i PATH="$PATH" "$SHELL" -l -c "(cd #{makeFilePath} && make)"
SCRIPT


s.script_phase = {
:name => 'Build MyMacroMacros macro plugin',
:script => script,
:execution_position => :before_compile
}

As you can see, the s.script_phase is a shell script that will be executed before the project compiling. The script will change the directory to the macro project and execute the make command to build the macro binary.

For easy to develop the macro locally, we can add a condition to judge the current path. If the path starts with /Users, meaning the macro is integrated in development mode, the script will use the local path to build the macro. Otherwise, it will use the PODS_ROOT path.

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
How to import Swift macros without using Swift Package Manager
Distributing a Swift Macro to CocoaPods
Support Swift Macros with CocoaPods