一、Swift Macro介紹
WWDC2023會(huì)上Swift 5.9加入了Swift Macro,它允許我們?cè)诰幾g時(shí)生成代碼或在編譯之前動(dòng)態(tài)地操作項(xiàng)目的 Swift 代碼,從而實(shí)現(xiàn)在編譯時(shí)注入額外的功能,使我們的應(yīng)用程序的代碼庫更易于閱讀且更高效地編碼。
Swift Macro沒有iOS版本要求,只要使用Swift 5.9及以上都可以。
OC Macro就是簡單的代碼替換,而 Swift 宏設(shè)計(jì)的比較抽象,可以通過框架提供的API直接獲取到Swift的句法樹,獲取或添加需要的代碼,功能更加強(qiáng)大。
Swift Macro源碼地址 注:本文基于510.0.3版本
本文主要目的是記錄以下Swift Macro的用法和每種類型的宏的作用,方便快速入門。如果內(nèi)容有誤或理解有誤,希望各位大佬指正,謝謝。
二、初探Swift Macro
1.創(chuàng)建Swift Macro工程
在Xcode的File菜單中選擇NEW->Package->Swift Macro,如圖:

先看看默認(rèn)生成的Macro程序,它會(huì)默認(rèn)實(shí)現(xiàn)一個(gè)
stringify Macro。
2.文件結(jié)構(gòu)
這里只先簡單介紹一下,具體代碼實(shí)現(xiàn)及作用后文再詳細(xì)分析。
2.1.SwiftMacroMacro.swift 宏實(shí)現(xiàn)
struct StringifyMacro就是stringify macro實(shí)現(xiàn)的源碼。
structSwiftMacroPlugin類似于注冊(cè)宏,實(shí)現(xiàn)的宏都要添加到providingMacros數(shù)組里才會(huì)生效,否則使用時(shí)會(huì)報(bào)錯(cuò)。

2.2.SwiftMacro.swift 宏聲明
通過macro關(guān)鍵詞定義一個(gè)stringify宏,具體結(jié)構(gòu)和聲明方法類似。
等號(hào)右邊賦值部分又是另一個(gè)宏,這個(gè)宏專門用來定義其他宏的,傳入需定義宏所在模塊和類型(也就是上面的structStringifyMacro)。
@freestanding(expression)官方定義為MacroRole宏角色(如下圖MacroRole的定義)。相當(dāng)于定義這個(gè)宏具備什么功能,不同的關(guān)鍵詞決定了這個(gè)宏具備不同的功能,并且是可以添加多個(gè)這種關(guān)鍵詞的,具體每個(gè)角色等后文講解各類型宏作用時(shí)就自然理解了。



2.3.main.swift 宏運(yùn)用
這個(gè)文件就是宏在項(xiàng)目中的實(shí)際運(yùn)用,可以看到傳入了a + b,返回了他們的相加結(jié)果a + b = 42,并且還能把"a + b"作為字符串返回。怎么做到的呢?等看完下文分析ExpressionMacro時(shí),自然就理解了。

我們還可以右鍵宏,在菜單里點(diǎn)擊Expand Macro展開宏,也就是預(yù)覽這個(gè)宏編譯后會(huì)轉(zhuǎn)換成什么代碼,依次來確認(rèn)宏是否正確的生成我們想要的代碼。如果發(fā)現(xiàn)點(diǎn)擊后沒任何效果,就要排查宏是不是那里寫錯(cuò)了。


2.3.SwiftMacroTests.swift 宏測(cè)試
除了上面直接使用宏來做測(cè)試以外,我們還可以用蘋果為我們提供的測(cè)試工具來進(jìn)行測(cè)試。這個(gè)測(cè)試類運(yùn)行后是可以在宏實(shí)現(xiàn)里面斷點(diǎn)調(diào)試的,2.2宏運(yùn)用運(yùn)行起來斷點(diǎn)是不起作用的。
等后面我們使用例子分析代碼時(shí),就會(huì)使用到這個(gè)測(cè)試類來斷點(diǎn)查看變量,追蹤問題。

三、Swift Macro分類


