什么是 Swift macro
Macro 是 Swift 5.9 的新特性之一。
在 OC 中,宏其实就是代码替换,相对比较简单。与 OC 中的宏类似,Swift 也是为了减少重复代码,不同点在于,可以在编译之前动态地操作项目的 Swift 代码,在编译时注入额外的功能。并且,Swift 中的宏可以调试。
注意:宏只会添加新代码,但绝不会修改或删除项目中已有的代码。

目前在GitHub上已经有很多开发者为 Swift 提供 Macro GitHub - krzysztofzablocki/Swift-Macros: A curated list of awesome Swift Macros
宏的类型
在 Swift 中,宏分类两种:
独立宏 (Freestanding Macro)
附加宏(Attached Macro)
独立宏
类似 OC 中的宏,主要作用是代替代码中的内容。使用 @freestanding 关键字声明,使用的时候以 # 开头。SwiftUI 中预览视图就使用了独立宏:
#Preview {
ContentView()
}
附加宏
主要作用是为声明的内容添加代码,声明的时候使用 @attached 关键字,使用的时候以 @ 开头。
例:@OptionSet<Int>
创建宏
创建宏主要使用 SwiftSyntax 库。SwiftSyntax 是一组用于解析、检查、生成和转换 Swift 源代码的 Swift 库。
环境要求
Xcode 15+
创建 Package
每个 Swift Macro 都是一个 Package,在 Xcode 中File -> New -> Package,选择 Swift Macro,创建即可。

可以看到,Xcode 已经为我们创建好了一个 Macro 的 Demo,并且添加了 SwiftSyntax 库的依赖。

[Macro name].swift宏的声明main.swift一些测试宏的代码[Macro name]Macro.swift宏的具体实现[Macro name]Tests.swift单元测试
声明
声明一个宏主要分为两部分:角色+签名
在刚才创建的 package 中,Xcode 已经在 [Macro name].swift 文件中声明了一个宏。
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroMacros", type: "StringifyMacro")
在 main.swift 文件中可以看到stringify这个宏的效果是将表达式 a + b 转换为字符串 "a + b"。
角色
Xcode 提供的示例声明中 @freestanding(expression) 就是一个宏的角色。目前一共有 7 个角色,2 个独立宏,5 个附加宏:
具体的内容及含义,后面会有详细解释。
由于附加宏可能修改属性、方法、协议等内容,所以会给编译器在预编译阶段产生很大的性能负担,不利于增量编译。为了解决这个问题,附加宏在声明的时候需要将修改或者添加的属性通过 names 明确声明出来。
named(<declaration-name>) - 特定固定名称
arbitrary - 动态名称,例如可能是根据其他参数动态生成的
overloaded - 名称不变,只重载参数或者返回
prefixed - 通过添加前缀生成的内容,
开头的名称
suffixed - 通过添加后缀生成的内容
签名
签名与方法的定义类似。在 Swift 中 func 来定义一个方法,macro 定义一个宏。由于 Swift 中的宏是独立的 Package 所以,宏的定义都是 public 的。最后使用externalMacro指定了宏的实现位置。
实现
根据不同角色,实现的方式有所不同。
根据 externalMacro 指定的信息,可以在 MyMacroMacro.swift 中找到 StringifyMacro。里面定义了宏的实现expansion方法。
可以看到StringifyMacro是一个结构体,遵守ExpressionMacro协议,不同的角色需要遵守不同的协议。
public struct StringifyMacro: ExpressionMacro
协议中有一个expansion方法,每一个 Macro 协议都有一个expansion方法,只是参数及返回值不同。
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax
对于协议内的 node 参数部分,可以查看SwiftSyntax 的相关文档,了解树形结构。Swift AST Explorer
可以看到,现在返回了一个字符串,字符串的内部是一段代码,代码的内容是一个元组。元组里有两个值,第一个是 #stringify 传入的参数的值,第二个是 #stringify 传入的参数的描述
return "(\(argument), \(literal: argument.description))"
在 main.swift 中可以看到一个使用示例
let a = 17
let b = 25
let (result, code) = #stringify(a + b)
print("\(code) = \(result)")
示例中,我们可以对#stringify 这个宏展开,展开后可以看到就是我们刚刚返回的字符串内容。

在 Swift 中编译器在编译时遇到宏的时候,会调用 MyMacroPlugin,生成代码后再进行编译