從上圖可以看到官方為我們提供了很多種宏,但實(shí)際目前分為兩類宏。
1.FreestandingMacro:即獨(dú)立宏,宏聲明中使用 @freestanding關(guān)鍵字,宏運(yùn)用時(shí)以#開頭。獨(dú)立宏和OC的宏類似,就是將宏替換宏中預(yù)定義的代碼。 上一個(gè)章節(jié)中的#stringify(a + b)就是一個(gè)獨(dú)立宏。
2.AttachedMacro:附加宏,宏聲明中使用@attached關(guān)鍵字,宏運(yùn)用時(shí)以@開頭。作用是為類、結(jié)構(gòu)體、協(xié)議、屬性、方法等目標(biāo)聲明附加額外的代碼(只能增加,不能修改刪除原有代碼)。
iOS17為我們提供的新的監(jiān)聽框架import Observation中的@Observable就是一個(gè)附加宏。
swift macro其實(shí)基礎(chǔ)入門并不難,比較麻煩的是需要處理各種語法、重復(fù)添加代碼等特殊情況,想要自己的宏更加完善,就需要兼容這些特殊情況。比如:
// 1.定義變量有多種語法,需要分別處理
var a: Int = 0 // 標(biāo)準(zhǔn)寫法,可以拿到變量類型
var a = 0 // 不能從語法樹中獲取變量類型,需要自己判斷
var a = 0, b = 0 // 有多個(gè)變量,需要遍歷獲取
// 2.給成員變量添加didSet訪問器,如果外部代碼已經(jīng)手動(dòng)添加了didSet。
// 宏內(nèi)就要自己做處理,根據(jù)自己的需求選擇拋異?;蛘叻祷乜?。
// 3.定義了一個(gè)給類使用的宏,不添加檢查代碼的話,外部是可以錯(cuò)誤的給協(xié)議、結(jié)構(gòu)體等添加的。
四、自定義宏
開始之前,有一些名詞需要先解釋一下:
-
declaration聲明:指宏添加到的目標(biāo)對(duì)象,可以是類、結(jié)構(gòu)體、屬性、方法等。 -
member成員:即屬性、方法。 -
attribute屬性: 指聲明前面添加的修飾符,Swift Attributes。比如@Observable、@available(*, deprecated)等,這些都算是屬性。
1.獨(dú)立宏
1.1.ExpressionMacro
在第二章節(jié)創(chuàng)建的宏工程默認(rèn)實(shí)現(xiàn)的宏StringifyMacro就是一個(gè)ExpressionMacro表達(dá)式宏。
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.arguments.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}
return "(\(argument), \(literal: argument.description))"
}
}
在宏聲明中需要對(duì)表達(dá)式宏添加@freestanding(expression)來表示這個(gè)宏是一個(gè)表達(dá)式類型的獨(dú)立宏。
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "SwiftMacroMacros", type: "StringifyMacro")
下圖就是此宏生成的Abstract Syntax Tree 抽象語法樹。可以很清晰的看到我們使用宏是傳入的表達(dá)式,多個(gè)操作符operator就會(huì)生成更深層級(jí)的嵌套。
此宏主要用于表達(dá)式的計(jì)算或轉(zhuǎn)換。需要傳入一個(gè)表達(dá)式,根據(jù)自身需求可對(duì)其中各節(jié)點(diǎn)進(jìn)行修改。
比如傳入的操作符+可以改成-,再將修改后的ExprSyntax作為返回參數(shù)傳遞出去,就能將原本的加法改為減法。
以下是宏#stringify(a + b + c)的語法樹結(jié)構(gòu):

1.2.DeclarationMacro
可以用來定義類、結(jié)構(gòu)體、枚舉、方法等,起到聲明的作用。如下代碼獲取外部傳入?yún)?shù).stringSegment(name) = expression.segments.first解析出參數(shù)字段name。然后生成了2個(gè)結(jié)構(gòu)體struct (name)和struct (name)Sub
public struct DeclarationTestMacro: DeclarationMacro {
public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let expression = node.arguments.first?.expression.as(StringLiteralExprSyntax.self),
expression.segments.count == 1,
case let .stringSegment(name) = expression.segments.first else {
return []
}
let syntax1 = DeclSyntax(stringLiteral:
"""
struct \(name) {
var a = 0
var b = 0
}
""")
let syntax2 = DeclSyntax(stringLiteral:
"""
struct \(name)Sub {
var a = 0
var b = 0
}
""")
return [syntax1, syntax2]
}
}
宏聲明如下:
因?yàn)檫@個(gè)宏定義了一個(gè)結(jié)構(gòu)體,這里需要標(biāo)記names,否則會(huì)報(bào)錯(cuò):
這個(gè)參數(shù)有4種定義:
1.arbitrary: 即任意名字都行
2.named(name): 必須是指定的名字
3.prefixed(name): 前綴必須是指定字符串(實(shí)際測(cè)試中疑似對(duì)declaration宏不生效,待確認(rèn))。
4.suffixed(name): 后綴必須是指定字符串(實(shí)際測(cè)試中疑似對(duì)declaration宏不生效,待確認(rèn))。
@freestanding(declaration, names: arbitrary)
public macro declarationStruct(_ value: String) = #externalMacro(module: "SwiftMacroMacros", type: "DeclarationTestMacro")
使用時(shí)展開宏即可預(yù)覽替換后的結(jié)果:

1.3.CodeItemMacro
此宏還是一個(gè)未啟用的實(shí)驗(yàn)性功能,不能正式使用。但是可以在SwiftMacroTests測(cè)試類中調(diào)試。(由于是實(shí)驗(yàn)性質(zhì)的功能,這里就只粗略展示一下用法)

在第二章節(jié)創(chuàng)建的宏工程的SwiftMacroMacro類實(shí)現(xiàn)中添加struct CodeItemTestMacro,實(shí)現(xiàn)CodeItemMacro協(xié)議。
CodeItemTestMacro宏只是簡單的實(shí)現(xiàn)了一個(gè)判斷傳入的參數(shù)是否等于0
比如將#codeItemTest(a, b)轉(zhuǎn)換為
if a == 0 {
return
}
if b == 0 {
return
}
具體的宏實(shí)現(xiàn)代碼如下(下文有講解具體代碼含義):
public struct CodeItemTestMacro: CodeItemMacro {
public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.CodeBlockItemSyntax] {
let identifiers = try node.arguments.map { argument in
guard let declReferenceExpr = argument.expression.as(DeclReferenceExprSyntax.self) else {
throw MacroError.codeItem
}
return declReferenceExpr.baseName
}
let items = identifiers.compactMap { syntax -> CodeBlockItemSyntax in
"""
if \(raw: syntax.text) == 0 {
return
}
"""
}
return items
}
}
enum MacroError: Error {
case codeItem
}
測(cè)試用例中測(cè)試代碼如下:
func testCodeItemMacro() throws {
#if canImport(SwiftMacroMacros)
assertMacroExpansion(
"""
#codeItemTest(a, b)
""",
expandedSource: """
if a == 0 {
return
}
if b == 0 {
return
}
""",
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}
//測(cè)試結(jié)果,可以看到測(cè)試通過:
Test Suite 'Selected tests' started at 2024-08-21 11:18:01.012.
Test Suite 'SwiftMacroTests.xctest' started at 2024-08-21 11:18:01.012.
Test Suite 'SwiftMacroTests' started at 2024-08-21 11:18:01.012.
Test Case '-[SwiftMacroTests.SwiftMacroTests testCodeItemMacro]' started.
Test Case '-[SwiftMacroTests.SwiftMacroTests testCodeItemMacro]' passed (0.008 seconds).
Test Suite 'SwiftMacroTests' passed at 2024-08-21 11:18:01.020.
Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.008) seconds
Test Suite 'SwiftMacroTests.xctest' passed at 2024-08-21 11:18:01.021.
Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.009) seconds
Test Suite 'Selected tests' passed at 2024-08-21 11:18:01.021.
Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.009) seconds
Program ended with exit code: 0
打斷點(diǎn)追蹤協(xié)議方法都提供了哪些屬性可以讓我們獲取。從下圖可以看到node有個(gè)arguments數(shù)組,里面包含了傳入的參數(shù)信息,所以這個(gè)宏實(shí)現(xiàn)我們就是去獲取baseName,然后將baseName轉(zhuǎn)換為上面的if-else代碼。


2.附加宏
2.1.AccessorMacro
這個(gè)宏是給成員變量附加訪問器的(比如didSet、willSet、getter、setter)。需要注意的是,如果本身已經(jīng)實(shí)現(xiàn)了didSet,宏又去添加一個(gè)didSet,不做處理的話會(huì)重復(fù)添加導(dǎo)致報(bào)錯(cuò),一般需要在宏實(shí)現(xiàn)中自己做判斷,下面的例子只是簡單的展示一下用法,不會(huì)做過多的容錯(cuò)處理。
public struct AccessorTestMacro: AccessorMacro {
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
guard let property = declaration.as(VariableDeclSyntax.self) else {
return []
}
guard let pattern = property.bindings.first?.pattern.as(IdentifierPatternSyntax.self) else {
return []
}
let identifier = pattern.identifier.trimmed
if case .argumentList(let list) = property.attributes.first?.as(AttributeSyntax.self)?.arguments,
let first = list.first,
let statements = first.expression.as(ClosureExprSyntax.self)?.statements {
let didSet: AccessorDeclSyntax =
"""
didSet {
print("宏內(nèi)部添加的代碼")
\(statements.trimmed)
}
"""
return [didSet]
} else {
let didSet: AccessorDeclSyntax =
"""
didSet {
print(\(identifier))
}
"""
return [didSet]
}
}
}
宏聲明如下:
這里加了一個(gè)入?yún)lock,是為了演示外部可以通過一定的方法將要執(zhí)行的代碼塊添加到宏生成的訪問器里(這種實(shí)現(xiàn)有一定的局限性,比如不能隨意訪問self及其他成員變量),這里只做演示用。
@attached(accessor, names: named(didSet))由于AccessorMacro是一個(gè)附加宏,所以不能像上一節(jié)的獨(dú)立宏那樣使用freestanding來修飾了。然后指定只生成didSet訪問器。
/// 實(shí)驗(yàn)傳代碼塊
@attached(accessor, names: named(didSet))
public macro accessorTest<T>(_ block:((T)-> Void)? = nil) = #externalMacro(module: "SwiftMacroMacros", type: "AccessorTestMacro")
/// 常規(guī)用法
@attached(accessor, names: named(didSet))
public macro accessorTest() = #externalMacro(module: "SwiftMacroMacros", type: "AccessorTestMacro")
在下面的使用代碼中右鍵菜單展開宏可以預(yù)覽到最終代碼會(huì)是什么樣的。