独立宏
一个宏只能有一个独立宏角色
@freestanding(expression)
用途
传入一个表达式,输出一个结果。
协议
ExpressionMacro
声明
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroMacros", type: "StringifyMacro")
实现
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))"
}
}
使用
import MyMacro
let a = 17
let b = 25
let (result, code) = #stringify(a + b)
print("\(code) = \(result)") // a + b = 42
相关文档
暂时无法在飞书文档外展示此内容
@freestanding(declaration)
用途
创建一个或多个声明,比如struct, function, variable、 type。SwiftUI 中的 Preview 就是这个角色。
协议
DeclarationMacro
声明
@freestanding(declaration)
public macro todo(_ message: String) = #externalMacro(module: "MyMacroMacros", type: "ToDoMacro")
实现
public struct ToDoMacro: DeclarationMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let messageExpr = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),
messageExpr.segments.count == 1,
let firstSegment = messageExpr.segments.first,
case let .stringSegment(message) = firstSegment else {
fatalError("todo macro requires a non-interpolated string literal")
}
context.diagnose(Diagnostic(node: Syntax(node), message: SimpleDiagnosticMessage(
message: message.description,
diagnosticID: .init(domain: "test", id: "error"),
severity: .warning)))
return []
}
}
使用
func someFunc() {
#todo("To do message")
}
相关文档
暂时无法在飞书文档外展示此内容
附加宏
相关文档
暂时无法在飞书文档外展示此内容
@attached(peer)
用途
可以在声明的同级增加代码
协议
PeerMacro
声明
@attached(peer, names: overloaded)
public macro AddCompletionHandler() =
#externalMacro(module: "MacroExamplesPlugin", type: "AddCompletionHandlerMacro")
实现
内容较长,见 MacroExamples 工程
使用
@AddCompletionHandler
func f(a: Int, for b: String, _ value: Double) async -> String {
return b
}
被AddCompletionHandler标记的async 方法,会自带一个CompletionHandler
@attached(accessor)
用途
向属性添加get、set 方法,例如 SwiftUI 中的 @State
协议
AccessorMacro
声明
@attached(accessor)
public macro ObservableProperty() = #externalMacro(module: "MacroExamplesPlugin", type: "ObservablePropertyMacro")
实现
内容较长,见 MacroExamples 工程
使用
@ObservableProperty
var name: String = ""
@attached(memberAttribute)
用途
给成员变量添加属性
协议
MemberAttributeMacro
声明
@attached(memberAttribute)
public macro wrapStoredProperties(_ attributeName: String) = #externalMacro(module: "MyMacroMacros", type: "WrapStoredPropertiesMacro")
实现
内容较长,见 MacroExamples 工程
使用
@wrapStoredProperties(#"available(*, deprecated, message: "hands off my data")"#)
struct OldStorage {
var x: Int
@available(*, deprecated, message: "hands off my data")
var y: Int
}
属性 x 在使用的时候会与 y 一样提示 deprecated
@attached(member)
用途
在其所应用的类型/扩展内添加新声明。例如init()
协议
MemberMacro
声明
@attached(member, names: named(init), named(shared))
public macro Singleton() = #externalMacro(module: "MyMacroMacros", type: "SingletonMacro")
实现
内容较长,见 MyMacro 工程
使用
@Singleton
struct MyStruct2 {
var name: String = ""
}
@attached(extension)
用途
原@attached(conformance)。
conformance 只能给声明的内容添加协议或者 where语句的扩展,但不能添加对应协议的属性,必须要通过其他宏一起使用。例如:
@attached(conformance)
macro AddEquatable() = #externalMacro(...)
@AddEquatable
struct S {}
// expands to
extension S: Equatable {}
所以后续推出了extension 替代 conformance。
协议
ExtensionMacro
声明
protocol MyProtocol {
func requirement()
}
@attached(extension, conformances: MyProtocol, names: named(requirement))
public macro MyProtocol() = #externalMacro(module: "MyMacroMacros", type: "MyProtocolMacro")
实现
public struct MyProtocolMacro: ExtensionMacro {
public static func expansion(of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo protocols: [SwiftSyntax.TypeSyntax], in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
guard let protocolType = protocols.first else { return [] }
let decl: DeclSyntax =
"""
extension \(raw: type.trimmedDescription): \(protocolType) {
func requirement() {
print("requirement")
}
}
"""
guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else {
return []
}
return [
extensionDecl
]
}
}
使用
@MyProtocol
struct MyStruct3 {
var name: String = ""
}
相关文档
暂时无法在飞书文档外展示此内容
调试
开发一个宏的时候免不了需要对宏进行测试和调试。调试的方式与代码调试差不多,可以断点也可以 print。
需要注意的是,必须运行单元测试才会进入断点。
相关资料
示例代码
swift-macro-examples.zip
MyMacro.zip
WWDC相关视频
https://developer.apple.com/videos/play/wwdc2023/10164/
WWDC23 - 10164 - What’s new in Swift.srt
https://developer.apple.com/videos/play/wwdc2023/10167
WWDC23 - 10167 - Expand on Swift macros.srt
https://developer.apple.com/videos/play/wwdc2023/10166
WWDC23 - 10166 - Write Swift macros.srt