生成didSet訪問器很簡單,可以直接用字符串定義。然后是從下圖中打印的語法樹可以找到傳入的block,然后一步步提取其中的代碼塊,然后插入到didSet中(再次提醒此例子代碼比較簡單,許多容錯(cuò)都沒有處理)。
實(shí)際上@accessorTest<Int>({ a in print()}) 這里也是取巧并非真的從宏里面將a變量傳遞出來了,只是借用同名a讓代碼塊附加到didSet后能找到a這個(gè)變量。

2.2.ExtensionMacro
此宏用于創(chuàng)建Extension,下面的代碼創(chuàng)建了一個(gè)Extension并遵守Equatable,很簡單。
public struct ExtensionTestMacro: 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 syntax: ExtensionDeclSyntax? =
try? .init(.init(stringLiteral:
"""
extension \(type.trimmed): Equatable {}
"""
))
guard let syntax else {
return []
}
return [syntax]
}
}
@attached(extension, conformances: Equatable)
public macro extensionTest() = #externalMacro(module: "SwiftMacroMacros", type: "ExtensionTestMacro")

2.3.MemberAttributeMacro
為聲明中的成員添加屬性。聲明、成員、屬性指什么,可以看文章第四節(jié)開頭。
這里我們就可以用此宏給聲明的成員附加我們上面2.1實(shí)現(xiàn)的accessorTest宏。不做判斷處理的話,默認(rèn)是會(huì)給所有成員附加屬性,我們也可以根據(jù)自己需要添加邏輯過濾不想附加屬性的成員。
public struct MemberAttributeTestMacro: MemberAttributeMacro {
public static func expansion(of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
return [
AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier("accessorTest"))),
]
}
}
@attached(memberAttribute)
public macro memberAttributeTest() = #externalMacro(module: "SwiftMacroMacros", type: "MemberAttributeTestMacro")

2.4.MemberMacro
為聲明添加額外的成員。
如下例子中:給聲明(即實(shí)現(xiàn)中為結(jié)構(gòu)體)添加了一個(gè)flag屬性(成員)和一個(gè)memberAttributePrint方法(成員)。該方法中打印了聲明的屬性和flag的值。
public struct MemberTestMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let syntax1: DeclSyntax =
"""
var flag: Bool = false
func memberAttributePrint() {
print("\(node.attributeName.trimmed)-\\(flag)")
}
"""
return [syntax1]
}
}
@attached(member, names: arbitrary)
public macro memberTest() = #externalMacro(module: "SwiftMacroMacros", type: "MemberTestMacro")

2.5.PeerMacro
根據(jù)成員附加一個(gè)新成員。
下面的例子中,我們根據(jù)屬性生成了一個(gè)新的屬性var (a)Peer: Int = 0,新的屬性的類型、初始化與目標(biāo)屬性保持一致,并且還附帶了@accessorTest宏,為新屬性添加didSet訪問器。
public struct PeerTestMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let property = declaration.as(VariableDeclSyntax.self) else {
return []
}
guard let identifier = property.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.trimmed,
let type = property.bindings.first?.typeAnnotation?.type.trimmed,
let initializer = property.bindings.first?.initializer else {
return []
}
let syntax: DeclSyntax =
"""
@accessorTest
var \(identifier)Peer: \(type) \(raw: initializer)
"""
return [syntax]
}
}
@attached(peer, names: arbitrary)
public macro peerTest() = #externalMacro(module: "SwiftMacroMacros", type: "PeerTestMacro")
下面是宏展開的效果:
這里出Bug了,@accessorTest宏無法展開,但通過運(yùn)行可以看出附加的訪問器是生效了的,打印了屬性的值5

2.6.聚合使用
我們可以一個(gè)宏實(shí)現(xiàn)里實(shí)現(xiàn)多個(gè)宏協(xié)議,然后聲明宏時(shí)使用多個(gè)@attached修飾。
@attached(accessor, names: named(didSet))
@attached(peer, names: arbitrary)
public macro ObservableTracked() = #externalMacro(module: "ObservationMacros", type: "ObservableTrackedMacro")