阅读视图

发现新文章,点击刷新页面。

在平淡中等待 WWDC 2025 - 肘子的 Swift 周报 #84

不知不觉,距离 WWDC 2025 开始只有 20 天了。在过去的几年中,每当此时我都会写几篇文章畅想 WWDC 上会带来的新功能和我期待的一些变化。然而,或许是因为最近两年 WWDC 上展示的许多新功能并未完全落地,就我个人而言,今年似乎少了往日的热情和渴望。希望这只是我个人的情况。

Swift Macros - 宏替换声明体绑定宏

在 Swift 宏体系中,BodyMacro 是一种专门用于替换方法体实现的宏协议。通过 BodyMacro,开发者可以为已有方法、构造器等提供新的实现代码,减少重复代码的书写,并将功能逻辑更加灵活地注入到已有的声明体中。它与其他宏类型(如 MemberMacroAccessorMacro)的区别在于,它并不生成新的方法声明或属性,而是专注于方法实现的替换

本节建议结合《Swift Macros - 宏之全貌》和《Swift Macros - 宏之协议》一并阅读,以便更好地理解宏在声明体中的角色和具体应用。

1. BodyMacro 的定义

BodyMacro 协议允许开发者实现一个宏,该宏的主要功能是替换现有方法或构造器的实现部分。它与 FunctionDeclSyntax 等声明节点交互,在不修改方法签名的前提下,将方法体替换为新的实现。

同时也支持为未实现的方法提供实现。

BodyMacro 协议的定义如下:

 public protocol BodyMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
    in context: some MacroExpansionContext
  ) throws -> [CodeBlockItemSyntax]
 }

其中参数含义如下:

参数 说明
node 当前的宏语法节点,通常用作参数解析用途
providingBodyFor 要生成实现的声明体,如 func, init, var
context 提供宏展开时的上下文信息,可用于报错、追踪、生成唯一名称等用途

2. 适用范围与限制

语法结构 是否支持 BodyMacro 说明
func xxx() {} ✅ 支持 替换函数体
init() {} ✅ 支持 替换构造器体
deinit {} ✅ 支持 替换析构器体
var xxx: Type {} ✅ 支持 替换计算属性的 getter/setter 实现
subscript(...) {} ✅ 支持 替换下标访问体

不支持 @attached(body) 的声明类型:

语法结构 是否支持 原因
struct, class ❌ 不支持 没有方法体可替换
存储属性(var a = 1 ❌ 不支持 不是函数体结构,不能被 body 替换
enum case, typealias ❌ 不支持 没有可替换的声明体

3. 参数解析

of node: AttributeSyntax

node 表示宏的语法标记本身,它包含了宏调用的信息。例如,@AutoEquatable 中的 @AutoEquatable 会作为 node传递给宏处理方法。在宏实现中,开发者可以检查这个节点,解析传递给宏的参数,进而控制宏的行为。

attachedTo declaration: some DeclGroupSyntax

declaration 是宏附加到的声明体。它代表了宏应用的上下文。例如,如果宏应用于一个方法或构造器,declaration 就会是该方法或构造器的语法节点。开发者可以从中获取类型名、方法签名等信息。

in context: some MacroExpansionContext

context 提供了宏展开的上下文信息,包括文件路径、源代码位置等。这对于诊断错误、生成唯一名称以及确保代码的正确性非常重要。

4. BodyMacro 的返回值

BodyMacro 的返回值是一个数组,表示宏生成的 新的方法体实现代码。这些方法体会替换原有方法的实现。

返回的代码会按照开发者的需求生成新的方法体,这些方法体将替代原始方法的内容,而不会影响方法签名。

5. 示例解析

示例1:ReplaceWithHello

使用

 @HelloBody
 func greet() {
    print("Original implementation")
 }
 
 
 // 展开后
 func greet() {
    print("Hello from macro!")
 }

宏实现

 @attached(body)
 public macro HelloBody() = #externalMacro(module: "McccMacros", type: "HelloBodyMacro")
 
 
 public struct HelloBodyMacro: BodyMacro {
    public static func expansion(of node: AttributeSyntax, providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext) throws -> [CodeBlockItemSyntax] {
        let log = "print("Hello from macro!")"
        let exitLogItem = CodeBlockItemSyntax(stringLiteral: log)
        return [exitLogItem]
    }
 }

6. 总结

BodyMacro 是 Swift 宏体系中非常重要的一类宏,它允许开发者替换现有方法的实现部分。通过 BodyMacro,可以动态生成方法体,减少冗余代码,并提高代码的灵活性和可重用性。

  • 适用于需要方法体替换的场景
  • 简化重复逻辑,提升代码可维护性;
  • 可以结合 AccessorMacroMemberMacro 等宏类型共同使用,构建更高层次的自动化功能。

未来,开发者可以利用 BodyMacro 更加灵活地控制方法实现,为 Swift 项目注入强大的元编程能力。

Swift Macros - 成员属性绑定

Swift 宏系统中,MemberAttributeMacro 是一种用于为类型中的成员声明自动附加属性标记的宏。它适用于需要为多个成员统一附加如 @available@objc@discardableResult 等语义的场景。


1. 定义与原理

MemberAttributeMacro 的定义如下:

 public protocol MemberAttributeMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclGroupSyntax,
    providingAttributesFor member: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [AttributeSyntax]
 }

这说明它具备以下特征:

项目 说明
类型 attached
作用范围 附加在结构体、类、枚举等类型声明上
作用目标 对类型内部的每个成员声明自动附加额外属性
返回值 [AttributeSyntax],即附加的属性标记

2. 使用场景

场景 示例宏 功能描述
批量附加可用性标记 @iOSOnly 为所有成员添加 @available(iOS 13.0, *)
自动添加 @discardableResult @AllowDiscard 避免函数返回值未使用时警告
自动标记为 @objc @ExposeToObjC 支持 Objective-C 可见性

3. 参数详解

参数 用途
of node: AttributeSyntax 宏本身语法节点
attachedTo declaration 宏所附加的类型体(struct/class/enum)
providingAttributesFor member 被作用的每一个成员(方法、属性等)
in context: MacroExpansionContext 用于生成诊断、唯一名等辅助功能

你可以在 member 中判断成员类型、名称,并进行有选择性地附加属性。

4. 示例

为成员批量添加 @available 标记

宏实现

 @attached(memberAttribute)
 public macro iOSOnly() = #externalMacro(module: "McccMacros", type: "iOSOnlyMacro")
 
 public struct iOSOnlyMacro: MemberAttributeMacro {
    public static func expansion(
        of node: AttributeSyntax,
        attachedTo declaration: some DeclGroupSyntax,
        providingAttributesFor member: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [AttributeSyntax] {
        return [
            try AttributeSyntax("@available(iOS 13.0, *)")
        ]
    }
 }

使用示例

 @iOSOnly
 struct LegacyAPI {
    func oldMethod() { }
    var status: String { "ok" }
 }
 
 // 展开效果
 struct LegacyAPI {
    @available(iOS 13.0, *)
    func oldMethod() { }
 
    @available(iOS 13.0, *)
    var status: String { "ok" }
 }

为成员添加@UserDefalut

访问器绑定宏 中我们提供了 @UserDefalut, 我们可以通过 成员属性绑定宏 给属性都添加上。

使用

 @UserDefaultsProperty
 struct SettingsProperty {
    var username: String?
    var age: Int
 }
 
 // 展开
 
 
 struct SettingsProperty {
    @UserDefault
    var username: String?
    @UserDefault
    var age: Int
 }

实现

 @attached(memberAttribute)
 public macro UserDefaultsProperty() = #externalMacro(module: "McccMacros", type: "UserDefaultMacro")
 
 extension UserDefaultMacro: MemberAttributeMacro {
    public static func expansion(
        of node: AttributeSyntax,
        attachedTo declaration: some DeclGroupSyntax,
        providingAttributesFor member: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [AttributeSyntax] {
 
          // 通过字符串 "@UserDefault" 构造了一个 AttributeSyntax 实例(语法树中表示 @UserDefault 的对象)。
          // AttributeSyntax 是 SwiftSyntax 提供的一个类型,用来描述“一个属性修饰器”。
          // 因为手写 AttributeSyntax 很麻烦,要写一堆 AST 结构,但 Swift 宏允许我们偷懒,支持用字符串解析成 AST 片段,这个字符串只要符合 Swift 语法就可以。
          // 因为 MemberAttributeMacro 的返回类型是 [AttributeSyntax],也就是:可以对一个成员添加 多个 宏属性.
         
        // `.init(stringLiteral: "@UserDefault")`
        // 等同于:
        // `AttributeSyntax(stringLiteral: "@UserDefault")`
        return [.init(stringLiteral: "@UserDefault")]
    }
 }

5. 条件属性附加

例如,我们可以只为方法名以 "old" 开头的函数添加 @available

 if let funcDecl = member.as(FunctionDeclSyntax.self),
    funcDecl.identifier.text.hasPrefix("old") {
    return [try AttributeSyntax("@available(iOS 13.0, *)")]
 }
 return []

6. 限制与注意事项

限制 说明
只能附加属性 不能添加新方法或修改函数体
不影响嵌套类型 仅作用于第一层成员
与手动属性并存 可以手动添加属性,宏添加不会冲突

7. 总结

MemberAttributeMacro 是一种细粒度的声明增强工具,非常适合用于:

  • 给成员自动附加语义注解;
  • 降低重复写标记属性的成本;
  • 实现统一标记、跨平台适配等能力。

它的设计理念是“轻量级修饰”,通过规则生成统一的标记代码,是一种常见的声明式元编程方式。

Swift Macros - 扩展绑定宏

在 Swift 宏系统中,ExtensionMacro 是一种用于自动生成扩展(extension)代码块的宏协议,适用于为类型生成协议实现、工具方法、便捷功能等 “类型之外”的附加内容。它是 Swift 中唯一专门用于生成类型扩展的宏角色。

1. ExtensionMacro 的定义

Swift 标准库中对 ExtensionMacro 的定义如下:

 public protocol ExtensionMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclGroupSyntax,
    providingExtensionsOf type: some TypeSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [ExtensionDeclSyntax]
 }

这意味着:

  • 它是一种 @attached(extension) 宏;
  • 必须绑定在结构体、类、枚举等 类型声明体 上;
  • 它的职责是为该类型生成一个或多个完整的 extension
  • 返回的是 [ExtensionDeclSyntax],即多个扩展声明语法。

2. 使用场景分析

应用场景 示例 说明
自动协议实现 @AutoEquatable 在扩展中实现 Equatable 协议方法
添加工具方法 @Stringifyable 为类型扩展一个 stringify() 方法
组合属性行为 @Bindable 在扩展中添加辅助函数支持绑定逻辑
动态特性注入 @Observable 在扩展中生成 Publisher 等观察能力

3. 参数详解

of node: AttributeSyntax

代表宏标记语法本身,例如 @AutoEquatable,可用于分析传入参数、控制行为。

attachedTo declaration: some DeclGroupSyntax`

表示宏绑定的原始类型声明体,例如:

 @AutoEquatable
 struct User {
    var name: String
 }

此处 declaration 就是整个 struct User { ... } 的结构。

providingExtensionsOf type: some TypeSyntaxProtocol

即绑定的类型名(如 User),可以用于组装扩展语法,例如:

 extension (type.trimmedDescription): Equatable { ... }

in context: some MacroExpansionContext

上下文信息,包括定位宏展开位置、生成唯一 ID、发出诊断信息等。


4. 返回值 [ExtensionDeclSyntax]

返回的是多个完整的 extension 语法块:

 extension User: Equatable {
    static func == (lhs: User, rhs: User) -> Bool {
        lhs.name == rhs.name
    }
 }

宏系统将这些扩展插入到类型作用域之外。

5. 示例解析

示例:自动生成 Equatable 实现

使用

 @AutoEquatable
 struct UserEquatable {
    var name: String = ""
 }
 
 // 展开后 
 struct UserEquatable {
    var name: String = ""
 }
 extension UserEquatable: Equatable {
    public static func == (lhs: UserEquatable, rhs: UserEquatable) -> Bool {
        lhs.name == rhs.name
    }
 }

实现

 @attached(extension, conformances: Equatable, names: named(==))
 public macro AutoEquatable() = #externalMacro(module: "McccMacros", type: "AutoEquatableMacro")
 
 public struct AutoEquatableMacro: 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] {
        guard let structDecl = declaration.as(StructDeclSyntax.self) else {
            throw MacroError.message("@AutoEquatable 目前只支持结构体")
        }
         
        // 获取属性名
        let properties = structDecl.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .flatMap { $0.bindings }
            .compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text }
         
        // 拼接对比表达式
        let comparisons = properties.map { "lhs.($0) == rhs.($0)" }.joined(separator: " && ")
         
        let ext: ExtensionDeclSyntax = try ExtensionDeclSyntax("""
        extension (raw: type.trimmedDescription): Equatable {
            public static func == (lhs: (raw: type.trimmedDescription), rhs: (raw: type.trimmedDescription)) -> Bool {
                (raw: comparisons)
            }
        }
        """)
         
        return [ext]
    }
 }
 

6. 小贴士与进阶建议

  • 如果你只需要添加扩展方法(而不希望暴露在类型体内),推荐使用 ExtensionMacro
  • 若生成 static 方法、协议实现,优先考虑 ExtensionMacro 而非 MemberMacro
  • 你可以生成多个扩展块(比如将静态方法和实例方法拆分);
  • 不要与 @attached(member) 搞混,两者生成的位置与作用域不同。

7. 总结

ExtensionMacro 是一种强大的宏类型,它让你能够安全、清晰地将协议实现或工具逻辑注入到类型之外,而不干扰类型本身的结构声明。

适合用于:

  • 自动协议实现;
  • 类型功能模块化;
  • 属性绑定支持函数等逻辑的注入。

它是宏系统中实现“非侵入式增强”的关键角色。

Swift Macros - 访问器绑定宏

在 Swift 宏体系中,AccessorMacro 是一种专用于自动生成属性访问器(如 getter、setter、willSet、didSet 等) 的宏协议。它适用于那些希望对属性访问行为进行自定义、跟踪或扩展的场景,在构建声明式属性模型和状态观察系统中极具价值。

1. AccessorMacro 的定义

标准库中 AccessorMacro 的协议定义如下:

 public protocol AccessorMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclSyntaxProtocol,
    providingAccessorsOf storedProperty: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [AccessorDeclSyntax]
 }

这表示:

  • 它是一种 @attached(accessor) 类型的宏;
  • 专门用于属性级别(property-level) 绑定;
  • 它的返回值为 [AccessorDeclSyntax],即访问器数组;
  • MemberMacro 不同,它不生成新成员,只生成该属性的访问逻辑。

2. 使用场景分析

应用场景 示例 说明
自动打印追踪 @Observe 自动打印属性变化前后的值
自动脏标记更新 @DirtyTrack 属性变更时自动设置脏标志
数据合法性校验 @Validate 在 setter 中自动进行值的合法性校验
双向绑定触发器 @Bindable 在 set 时触发 UI 更新或事件回调

只要你希望控制属性访问行为(特别是赋值过程)AccessorMacro 就是首选工具。

3. 参数详解

of node: AttributeSyntax

代表宏标记语法本身,例如 @Observe,可用于参数识别与行为控制。

attachedTo declaration: some DeclSyntaxProtocol

表示宏所附着的原始属性声明,一般是 VariableDeclSyntax

providingAccessorsOf storedProperty: some DeclSyntaxProtocol

同样表示所操作的属性本身,与 attachedTo 通常相同,但语义更明确:你要为它提供访问器。

in context: some MacroExpansionContext

上下文信息:用于生成唯一标识符、定位源文件位置或报告错误。

4. 返回值 [AccessorDeclSyntax]

返回值是访问器声明数组,可以包含任意组合,如:

 [    AccessorDeclSyntax("get { _value }"),    AccessorDeclSyntax("set { print("New value: \(newValue)"); _value = newValue }") ]

这些访问器将完全替换原始属性的访问行为。


5. 示例解析

示例:@UserDefault

我们定义一个宏 @UserDefault,为属性生成 getter 和 setter,提供存储和获取能力。

使用

 struct Settings {
    @UserDefault
    var username: String
     
    @UserDefault
    var age: Int?
 }
 
 // 展开后
 struct Settings {
    
    var username: String
    {
        get {
            (UserDefaults.standard.value(forKey: "username") as? String)!
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: "username")
        }
    }
     
    var age: Int?
    {
        get {
            UserDefaults.standard.value(forKey: "age") as? Int
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: "age")
        }
    }
 }

实现

 @attached(accessor, names: arbitrary)
 public macro UserDefault() = #externalMacro(module: "McccMacros", type: "UserDefaultMacro")
 
 public struct UserDefaultMacro: AccessorMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [AccessorDeclSyntax] {
         
        // 把通用声明转成变量声明
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              let binding = varDecl.bindings.first,
              // 获取属性名
              let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
              // 获取属性类型
              let typeSyntax = binding.typeAnnotation?.type
        else {
            throw ASTError("UserDefault can only be applied to variables with explicit type")
        }
 
         
        let isOptional: Bool
        let type: String
 
         
        // 判断是否可选类型
        if let optionalType = typeSyntax.as(OptionalTypeSyntax.self) {
            isOptional = true
            // 去掉 `?` 获取实际类型
            type = optionalType.wrappedType.description
        } else {
            // 普通类型
            isOptional = false
            type = typeSyntax.description
        }
 
        // ✅ 构造 getter
        let getter: AccessorDeclSyntax
        if isOptional {
            getter = """
            get {
                UserDefaults.standard.value(forKey: "(raw: name)") as? (raw: type)
            }
            """
        } else {
            getter = """
            get {
                (UserDefaults.standard.value(forKey: "(raw: name)") as? (raw: type))! 
            }
            """
        }
 
        // ✅ 构造 setter
        let setter = AccessorDeclSyntax(
            """
            set {
                UserDefaults.standard.setValue(newValue, forKey: "(raw: name)")
            }
            """
        )
 
        return [getter, setter]
    }
 }
 

6. 与 PeerMacro 配合使用

通常 AccessorMacroPeerMacro 是组合使用的:

  • PeerMacro:负责生成底层的 _xxx 存储属性;
  • AccessorMacro:负责生成代理的访问逻辑,访问 _xxx 并包裹额外行为。

例如:

 @WithStorage
 @Observe
 var name: String

展开后等价于:

 private var _name: String = ""
 
 var name: String {
    get { _name }
    set {
        print("[name] 旧值:(_name),新值:(newValue)")
        _name = newValue
    }
 }

7. 限制与注意事项

  • 访问器宏只能附着在 var 属性上;
  • 不能生成 willSetdidSetget/set 同时存在的混合访问器(Swift 语法限制);
  • 原始属性必须有 backing 存储(可配合 PeerMacro 生成);
  • @propertyWrapper 不同,它不会引入额外类型或语义负担。

8. 总结

AccessorMacro 是 Swift 宏系统中控制“属性行为”的关键工具。它通过访问器代码生成机制,将属性语义与行为解耦,适用于:

  • 监听属性变化;
  • 构建数据流响应逻辑;
  • 执行赋值约束与处理。

结合 MemberMacroPeerMacro,你可以构建出完整的声明式状态模型系统,实现真正的结构驱动式编程体验。

6.4 Swift Macros - 对等绑定宏

在 Swift 宏体系中,PeerMacro 是一种非常灵活且强大的宏协议,专用于生成与绑定声明处于同一作用域的“对等”声明,常用于自动扩展同级的变量、函数或类型定义。

本节将深入介绍 PeerMacro 的用途、定义、参数结构以及实际示例,帮助你理解它在元编程场景中的独特价值。

建议结合《Swift Macros - 宏之全貌》和《Swift Macros - 宏之协议》一并阅读,便于全面理解宏系统的角色协作模型。

1. PeerMacro 的定义

标准库中 PeerMacro 的定义如下:

 public protocol PeerMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax]
 }

这意味着:

  • 它是一个 附加宏(attached macro)
  • 不能生成成员,而是生成与附着声明同级的其他声明
  • 它的返回值为 [DeclSyntax],即可以注入多个顶层/局部声明;
  • 使用范围包括变量、函数、类型、扩展等几乎所有可声明位置。

2. PeerMacro 的典型用途

Peer 宏的应用场景非常广泛,常用于:

场景 示例 说明
自动生成伴生变量 @WithWrapper 为属性生成 _xxx 存储变量
自动生成伴生函数 @BindAction 为属性自动生成相关行为函数
生成衍生声明 @AutoObservable 为属性自动生成观察者包装及通知机制
声明反射信息 @Reflectable 自动生成结构体元信息注册代码

特别适合那些需要基于现有声明生成“相关声明”的情境,但不适合直接插入原声明体内的场合。


3. 参数详解

of node: AttributeSyntax

代表宏的语法标记本身,例如 @WithWrapper。可用于:

  • 宏参数提取;
  • 判断具体调用语法。

attachedTo declaration: some DeclSyntaxProtocol

  • 表示宏附着的原始声明节点;
  • 类型是 DeclSyntaxProtocol,表示可以是变量、函数、类型等;
  • 你可以从中提取关键元信息(如变量名、类型名、访问级别等)。

in context: some MacroExpansionContext

上下文对象,常用于:

  • 生成唯一名称(防止冲突);
  • 获取源文件路径、位置;
  • 报告诊断信息(如参数错误)。

4. 对等声明的展开位置

Peer 宏生成的声明会插入到与原声明相同的作用域中,而不是类型或函数内部

例如:

 @WithWrapper
 var name: String

展开后等同于:

 var name: String
 private var _name: String = ""

即:_namename 的“对等声明”,它们在同一语法级别上。


5. 示例解析

示例:为变量自动生成属性

用法
 struct User {
    @DebugEqual
    var userName: String = ""
 }
 
 // 展开后
 struct User {
    var userName: String = ""
     
    var debug_userName: String {
        "userName = (userName)"
    }
 }
实现
 @attached(peer, names: arbitrary)
 public macro DebugEqual() = #externalMacro(module: "McccMacros", type: "DebugEqualMacro")
 
 public struct DebugEqualMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntaxProtocol,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // 把通用声明转成变量声明
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              // 变量可鞥有多个绑定(var a = 1, b = 2),这里获取第一个。
              let binding = varDecl.bindings.first,
              // 获取变量名,比如”userName“
              let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
        else {
            return []
        }
 
 
        // 生成新的变量名,如 debug_username
        // raw: 的作用?原样插入这个标识符文本,不会加引号,也不会逃逸。这是写 Swift 宏时推荐的写法之一。
        return [
            """
            var debug_(raw: identifier): String {
                "(raw: identifier) = \((raw: identifier))"
            }
            """
        ]
    }
 }

6. 注意事项

  • PeerMacro 会生成多个完整的顶层声明节点,开发者需手动控制命名与作用域;
  • 若生成的名称不一致,建议配合 names: 标注宏声明;
  • 生成类型或函数声明时,需手动处理访问修饰符和重名冲突。

7. 总结

PeerMacro 是 Swift 宏系统中“横向扩展”的核心工具,它允许开发者在不修改原始声明的前提下添加紧密关联的辅助声明。适用于:

  • 分离逻辑与存储
  • 为现有属性扩展行为能力
  • 构建声明式属性模型

当你需要构建“围绕声明的附属结构”,PeerMacro 就是你的利器。

Swift Macros - 成员绑定宏

在 Swift 中,结构体和类的声明体(即 {} 中的内容)常常会包含许多重复或模式化的成员声明。为了提升开发效率并避免重复劳动,Swift 宏系统提供了一种用于自动生成成员声明的宏协议:MemberMacro。在 Swift 宏体系中,MemberMacro 是一种具有极高实用价值的宏协议,它专门用于在类型声明内部生成新的成员(如属性、方法、构造器等)。这种宏是典型的附加宏(attached macro) ,能够大幅减少重复成员定义的样板代码,提高类型声明的表达能力。

本节建议结合《Swift Macros - 宏之全貌》和《Swift Macros - 宏之协议》一并阅读,以便更好地理解宏在声明结构中的角色。

1. MemberMacro 的定义

MemberMacro 是一种 附加宏协议,用于将成员注入至类型声明体中。它只作用于结构体、类、actor、枚举这些具备声明体的类型定义,不能用于函数、变量或其他非类型声明。

它在 Swift 中的声明为:

 public protocol MemberMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax]
 }
参数名 类型 说明
node AttributeSyntax 当前宏调用的语法节点(包含宏名与参数)
declaration some DeclGroupSyntax 宏所附加的类型声明体,例如 structclass
context some MacroExpansionContext 提供诊断、源文件信息等上下文能力

你可以通过 MacroExpansionContext 提供的 diagnose() 方法抛出编译错误,也可以用 context.location(of:) 进行精确定位。

返回值为 [DeclSyntax],表示你希望宏注入的成员声明数组。例如你可以生成变量、函数、嵌套类型等内容:

 return [  "var id: String = UUID().uuidString",  "func reset() { self.id = UUID().uuidString }" ]
 .map { DeclSyntax(stringLiteral: $0) }

💡 注意:返回的成员会插入到原始类型声明体中,因此要避免命名冲突。

📌 使用限制

  • 只可用于具有声明体({})的类型定义:structclassenumactor
  • 不可用于 funcvarextension 等其他声明
  • 若注入的成员包含具名声明(如 var id),必须在宏声明中通过 names: 显式声明,以避免命名未覆盖错误(Declaration name 'id' is not covered by macro

2. 使用场景分析

MemberMacro 适用于所有需要自动生成类型成员的场景,特别是:

场景 示例 说明
自动生成协议实现 @AutoEquatable 自动实现 Equatable== 方法
自动添加辅助属性 @Observe 为属性生成 _xxx 存储与监控 getter
自动实现构造器 @AutoInit 基于属性自动生成初始化函数
自动生成默认值 @WithDefaults 为成员属性自动附加默认实现

3. 示例解析

示例1:AddID

用法:

 @AddID
 struct User {
  var name: String
 }
 
 // 等价于
 struct User {
  var name: String
  var id = UUID().uuidString
 }

实现:

 @attached(member, names: named(id))
 public macro AddID() = #externalMacro(
  module: "MyMacroImpl",
  type: "AddIDMacro"
 )
 
 public struct AddIDMacro: MemberMacro {
  public static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    return [
      "var id = UUID().uuidString"
    ].map { DeclSyntax(stringLiteral: $0) }
  }
 }

如果不明确名称

 @attached(member)

运行会报错:

 ❗️Declaration name 'id' is not covered by macro 'AddID'

说明你使用的是 @attached(member) 宏,但没有在宏声明中说明要生成的成员名字,Swift 宏系统默认是不允许你偷偷“注入”成员名的,除非你通过 names: 明确标注。

示例2:CodableSubclass

对于继承自某个父类的子类,我们希望自动生成 CodingKeysinit(from:) 方法.

用法

 class BaseModel: Codable {
    var name: String = ""
 }
 
 @CodableSubclass
 class StudentModel: BaseModel {
    var age: Int = 0
 }
 
 
 // 宏展开后等效于
 class StudentModel: BaseModel {
    var age: Int = 0
     
    private enum CodingKeys: String, CodingKey {
        case age
    }
 
    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.age = try container.decode(Int.self, forKey: .age)
    }
 }

实现

 @attached(member, names: named(init(from:)), named(CodingKeys))
 public macro CodableSubclass() = #externalMacro(module: "McccMacros", type: "CodableSubclassMacro")
 
 
 public struct CodableSubclassMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // 1. 验证是否是类声明
        guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
            throw MacroError.message("@CodableSubclass 只能用于类")
        }
         
        // 2. 验证是否有父类
        guard let inheritanceClause = classDecl.inheritanceClause,
              inheritanceClause.inheritedTypes.contains(where: { type in
                  type.type.trimmedDescription == "BaseModel" ||
                  type.type.trimmedDescription.contains("Codable")
              }) else {
            throw MacroError.message("@CodableSubclass 需要继承自 Codable 父类")
        }
         
        // 3. 收集所有存储属性
        let storedProperties = classDecl.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { $0.bindingSpecifier.text == "var" }
            .flatMap { $0.bindings }
            .compactMap { binding -> String? in
                guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
                    return nil
                }
                return pattern.identifier.text
            }
         
        // 4. 生成 CodingKeys 枚举
        let codingKeysEnum = try EnumDeclSyntax("private enum CodingKeys: String, CodingKey") {
            for property in storedProperties {
                "case (raw: property)"
            }
        }
         
        // 5. 生成 init(from:) 方法
        let initializer = try InitializerDeclSyntax("required init(from decoder: Decoder) throws") {
            // 调用父类解码器
            "try super.init(from: decoder)"
             
            // 创建容器
            "let container = try decoder.container(keyedBy: CodingKeys.self)"
             
            // 解码每个属性
            for property in storedProperties {
                "self.(raw: property) = try container.decode((raw: getTypeName(for: property, in: declaration)).self, forKey: .(raw: property))"
            }
        }
         
        return [DeclSyntax(codingKeysEnum), DeclSyntax(initializer)]
    }
     
    private static func getTypeName(for property: String, in declaration: some DeclGroupSyntax) -> String {
        for member in declaration.memberBlock.members {
            guard let varDecl = member.decl.as(VariableDeclSyntax.self) else { continue }
             
            for binding in varDecl.bindings {
                guard let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self),
                      identifierPattern.identifier.text == property else {
                    continue
                }
                 
                if let typeAnnotation = binding.typeAnnotation {
                    return typeAnnotation.type.trimmedDescription
                }
            }
        }
         
        // 默认返回 Any,如果找不到匹配
        return "Any"
    }
 }
 
 public enum MacroError: Error, CustomStringConvertible {
    case message(String)
     
    public var description: String {
        switch self {
        case .message(let text):
            return text
        }
    }
 }

4. 总结

MemberMacro 是 Swift 宏体系中连接语法结构与声明注入的关键机制。它让开发者能够根据类型结构自动生成成员,真正实现:

  • 结构自动扩展;
  • 代码样板消除;
  • 类型驱动式逻辑推导。

未来你可以将它与 AccessorMacroPeerMacro 等组合使用,构建更高层次的声明式元编程能力。

Swift Macros - 声明式独立宏

在 Swift 宏体系中,DeclarationMacro 是一种用途广泛的角色,专门用于生成声明级别的代码,如变量、函数、结构体等。它同样属于自由悬挂宏(freestanding macro)的一种,但与 ExpressionMacro 不同,它不会展开为表达式,而是生成一个或多个 完整的声明语法节点(DeclSyntax)

本节将深入讲解 DeclarationMacro 的定义、用途、特点,以及其参数、返回值的结构分析,并通过示例帮助你掌握其使用方式。

建议先阅读基础篇《Swift Macros - 宏之全貌》与协议篇《Swift Macros - 宏之协议》,以更好地理解本节内容。

1. DeclarationMacro 的定义

DeclarationMacro 协议由标准库提供,其定义如下:

 public protocol DeclarationMacro: FreestandingMacro {
  static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax]
 }

简而言之,声明式独立宏具备以下特性:

  • 触发位置:可直接作为独立语句出现在作用域中;
  • 作用对象:生成一个或多个完整的声明(如变量声明、函数定义);
  • 返回类型:必须是 [DeclSyntax] 数组,支持生成多个声明。

2. DeclarationMacro 的作用分析

核心作用

  • 在当前作用域中插入新的声明
  • 通过参数驱动,动态生成声明代码
  • 避免重复书写、提升可维护性与一致性

常见应用场景

场景 示例 说明
自动生成函数 #makeDebugFunction("log") 生成具名的调试函数
统一封装声明 #injectCommonImports() 插入一批通用 import 语句
构建配置项常量集 #defineKeys("id", "name") 根据传入字符串列表定义常量
静态信息注入 #generateBuildInfo() 生成包含版本、时间、构建号的静态变量

3. DeclarationMacro 的参数解析

ExpressionMacro 一样,DeclarationMacroexpansion 函数也接受以下两个参数:

of node: some FreestandingMacroExpansionSyntax

  • 代表宏本身的调用语法;
  • 可通过 .argumentList 访问用户传入的参数列表;
  • 每个参数都是一个 LabeledExprSyntax 类型,可以进一步分析是否为字面量、表达式等。

in context: some MacroExpansionContext

  • 提供宏展开的上下文信息;
  • 可用于生成唯一名称、获取调用源位置、报错诊断等;
  • ExpressionMacro 中的 context 功能完全一致。

4. DeclarationMacro 的返回值

返回类型:[DeclSyntax]

  • 宏必须返回一个 声明语法节点数组
  • 每个元素都必须是合法的声明类型(例如 VariableDeclSyntaxFunctionDeclSyntaxStructDeclSyntax等);
  • 所有返回的声明会被直接插入到调用宏的位置。
 return [
  DeclSyntax("let name = "Mccc""),
  DeclSyntax("let age = 30")
 ]

调用:

 #defineProfile()

展开:

 let name = "Mccc"
 let age = 30

5. DeclarationMacro 示例解析

示例1:定义常量

定义一个宏 #defineKeys,接受一组字符串参数,并为每个参数生成一个常量:

 @freestanding(declaration)
 public macro defineKeys(_ keys: String...) = #externalMacro(module: "McccMacros", type: "DefineKeysMacro")

实现:

 public struct DefineKeysMacro: DeclarationMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    let identifiers: [String] = try node.arguments.map {
      guard let stringLiteral = $0.expression.as(StringLiteralExprSyntax.self),
            let key = stringLiteral.segments.first?.description.trimmingCharacters(in: .init(charactersIn: """)) else {
        throw ASTError("#defineKeys 参数必须为字符串字面量")
      }
      return key
    }
 
    return identifiers.map { name in
      DeclSyntax("let (raw: name) = "(raw: name)"")
    }
  }
 }

调用:

 #defineKeys("id", "name", "email")

展开后:

 let id = "id"
 let name = "name"
 let email = "email"

示例2:生成通用 Imports

宏定义:

@freestanding(declaration)
public macro commonImports() = #externalMacro(module: "McccMacros", type: "ImportMacro")

实现:

public struct ImportMacro: DeclarationMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    return [
      DeclSyntax("import Foundation"),
      DeclSyntax("import SwiftUI"),
      DeclSyntax("import Combine")
    ]
  }
}

调用:

#commonImports()

展开:

import Foundation
import SwiftUI
import Combine

总结

  • DeclarationMacro 是声明级别的独立宏,适合生成变量、函数等完整声明;
  • 它通过 expansion 返回 [DeclSyntax],一次可插入多条声明;
  • 场景广泛,尤其适合模板生成、批量定义、封装声明逻辑等;
  • 相比表达式宏,它更接近“代码插入器”的角色。

Swift Macros - 表达式独立宏

在 Swift 宏体系中,ExpressionMacro 是一种非常重要且常用的角色。它专门用于生成表达式级别的代码,并且属于独立宏(freestanding macro) 的一种。

本节将深入讲解 ExpressionMacro 的定义、用途、特点,以及其参数、返回值的详细分析,帮助你全面掌握这一类型宏的设计与使用。

在阅读本节前,建议先了解基础篇《Swift Macros - 宏之全貌》和协议篇《Swift Macros - 宏之协议》,可以更流畅地理解本节内容。

1. ExpressionMacro 的定义

Swift 标准库中,ExpressionMacro 协议的定义如下:

 public protocol ExpressionMacro: FreestandingMacro {
  static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> ExprSyntax
 }

简而言之,表达式独立宏就是:

  • 触发位置:可以直接单独使用在表达式的位置;
  • 作用对象:生成一个新的 ExprSyntax 节点;
  • 典型场景:封装复杂逻辑、生成动态表达式、优化代码书写。

注意:ExpressionMacro 必须是 freestanding 的,意味着它本身不附加到其他声明上,而是以独立表达式的形式展开。

2. ExpressionMacro 的作用分析

核心作用

  • 生成一个完整的表达式节点(ExprSyntax
  • 简化复杂表达式的手写工作
  • 在编译期根据参数动态生成逻辑

常见应用场景

场景 示例 说明
自动封装日志 #log("message") 自动插入打印或记录代码
调试辅助工具 #dump(expr) 在调试时自动格式化输出
表达式改写 #optimize(expr) 将通用表达式展开成更高效的版本
自动计时 #measure { work() } 计算某段代码的执行时间

可以看出,凡是需要在编译期生成"一个表达式"的场景,都可以使用 ExpressionMacro 实现。

3. ExpressionMacro 的参数分析

of node: some FreestandingMacroExpansionSyntax

  • 代表宏调用语法本身。
  • node 包含了宏的名字参数列表调用位置等信息。
  • 通过解析 node,可以获取用户传递给宏的具体内容。

小提示:常用 node.argumentList 来解析参数。

例如,对于调用:

 #stringify(a + b)

node 会表示整个 #stringify(a + b),你可以从中取出 a + b 作为参数。


in context: some MacroExpansionContext

  • 提供宏展开时的上下文信息。

  • 可以用于:

    • 生成唯一名称;
    • 报告诊断错误或警告;
    • 获取节点的源代码位置;
    • 获取当前词法作用域。

context 是你在编写宏时的"万能工具箱",尤其在需要辅助信息(如生成辅助变量名、给出友好错误提示)时特别重要。

4. ExpressionMacro 的返回值分析

返回类型:ExprSyntax

  • 代表一个标准的 Swift 表达式;
  • 会直接替换调用宏的位置。

举个简单例子,假设你写了一个 @ExpressionMacro#double(x),展开后返回的是:

 ExprSyntax("((x) * 2)")

那么用户代码:

 let value = #double(21)

最终编译器看到的是:

 let value = (21 * 2)

注意:表达式宏必须返回单个表达式,不能直接返回语句、声明或其他结构。

5. ExpressionMacro 示例解析

示例1:生成字符串化表达式

 @freestanding(expression)
 public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "McccMacros", type: "StringifyMacro")
 
 public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression else {
            throw ASTError("stringify 宏必须至少传入一个参数")
        }
         
        return "((literal: argument.description), (argument))"
    }
 }

调用:

 let result = #stringify(a + b)

展开后等同于:

 let result = ("a + b", a + b)

示例2:加法

定义一个宏 #sum,用于在编译期间将一组整数字面量求和,提升运行时性能。

@freestanding(expression)
public macro sum(_ values: Int...) -> Int = #externalMacro(module: "McccMacros", type: "SumMacro")


public struct SumMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        
        // 确保传入的是整数字面量,并进行转换
        let values: [Int] = try node.arguments.map { element in
            // 逐个检查每个参数是否是 IntegerLiteralExprSyntax
            guard let literalExpr = element.expression.as(IntegerLiteralExprSyntax.self),
                  let intValue = Int(literalExpr.literal.text) else {
                throw ASTError("All arguments to #sum must be integer literals.")
            }
            return intValue
        }
        
        // 求和
        let sum = values.reduce(0, +)

        // 返回表达式
        return "(raw: sum)"
    }
}

调用:

let sums = #sum(1, 2, 3, 4)

展开后等同于:

let sums = 10

Swift Macros - 宏之协议

Swift 宏的强大源于其背后一套精巧严谨的协议体系。这些协议定义了:

  • 宏的行为规范:如何与编译器通信,如何生成语法树
  • 宏的能力边界:什么宏可以插入什么样的结构
  • 宏的输入输出约束:需要接受什么样的输入,返回什么样的输出

在 Swift 中, “宏 = 协议方法的实现” 。宏不会在运行时参与逻辑,而是在编译期间将协议方法转换为结构化代码。

本篇将深入解析这些协议的共性特征与调用方式,为你在后续实现各种角色宏打下统一的基础。

Swift 宏协议的共性特征

Swift 宏虽然分工明确(表达式宏、声明宏、成员宏等),但它们的实现方式高度统一,主要体现为以下特征:

编号 特征 描述
1 方法统一命名为 expansion 所有宏协议都实现 static func expansion(...) 作为展开主入口。
2 支持 throws 异常机制 展开过程中可中止并抛出诊断错误。
3 必带 context 参数 提供编译期上下文信息,是宏的“工具箱”。
4 必带 node 参数 表示宏的调用现场,如 #宏名(...)@宏名
5 输入输出皆为 Syntax 类型 宏只操作语法树,输入输出都是 SwiftSyntax 节点。
6 仅在编译期执行 宏不能访问运行时信息,所有逻辑基于静态源码。
7 返回类型严格固定 每种宏角色返回类型不同,且不可交叉使用。

1. 所有宏都实现 static func expansion(...)

Swift 宏协议统一使用 expansion 方法命名,使得不同类型的宏拥有相似的签名与调用习惯,极大降低学习与维护成本。

 // 各协议方法签名示例
 protocol ExpressionMacro {
    static func expansion(...) throws -> ExprSyntax
 }
 
 protocol DeclarationMacro {
    static func expansion(...) throws -> [DeclSyntax]
 }
  • 方法总是 static,因为宏不依赖实例
  • 输入是调用现场 node + 编译上下文 context
  • 输出是结构化语法树,如 ExprSyntaxDeclSyntax

2. 宏支持 throws,可中止并报告错误

所有宏的 expansion 方法都支持 throws,允许在发现语义错误时立即中止,并通过 context.diagnose(...) 抛出诊断信息,提升宏的可维护性与用户友好度。

错误提示.png

只需要在适当的地方抛出异常,你可以自行编辑异常的message,以便使用者更好的理解该异常。

 public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        throw ASTError("错误提示: the macro does not have any arguments")
    }
 }

你可以通过自定义错误类型(如 ASTError)提供清晰的人类可读信息,IDE 也会高亮定位到宏调用位置,提升调试体验。

3. context 宏的工具箱

每个宏都会收到一个 context 参数(类型为 some MacroExpansionContext),这是宏与编译器交互的主要手段,具备多项能力:

 public protocol MacroExpansionContext: AnyObject {
  func makeUniqueName(_ name: String) -> TokenSyntax
  func diagnose(_ diagnostic: Diagnostic)
  func location(of node: some SyntaxProtocol, at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode) -> AbstractSourceLocation?
  var lexicalContext: [Syntax] { get }
 }

它是宏与编译器沟通的桥梁,也是实现宏逻辑动态化的关键接口。以下是 Swift 宏系统中 MacroExpansionContext 协议四个核心成员的作用详解,按重要性分层说明:

3.1 命名避冲突:makeUniqueName(_:)

自动生成唯一标识符,避免命名冲突

 // 使用场景:临时变量、缓存值、内部标识符等场景。
 let uniqueVar = context.makeUniqueName("result")
 // 输出结果可能是 `result_7FE3A1` 之类的唯一名称

3.2 诊断报告:diagnose(_:)

核心作用:编译时错误报告系统

  • 多级诊断:支持 error / warning / note 三种严重级别
  • 精准定位:关联到具体语法节点(如高亮错误位置)
  • 修复建议:可附加自动修复方案(FixIt)
 public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
     
        context.diagnose(Diagnostic(node: node, message: MacroDiagnostic.deprecatedUsage))
        throw ASTError("错误提示: xxxxxx")
    }
 }

某些宏过期时,可以通过 context.diagnose(...) 给于警告提醒。

警告提醒.png

DiagnosticMessage

这里的 Diagnostic.message 需要一个实现 DiagnosticMessage 协议的实例。

 public protocol DiagnosticMessage: Sendable {
 /// The diagnostic message that should be displayed in the client.
 var message: String { get }
 
 /// See ``MessageID``.
 var diagnosticID: MessageID { get }
 
 var severity: DiagnosticSeverity { get }
 }
  • message:诊断信息的信息

  • diagnosticID:诊断 ID

  • severity:诊断严重程度

     public enum DiagnosticSeverity {
        case error   // 编译错误,阻止构建。
        case warning // 编译警告,不阻止构建。
        case note     // 提示信息,常用于补充说明。
     }
    

3.3 源码定位:location(of:at:filePathMode:)

可定位到调用宏的具体源代码行列,便于诊断、代码导航、日志标注等用途:

 public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
 ) throws -> ExprSyntax {   
    let loc = context.location(of: node, at: .afterLeadingTrivia, filePathMode: .fileID )
    ......
 }

func location( of node: some SyntaxProtocol, at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode ) -> AbstractSourceLocation?

AbstractSourceLocation 返回值中,可以获取以下信息:

public struct AbstractSourceLocation: Sendable {
/// 文件位置
public let file: ExprSyntax

/// 行的位置
public let line: ExprSyntax

/// 字符位置
public let column: ExprSyntax
  • 四种定位模式

    enum PositionInSyntaxNode {
        case beforeLeadingTrivia  // 包含注释/空格
        case afterLeadingTrivia   // 实际代码起始处
        case beforeTrailingTrivia // 实际代码结束处
        case afterTrailingTrivia  // 包含尾部注释
    }
    
  • 路径显示控制

    • .fileID"ModuleName/FileName.swift"(安全格式)
    • .filePath → 完整系统路径(调试用)

3.4 词法作用域追踪:lexicalContext

核心作用:获取词法作用域上下文

以数组形式,记录从当前节点向外的层层包裹结构;

经过脱敏处理(如移除函数体、清空成员列表)。

// 检查是否在类方法中
let isInClassMethod = context.lexicalContext.contains { 
    $0.is(FunctionDeclSyntax.self) && 
    $0.parent?.is(ClassDeclSyntax.self) != nil
}

4. node 调用现场信息

每个宏的 expansion 方法,除了 context 外,还会接收一个 node 参数,类型通常是 some SyntaxProtocol(如 FreestandingMacroExpansionSyntaxAttributeSyntax 等)。

它代表了宏的调用现场——也就是源码中触发宏展开的那段语法结构。

简单理解:node 就是“#宏名(...)”或“@宏名” 这一整段的解析结果。

以自由宏为例,node 类型通常是 FreestandingMacroExpansionSyntax,它包含了调用宏时的所有组成元素:

public protocol FreestandingMacroExpansionSyntax: SyntaxProtocol {
  var pound: TokenSyntax { get set }  // "#" 符号
  var macroName: TokenSyntax { get set }  // 宏名
  var genericArgumentClause: GenericArgumentClauseSyntax? { get set } // 泛型参数
  var leftParen: TokenSyntax? { get set }  // 左括号 "("
  var arguments: LabeledExprListSyntax { get set }  // 参数列表
  var rightParen: TokenSyntax? { get set }  // 右括号 ")"
  var trailingClosure: ClosureExprSyntax? { get set }  // 尾随闭包
  var additionalTrailingClosures: MultipleTrailingClosureElementListSyntax { get set }  // 多个尾随闭包
}

具体能做什么?

通过解析 node,可以在宏内部获取宏调用时传递的信息,从而进行自定义生成:

  • 提取参数:解析 arguments,得到用户传入的内容;
  • 读取宏名:从 macroName 获取调用者使用的名字(有些宏支持重名扩展);
  • 处理泛型:如果 genericArgumentClause 存在,可以根据泛型参数生成不同代码;
  • 解析闭包:支持分析和利用用户传递的尾随闭包;
  • 实现自定义行为:比如根据传入参数数量、类型、值,决定生成什么样的代码。

示例

public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
) throws -> ExprSyntax {
    // 取出第一个参数
    guard let firstArg = node.arguments.first?.expression else {
        throw ASTError("缺少参数")
    }
    
    // 根据参数生成不同表达式
    return "print((firstArg))"
}

小结: node = 宏调用时的源码快照context = 辅助功能工具箱

两者结合使用,才能让宏既能理解调用现场,又能灵活地生成对应代码。

5. 输入输出皆基于 Syntax 节点

Swift 宏以结构化 AST(抽象语法树)为基础,输入输出都基于 SwiftSyntax 类型,例如:

  • 输入:AttributeSyntaxFreestandingMacroExpansionSyntaxDeclSyntaxProtocol
  • 输出:ExprSyntax[DeclSyntax][AccessorDeclSyntax] 等。

这种设计保证了宏生成的代码具备:

  • 与手写代码一致的结构完整性;
  • 良好的可分析性与可重构性;
  • 自动享受 IDE 语法高亮、错误检测等支持。

Swift 宏不是简单拼接字符串,而是真正生成 AST。

6. 宏只运行于编译时

Swift 宏只能在编译期运行,这意味着它们不能访问运行时信息、全局变量、实例状态或外部服务。所有宏的行为都必须建立在静态源代码、类型系统和语法结构之上。

这为宏提供了如下保证:

  • 可预测性:展开结果与运行环境无关,确保行为一致;
  • 可分析性:工具链可以分析宏行为,进行语法检查与补全;
  • 可维护性:宏代码不会隐藏运行时副作用,有利于重构和测试。

开发者在编写宏时,也应遵循“编译时思维”,尽可能将逻辑转化为静态分析与结构转换。

7. 每种宏的返回类型固定

每个宏协议都明确限定了其 expansion 方法的返回类型,这种限制具有强约束力:

宏协议 返回类型
ExpressionMacro ExprSyntax
DeclarationMacro [DeclSyntax]
MemberMacro [DeclSyntax]
AccessorMacro [AccessorDeclSyntax]
BodyMacro [CodeBlockItemSyntax]
ExtensionMacro [ExtensionDeclSyntax]
MemberAttributeMacro [AttributeSyntax]

这种强约束带来:

  • 类型安全;
  • 生成结果合法;
  • 避免不同宏角色混淆使用。

比如:成员宏只能生成成员声明,不能直接生成表达式或代码块。

总结

Swift 宏协议的结构化设计,使得宏具备了安全、清晰、灵活的特性。无论你编写哪种类型的宏,理解 expansion 的统一调用模式、context 工具箱能力、node 的语法抽象、以及 Syntax 类型的输入输出机制,都是构建可靠宏逻辑的基础。

在接下来的章节中,我们将深入每一种宏协议(如 ExpressionMacroDeclarationMacro 等),并结合实际案例,帮助你实现更多有趣且实用的 Swift 宏。

Swift Macros - SwiftSyntax 节点指南

版本:2025.04.27|维护者:Mccc|欢迎共同维护与补充!

在编写 Swift 宏时,你将频繁与 SwiftSyntax 打交道。SwiftSyntax 将源码拆解为结构化的语法节点(Syntax)树,这些节点覆盖了表达式、声明、语句、类型、模式、属性等各个层面。

本篇文章提供一个实用速查表,帮助你快速了解各类常见语法节点的用途与构造方法,便于高效构建宏所需的代码结构。

目录

1. 表达式(ExprSyntax)

用于表示各种计算表达式、函数调用、字面量等,是最常见的语法结构之一。

名称 描述 快速构造示例
ArrayExprSyntax 数组表达式 [a, b, c] ArrayExprSyntax(elements: [...])
BooleanLiteralExprSyntax 布尔字面量 true / false BooleanLiteralExprSyntax(value: true)
IntegerLiteralExprSyntax 整数字面量 123 IntegerLiteralExprSyntax(literal: "123")
FloatLiteralExprSyntax 浮点字面量 1.23 FloatLiteralExprSyntax(floatingDigits: "1.23")
StringLiteralExprSyntax 字符串 "abc" StringLiteralExprSyntax(content: "abc")
IdentifierExprSyntax 标识符 foo IdentifierExprSyntax(identifier: .identifier("foo"))
FunctionCallExprSyntax 函数调用 foo(a, b) FunctionCallExprSyntax(calledExpression: ..., arguments: [...])
MemberAccessExprSyntax 成员访问 a.b MemberAccessExprSyntax(base: ..., name: .identifier("b"))
PrefixOperatorExprSyntax 前缀操作 -a PrefixOperatorExprSyntax(operator: "-", expression: ...)
PostfixOperatorExprSyntax 后缀操作 a! PostfixOperatorExprSyntax(expression: ...)
NilLiteralExprSyntax 空值 nil NilLiteralExprSyntax()
ClosureExprSyntax 闭包 { a in a + 1 } ClosureExprSyntax(parameters: ..., statements: [...])
TupleExprSyntax 元组 (a, b) TupleExprSyntax(elements: [...])
TryExprSyntax try 表达式 TryExprSyntax(expression: ...)
AwaitExprSyntax await 表达式 AwaitExprSyntax(expression: ...)
AsExprSyntax 类型转换 as AsExprSyntax(expression: ..., type: ...)
IsExprSyntax 类型检查 is IsExprSyntax(expression: ..., type: ...)
TernaryExprSyntax 三目表达式 a ? b : c TernaryExprSyntax(condition: ..., thenExpr: ..., elseExpr: ...)
SequenceExprSyntax 表达式序列 1 + 2 * 3 SequenceExprSyntax(elements: [...])

💡 技巧: 中缀表达式(如 +, -, *)统一由 SequenceExprSyntax 表示,不再有 BinaryExpr。

2. 声明(DeclSyntax)

表示变量、函数、类型、协议等的定义,是构建宏时生成结构代码的核心组成。

名称 描述 快速构造示例
VariableDeclSyntax 变量 let/var VariableDeclSyntax(bindingSpecifier: "let", bindings: [...])
FunctionDeclSyntax 函数 FunctionDeclSyntax(name: "foo", signature: ..., body: ...)
StructDeclSyntax 结构体 StructDeclSyntax(identifier: "Foo", memberBlock: ...)
ClassDeclSyntax ClassDeclSyntax(identifier: "Foo", memberBlock: ...)
EnumDeclSyntax 枚举 EnumDeclSyntax(identifier: "Foo", memberBlock: ...)
ExtensionDeclSyntax 扩展 ExtensionDeclSyntax(extendedType: ..., memberBlock: ...)
ProtocolDeclSyntax 协议 ProtocolDeclSyntax(identifier: "Foo", memberBlock: ...)
ImportDeclSyntax 导入模块 ImportDeclSyntax(path: ["Foundation"])
TypeAliasDeclSyntax 类型别名 TypeAliasDeclSyntax(identifier: "Alias", type: ...)
AssociatedTypeDeclSyntax 协议中关联类型 AssociatedTypeDeclSyntax(identifier: "T")
MacroDeclSyntax 宏声明 MacroDeclSyntax(identifier: "MyMacro")
OperatorDeclSyntax 自定义操作符声明 OperatorDeclSyntax(operatorKeyword: "operator", name: "+")

3. 语句(StmtSyntax)

用于构建控制流程语句(if、guard、switch 等)和函数体内逻辑结构。

名称 描述 快速构造示例
IfStmtSyntax if 语句 IfStmtSyntax(conditions: [...], body: ...)
GuardStmtSyntax guard 语句 GuardStmtSyntax(conditions: [...], body: ...)
WhileStmtSyntax while 循环 WhileStmtSyntax(conditions: [...], body: ...)
RepeatWhileStmtSyntax repeat-while 循环 RepeatWhileStmtSyntax(body: ..., condition: ...)
ForStmtSyntax for-in 循环 ForStmtSyntax(pattern: ..., inExpr: ..., body: ...)
SwitchStmtSyntax switch 分支 SwitchStmtSyntax(expression: ..., cases: [...])
ReturnStmtSyntax return 返回 ReturnStmtSyntax(expression: ...)
ThrowStmtSyntax 抛出异常 ThrowStmtSyntax(expression: ...)
BreakStmtSyntax break 跳出 BreakStmtSyntax()
ContinueStmtSyntax continue 继续 ContinueStmtSyntax()
DeferStmtSyntax defer 延后执行 DeferStmtSyntax(body: ...)

4. 类型(TypeSyntax)

用于表示类型声明,包括简单类型、数组、可选、元组、函数类型等。

名称 描述 快速构造示例
SimpleTypeIdentifierSyntax 基本类型 Int, String SimpleTypeIdentifierSyntax(name: "Int")
OptionalTypeSyntax 可选类型 Int? OptionalTypeSyntax(wrappedType: ...)
ArrayTypeSyntax 数组类型 [Int] ArrayTypeSyntax(elementType: ...)
DictionaryTypeSyntax 字典类型 [K: V] DictionaryTypeSyntax(keyType: ..., valueType: ...)
TupleTypeSyntax 元组类型 (Int, String) TupleTypeSyntax(elements: [...])
FunctionTypeSyntax 函数类型 (Int) -> Bool FunctionTypeSyntax(parameters: [...], returnType: ...)
AttributedTypeSyntax 带属性类型 @Sendable AttributedTypeSyntax(attributes: [...], baseType: ...)
SomeTypeSyntax some 类型 SomeTypeSyntax(baseType: ...)
MetatypeTypeSyntax .Type .Protocol MetatypeTypeSyntax(baseType: ..., typeOrProtocol: ...)
ExistentialTypeSyntax any 协议类型 ExistentialTypeSyntax(type: ...)

5. 模式(PatternSyntax)

用于 let/var 绑定、模式匹配等结构。

名称 描述 快速构造示例
IdentifierPatternSyntax 标识符模式 IdentifierPatternSyntax(identifier: .identifier("name"))
TuplePatternSyntax 元组模式 TuplePatternSyntax(elements: [...])
WildcardPatternSyntax 通配符 _ WildcardPatternSyntax()
ValueBindingPatternSyntax let/var 模式 ValueBindingPatternSyntax(bindingSpecifier: "let", pattern: ...)
ExpressionPatternSyntax 表达式匹配 ExpressionPatternSyntax(expression: ...)

6. 属性(AttributeSyntax)

用于修饰声明,包括标准属性和自定义属性包装器。

名称 描述 快速构造示例
AttributeSyntax 标准属性 AttributeSyntax(attributeName: "available")
CustomAttributeSyntax 自定义属性 CustomAttributeSyntax(attributeName: "MyWrapper")

7. 宏(MacroExpansionSyntax)

专门用于表示宏的使用与展开。

名称 描述 快速构造示例
FreestandingMacroExpansionSyntax 表达式独立宏 #stringify(x) FreestandingMacroExpansionSyntax(macroName: "stringify", arguments: [...])
AttributeMacroExpansionSyntax 属性宏 @MyMacro AttributeMacroExpansionSyntax(macroName: "MyMacro", arguments: [...])
AccessorMacroExpansionSyntax Accessor 宏(getter/setter) AccessorMacroExpansionSyntax(macroName: "MyAccessor")

8. 其他常用节点

名称 描述 快速构造示例
CodeBlockSyntax 一组语句块 { ... } CodeBlockSyntax(statements: [...])
MemberDeclListSyntax 成员声明列表 MemberDeclListSyntax(members: [...])
ParameterClauseSyntax 参数签名 (x: Int) ParameterClauseSyntax(parameters: [...])
TupleExprElementListSyntax 元组表达式元素列表 TupleExprElementListSyntax(elements: [...])
TokenSyntax 基础 Token,如标识符/关键字等 .identifier("foo"), .keyword(.func)
SourceFileSyntax 整个 Swift 源文件语法结构 SourceFileSyntax(statements: [...])

来源

为了确保内容的准确性和时效性,欢迎您定期参考官方文档和资源:

swift-syntax源码

SwiftSyntax文档

如有更新,提交MR,一起维护它。

Swift Macros - 宏之语法树

在正式深入宏的世界之前,我们必须理解一个核心概念:Syntax(语法节点) 。它不仅是 Swift 宏生成和操作代码的“原材料”,更是编译器理解代码结构的基础。

语法树(Syntax Tree) 是代码生成与转换的基础数据结构。理解语法树的结构和操作方式是掌握宏开发的关键第一步。

本篇文章旨在帮助你掌握 SwiftSyntax 提供的语法节点体系、如何从语法树中提取信息、如何构建语法树,以及这些能力在宏中的实战应用,为你后续理解宏协议与宏实现打下扎实的基础。

1. 为什么需要了解语法树

在 Swift 宏中:

  • 你处理的不是“字符串代码”,而是结构化的 语法树
  • 宏的输入是语法节点,输出也是语法节点
  • 宏的参数、上下文、返回值都来自语法结构

简言之:不了解语法树,就无法理解宏的工作方式。

2. Syntax 与 SwiftSyntax

2.1 什么是 Syntax?

Syntax 是对 Swift 源代码的结构化表示。Swift 编译器在编译时,将源代码依次转换为:

 🟡 源代码 → 🟢 词法分析 → 🔵 语法分析 → 🟣 语法树 → 🔴 宏处理 → 🟤 编译 

每一行 Swift 代码,都会被解析为一棵树状结构,树上的每个节点都是一个语法片段,称为 Syntax 节点(Syntax Node)

2.2 常见语法节点类型举例

节点类别 示例类型 对应代码示例
表达式节点 InfixOperatorExprSyntax a + b
声明节点 VariableDeclSyntax let x = 1
语句节点 ReturnStmtSyntax return result
类型节点 TypeAnnotationSyntax : Int

每种节点类型都有明确的结构定义,可通过 SwiftSyntax 操作。

2.3 为什么宏操作的是 Syntax?

在 Swift 宏中,你不是直接操作字符串文本,也不是直接修改源代码,而是:

🟡 读取 Syntax → 🟢 生成新的 Syntax → 🔵 交给编译器继续处理

直接操作结构化的语法树,能带来:

优势 说明
安全性高 生成的语法结构不会导致非法代码
可读性强 结构清晰,易于调试和理解
自动格式化 编译器可自动对齐风格,无需手动调整
易于优化 编译器直接理解语法结构,可执行更智能的优化

所以,你可以把 Swift 宏想象成是在编辑一棵代码树(Syntax Tree) ,而你的任务,就是在这棵树上插入、修改、替换节点。

2.3 语法树结构示意

可以用一张简单图理解:

 源代码
  └──> 词法分析(Tokenize)
        └──> 语法分析(Parse)
              └──> 生成 Syntax Tree(语法树)

每个 Syntax 节点都有:

  • 节点类型(比如表达式、声明、类型等)
  • 子节点(例如函数调用有函数名、参数列表子节点)
  • 源代码位置信息(可以定位到具体代码行列)
  • 描述信息(可以输出源代码片段)

以代码 print(a + b) 为例,它的语法树大致如下:

对应的 Syntax 树结构大致是:

 FunctionCallExprSyntax
 ├── calledExpression: DeclReferenceExprSyntax                 // 不是 IdentifierExprSyntax
 │   └── baseName: .identifier("print")                       // 标识符节点
 ├── leftParen: .leftParen                                     // 左括号
 ├── arguments: LabeledExprListSyntax                         // 参数列表
 │   └── [0]: LabeledExprSyntax                               // 参数元素
 │       ├── expression: InfixOperatorExprSyntax               // 中缀表达式
 │       │   ├── leftOperand: DeclReferenceExprSyntax("a")
 │       │   ├── operator: BinaryOperatorExprSyntax("+")
 │       │   └── rightOperand: DeclReferenceExprSyntax("b")
 └── rightParen: .rightParen                                   // 右括号

这种树形结构确实体现了宏系统的核心优势:

特性 语法树体现 宏系统收益
层次化 表达式嵌套(InfixOperatorExpr 作为 FunctionCall 的子节点) 允许递归处理复杂表达式
类型安全 每个节点类型明确(如区分 DeclReferenceBinaryOperator 编译时验证生成代码合法性
可组合性 独立节点通过父子关系组合(如操作符左右操作数) 支持模块化代码生成
精准定位 每个节点包含位置信息(leading/trailing trivia) 实现精确的错误诊断

2.5 SwiftSyntax 的协议体系

SwiftSyntax 中的节点都遵循一套协议:

协议名 描述
SyntaxProtocol 所有节点的基类协议
DeclSyntaxProtocol 声明类节点
ExprSyntaxProtocol 表达式类节点
TypeSyntaxProtocol 类型相关节点
StmtSyntaxProtocol 语句节点

这些协议能帮助你在代码中进行统一操作与类型匹配。

3. 如何从语法树中提取信息?

3.1 .as(...) 类型转换

 if let call = expr.as(FunctionCallExprSyntax.self) {
    let functionName = call.calledExpression.description
 }

3.2 访问节点字段

 let structDecl = decl.as(StructDeclSyntax.self)
 let name = structDecl?.identifier.text
 let members = structDecl?.memberBlock.members

3.3 遍历子节点

 for child in node.children(viewMode: .all) {
    print(child.syntaxNodeType)
 }

4. 如何构建语法节点?

4.1 使用字符串构造

 let expr: ExprSyntax = "1 + 2"

这是最常用且便捷的构造方式,适合简单的宏输出场景。

4.2 使用 SwiftSyntaxBuilder 构造复杂结构

 let one = ExprSyntax("1")
 let two = ExprSyntax("2")
 let plus = TokenSyntax.binaryOperator("+")
 let expr = InfixOperatorExprSyntax(
    leftOperand: one,
    operatorOperand: plus,
    rightOperand: two
 )

适用于需要控制每个组成部分、生成复杂结构的宏实现。

5. (raw:):安全插入语法节点

Swift 宏返回 ExprSyntax 时常见写法是:

 return "(raw: value)"

这和普通的字符串插值有什么区别?

  • 错误写法:生成的是字符串
 let sum = 10
 return "(sum)" // 实际生成的是字符串字面量 "10"
  • 正确写法:使用 raw: 插入表达式
 let sum = 10
 return "(raw: sum)" // 生成真正的数字表达式 10

为什么推荐使用 (raw:)

场景 不使用 raw 使用 raw
插入 Int "10"(字符串) 10(数字)
插入表达式 "(a + b)"(字符串) a + b(语法结构)

这能确保生成的是 合法的语法节点,而非拼接的字符串,避免类型错误。

  • 保持类型正确性(比如数字就是数字,表达式就是表达式)
  • 避免字符串包裹(防止出现 "10" 这种非预期结果)
  • 直接生成合法的 Syntax 节点

6. 示例:实现一个表达式宏 #sum(...)

下面是一个简单的宏,它可以将多个整数参数相加:

宏声明

 @freestanding(expression)
 public macro sum(_ values: Int...) -> Int = #externalMacro(module: "McccMacros", type: "SumMacro")

宏实现

 public struct SumMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
         
        // 解析所有参数,确保是整数
        let values: [Int] = try node.arguments.map { element in
            guard let literalExpr = element.expression.as(IntegerLiteralExprSyntax.self),
                  let intValue = Int(literalExpr.literal.text) else {
                throw ASTError("All arguments to #sum must be integer literals.")
            }
            return intValue
        }
         
        // 计算总和
        let sum = values.reduce(0, +)
 
        // 直接返回表达式
        return "(raw: sum)"
    }
 }

使用示例

 let total = #sum(1, 2, 3, 4)

宏展开后:

 let total = 10

6. 小结

在 Swift 宏系统中,你要掌握的不是字符串拼接技巧,而是:

  • 如何识别语法节点类型(如函数、变量、表达式)
  • 如何提取节点信息(名称、参数、属性等)
  • 如何构建语法结构(表达式、语句、声明等)
  • 如何插入语法节点(使用 (raw:) 保证结构合法)

语法树是宏系统的“语言”,也是宏生成代码的唯一通道。

Swift Macros - 宏角色与命名控制

在 Swift 宏系统中,宏类型(Macro Kind)宏角色(Macro Role)命名说明符(Name Specifier) 共同决定了宏的使用范围和生成内容的可控性。

  • 宏类型 决定宏的附着方式;
  • 宏角色 决定宏可以干什么;
  • 命名说明符 决定宏生成的内容叫什么;

理解这三者,是编写稳定、可维护、具协作性的宏的前提。

1. 宏类型

表示宏的附着方式,它分为两类:

  • 独立宏(Freestanding) :使用 @freestanding(...) 标记,独立于任何已有声明,适合生成表达式或新的声明语句。
  • 绑定宏(Attached) :使用 @attached(...) 标记,附着在已有声明(如类型、函数、属性)上,用于扩展或修改它们的结构。
类型 展开位置 可生成内容 示例用途
独立宏 代码中的任意表达式或声明位置 表达式、声明语句(变量、类型、函数等) 生成常量、表达式等
绑定宏` 类型、函数、属性、扩展等的前后或内部位置 成员、访问器、扩展、函数体等 注入属性、协议实现等

📌 独立宏使用 #宏名(...) 语法,绑定宏使用 @宏名 修饰已有声明。

2. 宏角色:Swift 宏的职责划分与展开场景

Swift 宏不是一刀切的,它有明确的“职责划分”,这些职责被称为 宏角色(Macro Role) 。每一个宏都必须声明其“角色”,告诉编译器它将在什么位置展开,以及要生成什么类型的语法结构。

宏角色可以分为以下几类:

宏角色 宏描述 对应协议名 宏用途示例
@freestanding(expression) 表达式独立宏 ExpressionMacro 替换表达式,生成新的表达式
@freestanding(declaration) 声明式独立宏 DeclarationMacro 插入变量、函数、类型等声明
@attached(member) 成员绑定宏 MemberMacro 向类型内部插入成员(变量、函数等)
@attached(peer) 对等绑定宏 PeerMacro 插入与目标声明并列的新声明
@attached(accessor) 访问器绑定宏 AccessorMacro 插入属性访问器(get、set等)
@attached(extension) 扩展绑定宏 ExtensionMacro 插入扩展声明(extension
@attached(memberAttribute) 成员属性绑定宏 MemberAttributeMacro 修改成员属性上的注解或特性
@attached(body) 替换声明体的绑定宏 BodyMacro 替换函数、方法或计算属性的函数体

宏角色赋予了宏“编译期插件”的能力,开发者可以选择正确的角色,把宏的能力注入到特定结构,而不是无控制地生成代码。因此,宏角色也被称为 “Swift 宏的功能标识”

如何选择正确的宏角色?

这取决于你想生成的内容:

想做的事情 应使用的宏角色
生成一个表达式(如日志宏) @freestanding(expression)
在某个位置生成一个函数/类型等 @freestanding(declaration)
向已有类型添加属性、方法等成员 @attached(member)
基于某声明并列插入“配套”声明 @attached(peer)
给属性自动加上访问器(get/set) @attached(accessor)
给类型添加额外扩展(如协议实现) @attached(extension)
修改已有成员的修饰符或属性注解 @attached(memberAttribute)
自动生成函数或计算属性的 body @attached(body)

3. 命名说明符:控制宏生成内容的命名方式

绑定宏生成代码时,常会生成具名实体:函数、变量、类型、扩展等。默认情况下,这些名称不明确,导致:

  • 多宏协作时命名冲突;
  • 工具链无法提供跳转、补全支持;
  • 宏之间无法互相引用生成内容;
  • 测试和验证困难。

因此 Swift 引入了 命名说明符(name specifiers) ,允许在宏声明时指定生成内容的命名方式。

命名说明符的种类

名称方式 说明 示例生成内容
named("xxx") 直接指定名称 func makePreview()
prefixed("xxx_") 给原始名称加前缀 var debug_name: String
suffixed("Async") 给原始名称加后缀 func saveAsync()
overloaded 表示该成员为同名重载 func log(level:)
arbitrary 自定义命名,适用于工具生成辅助结构 _MetaHelper__mapTable

使用范围说明

所有 @attached(...) 宏 和 @freestanding(declaration)@freestanding(expression) 不支持,因为它不生成命名实体。

使用时机与建议

使用时机:

  • 生成对外可见代码(函数、属性);
  • 希望多宏协作,避免冲突;
  • 需要支持代码跳转、补全、文档;
  • 生成 DSL 或辅助结构。

使用建议:

  • 统一前缀/后缀风格,方便识别;
  • _ 或命名空间隐藏内部实现,避免命名污染;
  • 通过具名成员留钩子供其他宏或模块访问;
  • 谨慎使用 arbitrary,仅限工具生成结构;
  • 表达式宏不需要命名说明符。

命名说明符的示例

1. 给类型自动添加一个调试属性,属性名带前缀避免冲突

 @attached(peer, names: arbitrary)
 public macro DebugEqual() = #externalMacro(module: "McccMacros", type: "DebugEqualMacro")

命名说明符示例.jpg

4. 最后

Swift 宏并非简单模板,而是一套基于类型系统和结构规则的元编程能力。

  • 宏类型告诉你“宏怎么附着”;
  • 宏角色告诉你“宏能做什么”;
  • 命名说明符告诉你“生成的内容叫什么”。

三者合力,使宏在复杂多模块环境中保持结构清晰、命名隔离与语义明确。

理解宏的职责控制与命名控制,是构建健壮、可维护宏功能的基石。

后续文章将详细拆解各宏角色的使用方式、限制与最佳实践,配合命名说明符示例,帮助你构建结构良好、可组合的宏能力体系。

📌 想打造真实可维护的宏框架?必须先理解角色,控制命名。

Swift Macros - 宏之全貌

1. 宏的定义

Swift 宏(Macro) 是一种在编译期执行的代码生成机制。开发者可以通过简洁的语法标记,在源代码中自动插入、替换或补充逻辑,从而实现样板代码的自动化。

Swift 宏建立在语法树与类型系统之上,具备类型安全语义明确可预测的元编程特性。

宏结构解析.png

为什么使用宏?

Swift 宏的优势体现在以下几个方面:

  • 编译期执行,零运行时开销 宏在编译阶段完成代码展开,避免运行时反射或动态调用的性能负担。
  • 减少样板代码,提升开发效率 无需手动实现 EquatableCodable、监听器等重复性逻辑,宏可以自动生成这些代码。
  • 类型安全,语法无缝衔接 宏展开后的代码与手写代码一样,会经历完整的语法与类型验证,确保可靠性与一致性。

2. 宏的设计原则

Swift 宏的设计秉承“显式、安全、可预测”三大原则,避免“魔法式”的隐式行为:

原则 说明
显式调用 宏必须通过明确语法标记使用,开发者清晰可见。
类型检查 宏生成的代码会经过完整的类型系统验证,不会绕过语言规则。
可预测展开 宏的展开逻辑必须是稳定的、可预期的,结果不会因外部环境而改变。

宏不是魔法,它并不神秘,也不凌驾于语言规则之上。你写下的每一个宏调用,都将以可读、可测、可调试的方式插入源代码中。

3. 宏的原理

Swift 宏基于编译器插件(Compiler Plug-in) 机制运行,整个过程发生在编译期,并受到严格的沙盒限制。

宏的展开流程

宏的扩展.png

  1. 提取宏调用:编译器识别源码中的宏语法,并生成对应的原始语法树(Raw Syntax Tree)。
  2. 发送到宏插件:宏语法树被发送至对应插件,该插件在沙盒中以独立进程运行。
  3. 执行宏逻辑:插件处理语法树并生成新的代码片段(语法节点)。
  4. 插入并继续编译:新生成的语法节点被插入原始源码,参与后续的编译过程。

宏的安全性与纯粹性

为了确保宏系统的 安全、稳定与可控性,Swift 从两个维度对宏行为做出约束:

系统隔离:沙盒机制

所有宏插件运行在独立的沙盒进程中,Swift 对其能力进行了严格限制:

  • ✖️ 禁止访问文件系统(如 FileManager
  • ✖️ 禁止发起网络请求
  • ✖️ 禁止调用系统级 API

这些限制是编译器层面的强制规定,一旦访问受限资源,会立即报错,例如:

 "The file “xxx” couldn’t be opened because you don’t have permission to view it."

因此,即使使用第三方宏插件,也无需担心其在背后执行未授权的操作。

设计哲学:纯粹性原则

Swift 鼓励将宏视为纯函数 —— 相同输入始终生成相同输出。这有助于:

  • 提高宏行为的可预测性
  • 避免构建结果因环境不同而变化
  • 支持编译器缓存宏结果,提升性能

推荐做法

  • ✔️ 仅依赖编译器传入的语法树与上下文
  • ✔️ 避免访问系统环境、网络、文件
  • ✔️ 生成稳定、可测、可重现的代码

不建议行为

  • ✖️ 使用 UUID()Date() 等生成动态值
  • ✖️ 使用随机数作为默认值
  • ✖️ 在多个宏之间共享全局上下文或隐式状态

这些行为虽然 技术上允许,但会破坏宏的一致性,导致难以调试、不可复现的构建结果。

4. 宏角色与命名说明符:Swift 宏的职责与命名控制

Swift 宏并非千篇一律,它具备明确的职责划分,这种职责由编译器通过一套称为 宏角色(Macro Role) 的机制识别和执行。

4.1 宏角色:Swift 宏的功能标识

宏角色决定了一个宏可以做什么。Swift 中的宏主要分为两类:

  • 独立宏(Freestanding) :使用 @freestanding(...) 标记,独立于任何已有声明,适合生成表达式或新的声明语句。
  • 绑定宏(Attached) :使用 @attached(...) 标记,附着在已有声明(如类型、函数、属性)上,用于扩展或修改它们的结构。

每种宏角色都对应特定的协议,定义其展开行为:

宏角色 描述 协议名 示例用途
@freestanding(expression) 表达式独立宏 ExpressionMacro 替换或扩展表达式
@freestanding(declaration) 声明式独立宏 DeclarationMacro 添加变量、函数、类型声明
@attached(member) 成员绑定宏 MemberMacro 向类型中注入属性、方法等成员
@attached(peer) 对等绑定宏 PeerMacro 在声明旁插入并列的新声明
@attached(accessor) 访问器绑定宏 AccessorMacro 自动生成 get/set 等属性访问器
@attached(extension) 扩展绑定宏 ExtensionMacro 生成扩展(extension)
@attached(memberAttribute) 成员属性绑定宏 MemberAttributeMacro 修改成员的注解、属性等
@attached(body) 函数体替换绑定宏 BodyMacro 替换计算属性或函数的实现体

独立宏以 #宏名(...) 使用,绑定宏以 @宏名(...) 使用。

这些角色为 Swift 宏构建起了清晰的职责体系 —— 每个宏角色都对应一类语法结构的生成或修改行为。

4.2 命名说明符:绑定宏中的命名控制器

对于会生成 具名实体(如属性、函数、类型) 的宏,Swift 提供了另一套机制来进一步控制“生成出来的东西叫什么”,这就是 命名说明符(Name Specifier)

在绑定宏(例如 MemberMacroAccessorMacro)中,我们通常使用 expanded 方法返回字符串形式的声明代码。但如果不明确命名,编译器将视这些内容为 匿名生成,从而带来几个问题:

  • 无法在语义层面识别生成成员的名称;
  • 代码补全、跳转、文档工具支持不佳;
  • 多个宏同时生成代码时容易发生命名冲突;
  • 其他宏无法可靠地引用这些生成内容。

为了解决这些问题,Swift 引入了 命名说明符 机制,用于精确指定宏生成的实体名称。例如:

 @attached(extension, names: named(==))

这表示:宏将生成一个具名为 == 的成员方法。

命名说明符的种类与用途

命名说明符 典型用途 原始声明 宏生成结果
named("...") 设定固定名称 struct MyView {} static func makePreview()
prefixed("...") 给生成成员加前缀 var name: String var debug_name: String
suffixed("...") 给生成成员加后缀 func save() func saveAsync()
overloaded 添加重载版本 func log() func log(level: LogLevel)
arbitrary 自定义命名(复杂场景) struct User {} _UserFlagsHelper, internalMap

5. 宏协议:决定宏行为的功能接口

Swift 宏的功能是建立在一套明确分层的协议体系上的。这些协议定义了宏的 基本行为适用场景,以及 如何响应编译器的宏展开请求

5.1 宏的基础协议:Macro

所有 Swift 宏都遵循 Macro 协议,它是宏体系的根基,定义了宏的基本能力和默认行为。

 public protocol Macro {
  /// 控制宏展开后的代码是否格式化,默认为 `.auto`
  static var formatMode: FormatMode { get }
 }
  • .auto(默认):使用格式化后的展开代码,推荐使用,能保持代码一致性。
  • .disabled:展开后的代码将原样插入,不进行格式化,适用于自定义输出。

5.2 宏的分类协议:FreestandingMacroAttachedMacro

Macro 协议的基础上,Swift 将宏分为两类:

  • FreestandingMacro:用于 独立使用的宏,可以直接插入到表达式、声明等任何地方,适合用来生成简单的表达式。
 public protocol FreestandingMacro: Macro { }
  • AttachedMacro:用于 附着在已有代码上的宏,必须绑定到已有的类型、属性、函数等声明上,适合对已有代码进行扩展。
 public protocol AttachedMacro: Macro { }

这两个协议本身不定义任何具体行为,它们为更细分的角色协议提供了基础。

💡 Swift 使用协议体系来设计宏的目的是:

  • 层次清晰:基础协议定义宏的公共行为,高层协议划分宏的使用场景,角色协议定义宏的具体能力。
  • 编译器驱动:根据宏的角色和位置,编译器调用特定协议中的 expansion(...) 方法展开宏。
  • 类型安全:协议方法的定义明确,展开时处理的语法结构与上下文类型都有严格的检查。

5.3 宏的角色协议

每个宏的角色都需要实现一个静态方法 expansion(of:in:),这是编译器在宏展开时调用的核心方法。该方法将接收当前语法节点和上下文信息,并返回生成的语法树,最终插入到用户代码中。

💡 一个宏的实现可以遵循多个协议,从而具备多重角色能力。 例如,以下 AutoCodableMacro 同时实现了 MemberMacroAccessorMacro,因此它具备生成成员和访问器的能力:

 public struct AutoCodableMacro: MemberMacro, AccessorMacro {
  public static func expansion(...) -> [DeclSyntax] { ... }
 
  public static func expansion(...) -> [AccessorDeclSyntax] { ... }
 }

这正是 Swift 宏系统的强大之处 —— 通过协议组合实现宏的 多重角色

5.4 主要宏角色协议

接下来,我们将一一解析不同的角色协议,详细说明每个协议的职责、调用时机及适用场景。

1. 表达式独立宏:ExpressionMacro

 public protocol ExpressionMacro: FreestandingMacro {
  static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> ExprSyntax
 }

功能:插入和替换表达式。适用于动态计算、生成常量、包装表达式等。

2. 声明式独立宏:DeclarationMacro

 public protocol DeclarationMacro: FreestandingMacro {
  static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax]
 }

功能:用于插入新的声明(例如,变量、函数、类型声明等)。

3. 对等绑定宏:PeerMacro

public protocol PeerMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingPeersOf declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax]
}

功能:在现有声明旁边生成平级结构,通常用于插入同级声明。

4. 访问器绑定宏:AccessorMacro

public protocol AccessorMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingAccessorsOf declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [AccessorDeclSyntax]
}

功能:为属性添加访问器(如 getsetdidSet 等)。

5. 成员属性修饰宏:MemberAttributeMacro

public protocol MemberAttributeMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclGroupSyntax,
    providingAttributesFor member: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) throws -> [AttributeSyntax]
}

功能:为成员添加统一的修饰符或属性标签。

6. 成员绑定宏:MemberMacro

public protocol MemberMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax]

  static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    conformingTo protocols: [TypeSyntax],
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax]
}

功能:为类型添加成员(如属性、方法、构造器等)。

7. 替换声明体绑定宏:BodyMacro

public protocol BodyMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
    in context: some MacroExpansionContext
  ) throws -> [CodeBlockItemSyntax]
}

功能:为现有声明提供具体实现或行为,常用于生成计算属性的实现或补充函数体。

8. 扩展绑定宏:ExtensionMacro

public protocol ExtensionMacro: AttachedMacro {
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclGroupSyntax,
    providingExtensionsOf type: some TypeSyntaxProtocol,
    conformingTo protocols: [TypeSyntax],
    in context: some MacroExpansionContext
  ) throws -> [ExtensionDeclSyntax]
}

功能:为类型生成扩展,通常用于协议一致性等。

9. 未公开使用的实验宏

CodeItemMacro:用于插入宽泛的代码片段。 PreambleMacro:为文件自动注入文件级代码。

6. 宏的结构设计:从角色到行为的思维路径

Swift 宏系统之所以强大,在于它并不追求“全能型”宏,而是通过“角色划分”将每种宏限制在特定场景中。这不仅让系统具备类型安全与上下文约束,还帮助开发者在设计宏时建立起清晰的思维路径。

本章我们将构建这样一个模型:每一种宏的“角色” → 应该遵循的“协议” → 实现的“行为结构”

并通过一些实际例子,帮助你理解 如何基于宏的使用意图,选择正确的协议与输出结构

6.1 宏角色简析:你要扩展什么?

一个宏所作用的语法位置被称为它的 角色(Role) 。角色决定了宏能应用在哪类语法结构上(如表达式、属性、类型、函数体等),也决定了宏展开时能生成哪类代码结构。

你想做什么? 角色名称 示例
在表达式中插入代码? 表达式级宏 #stringify(value)
为 struct 自动添加成员? 成员绑定宏 @AddID
生成 computed 属性的 getter/setter? 属性访问宏 @UserDefault
自动生成某个函数体? 函数体宏 @AddDescription
为类型生成协议扩展和默认实现? 扩展绑定宏 @CodableSubclass
额外添加旁路函数或类型? 对等绑定宏 @BindEvent

每个角色背后都对应着一个(或多个)专用协议,用来限制其行为。

6.2 协议是角色的具象化

Swift 宏协议是以 Macro 结尾的一组协议,定义了你在该角色下应该实现的接口。

角色 对应协议 你要返回的结构类型
表达式级宏 ExpressionMacro ExprSyntax
声明级宏 DeclarationMacro [DeclSyntax]
成员绑定宏 MemberMacro [DeclSyntax]
对等绑定宏 PeerMacro [DeclSyntax]
属性访问宏 AccessorMacro [AccessorDeclSyntax]
扩展绑定宏 ExtensionMacro [ExtensionDeclSyntax]
成员属性宏 MemberAttributeMacro [AttributeSyntax]
函数体宏 BodyMacro CodeBlockSyntax

这些协议都提供了一个 static func expansion(...) 方法,但根据角色不同,返回的语法结构也各不相同。

6.3 建立宏的设计思维路径

宏的本质是 “你想让它为你生成什么代码?” ,这套设计过程可以简化为三步:

你想扩展的目标(角色)
     ↓
确定宏协议
     ↓
实现 expansion,构造语法树(行为)

我们将这个过程称为**「角色 → 协议 → 行为」**的思维模型。

例子 1:我想为 struct 添加一个成员 ID

  • ⛳ 目标:为 struct 添加成员
  • 🎭 角色:成员绑定宏(struct 的成员)
  • 📜 协议:MemberMacro
  • 🔧 行为:返回 DeclSyntax 形式的变量声明
@AddID
struct User { }

→ 展开为:

struct User { 
   var id: String = UUID().uuidString
}

例子 2:我想为属性自动生成访问器(getter/setter)

  • ⛳ 目标:替属性添加访问器
  • 🎭 角色:属性访问宏
  • 📜 协议:AccessorMacro
  • 🔧 行为:返回 [AccessorDeclSyntax],如 getset
@UserDefault("age")
var age: Int

→ 展开为:

get { UserDefaults.standard.integer(forKey: "age") }
set { UserDefaults.standard.set(newValue, forKey: "age") }

例子 3:我想自动为某个函数生成实现体

  • ⛳ 目标:添加函数体
  • 🎭 角色:函数体宏
  • 📜 协议:BodyMacro
  • 🔧 行为:返回 CodeBlockSyntax
@AddDescription
func description() -> String

→ 展开为:

{
  return "name=(self.name), age=(self.age)"
}

Swift Macros - 宏之起点

Swift 宏(Macro)对许多开发者来说,是一种既熟悉又陌生的工具。在 Objective-C 时代,我们经常使用 #define、条件编译、日志封装,甚至自动插桩来提升开发效率。这些基于 C 的宏机制虽然灵活强大,却缺乏类型检查,容易引发错误,调试困难且可读性差。因此,Swift 在最初设计时,选择摒弃这种宏体系,专注于类型安全与语法清晰。

但这并不意味着 Swift 不需要“宏”能力。相反,随着语言的发展和应用场景的复杂化,开发者始终渴望一种既安全又可控的自动代码生成机制。正因如此,Swift 的全新宏系统应运而生。

自 Swift 5.9 起,Apple 正式引入了 宏系统(Macros) ,它允许我们通过编译期的语法扩展(Macro Expansion)自动生成 Swift 代码,具备更强的表达力、更高的类型安全性,以及良好的 IDE 支持。相较于传统 C 宏,Swift 宏具备以下显著优势:

  • 编译期类型检查,避免潜在错误
  • 上下文感知,生成逻辑更加智能
  • 保留原始源码的注释与格式,利于可读性与版本控制

宏会在编译阶段对源代码进行“展开”,自动生成结构化的 Swift 代码,帮助我们避免重复劳动。下图展示了宏展开的过程:

宏展开说明.png

官方描述中提到,Swift 宏遵循“加法原则”:宏只添加代码,不移除或篡改现有逻辑,以确保代码结构和意图的清晰可控。这意味着,宏通常在现有代码基础上进行扩展,而不是完全替换原有的实现。

然而,实际上,Swift 宏的行为并非总是如此。特定类型的宏,如 BodyMacro,确实可以完全替换现有代码。因此,可以理解为在某些场景下,完全替换代码是预期的行为,而非违反“加法原则”的异常情况。

💡 个人认为:最初的 Swift 宏定义严格遵循“加法原则”,但随着 BodyMacro 类型的引入,这一原则在某些情况下被打破,以提供更高的灵活性和功能扩展。

更重要的是,Swift 会对宏的输入与输出进行语法与类型检查,确保生成的代码在语义上也是正确的。如果宏实现存在问题,或使用方式不符合规则,编译器会抛出错误,使得问题可以在编译期被及时发现。这种设计大幅提升了宏使用的可靠性与开发信心。

Swift 中的宏主要分为两类:

  • 独立宏(Freestanding Macro) :以表达式、语句或声明的形式出现在代码中,不依附于任何已有声明
  • 附加宏(Attached Macro) :修饰某个已有的声明(如变量、函数、类型等),用于为该声明自动生成附加代码

虽然这两类宏的使用方式略有不同,但它们共享统一的扩展机制,且都通过实现相应的协议来定义行为。接下来的章节将详细介绍这两种宏的使用场景、实现方式与典型示例。

认识宏

Swift 标准库中内置了一些我们耳熟能详的宏,例如 #function, #warning

 func myFunction() {
    print("Currently running (#function)")
    #warning("Something's wrong")
 }

编译此代码时,Swift 会在编译阶段调用这些宏的实现。#function 会被替换为当前函数的名称,而 #warning 会生成编译器警告。

延伸阅读:若你希望进一步了解内置宏的实现机制与定义,可参考以下资源:

除了这些内置宏,Swift 还提供了自定义宏的能力。开发者可以通过实现自己的宏,在编译期生成结构化的 Swift 代码,从而减少样板逻辑,提高代码质量。

自定义宏

Swift 提供了自定义宏的能力,允许开发者通过 externalMacro 指定宏的实现位置,其语法如下:

 @freestanding(expression)
 macro externalMacro<T>(module: String, type: String) -> T
  • module 表示宏实现所在的模块名称
  • type 表示宏实现的类型名称(通常是一个 Macro 协议的实现类型)

需要注意的是,这个语法本身不会执行宏逻辑,而是用于声明该宏应由哪个模块与类型来实现

⚠️ externalMacro 只能出现在宏定义的位置,不能在其他上下文中直接调用使用。

为了更好地理解和掌握 Swift 宏,我们将以官方提供的宏模板为起点,逐步拆解其结构、分类与实现方式,深入探索它如何帮助我们构建更简洁、更智能、更高效的 Swift 代码。

制作宏

1. 创建工程模板

Swift 宏的实现和使用分为多个阶段。以官方模板示例( #stringify)为例,演示如何用 Xcode 创建一个可运行、可测试的宏工程。

要求:Xcode 15+,Swift 5.9+

通过 File → New → Package 创建一个新的 Swift 包,Xcode 会自动为你生成一个包含宏插件支持的标准结构。

创建制作工程.png

工程模版说明

 MyMacro/
 ├── Package.swift                 ← Swift 包描述文件 
 ├── Sources/
 │   ├── MyMacro/                 ← 宏声明(对外暴露)
 │   │   └── MyMacro.swift
 │   └── MyMacroMacros/           ← 宏实现(逻辑 + 注册)
 │       └── StringifyMacro.swift
 ├── Tests/
 │   └── MyMacroTests/             ← 单元测试
 │       └── MyMacroTests.swift

2. 定义宏的声明

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

3. 实现宏的逻辑

 import SwiftSyntax
 import SwiftSyntaxBuilder
 import SwiftCompilerPlugin
 import SwiftSyntaxMacros
 
 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))"
    }
 }

4. 注册宏的插件

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

Swift 宏是编译期运行的,需要通过插件(Plugin)注册; @main 表示插件入口,编译器在构建时会执行这个插件进程。

使用宏

 import MyMacro
 
 let a = 17
 let b = 25
 let (result, code) = #stringify(a + b)
 
 print("The value (result) was produced by the code "(code)"")
 //打印结果:The value 42 was produced by the code "a + b"

如果其他开发者接手你的代码,可能会疑惑:这个 #stringify 究竟做了什么?Xcode 提供了查看宏展开结果的功能:

右键点击宏 → 选择「Expand Macro」,即可看到宏生成的真实代码 宏的展开.png

调试宏

在开发宏的过程中,调试是一件比较特殊的事。你可能会遇到这些问题:

  • 设置了断点,但根本不会触发;
  • 使用了 print(...),却什么都没输出。

这并不是 Bug,而是 Swift 宏机制的特性。

宏是在一个 独立的插件进程(Plugin Process) 中执行的,这个进程由 Swift 编译器在构建期间调用。它与运行时环境无关,因此 它的输出不会出现在运行控制台中无法通过 LLDB 进行调试

官方推荐使用 单元测试 + assertMacroExpansion() 的方式进行宏调试。

 assertMacroExpansion(originalSource: String, expandedSource: String, macros: [String : any Macro.Type])

这种方式能够:

  • 显示宏调用的原始表达式;
  • 展示宏展开后的完整源码;
  • 自动校验是否与预期一致。
final class MyMacroTests: XCTestCase {
    func testMacro() throws {
        assertMacroExpansion(
            """
            #stringify(a + b)
            """,
            expandedSource: """
            (a + b, "a + b")
            """,
            macros: [ "stringify": StringifyMacro.self ]
        )
    }
}

上边的代码调用 assertMacroExpansion 方法传入的参数:

第一个参数是宏的调用方法 #stringify(a + b)

第二个参数是展开预期结果 (a + b, "a + b")

第三个参数是我们要测试的宏信息。

最后

在本篇中,我们了解了宏的起源、基本概念、使用方式与开发流程,也初步体验了 Swift 宏带来的便捷性。但这仅仅是一个开始。

你可能会思考一些问题:

  • 一个完整的 Swift 宏是如何构成的?

    宏的内部到底是怎样工作的?它是如何在编译时自动生成代码的,甚至可以做到像手写代码一样的安全性和类型检查?

  • “独立宏”和“附加宏”到底有什么区别?

    它们分别适用于什么场景?在什么时候应该选择一个独立存在的宏,而在什么情况下又该使用一个附加到已有代码上的宏?

  • Swift 宏系统能怎么帮助我实现代码生成?

    如果我有很多重复的代码任务(比如编写 CodableEquatable等),如何通过宏来减少重复工作,提升开发效率,而不需要每次手动编写一遍?

这些问题,都是理解 Swift 宏系统的关键。在你初次接触 Swift 宏时会感到迷茫,但正是这些思考,能够帮助你深入理解 Swift 宏系统的强大能力。

在下一篇《Swift Macros - 2. 宏之全貌》中,我们将从宏系统的设计理念出发,全面剖析 Swift 宏的结构组成与角色划分,建立起宏编程的认知地图。你将逐步掌握宏定义背后的底层原理,为真正驾驭 Swift 的编译期能力打下坚实基础。

🔐SwiftUI 权限请求优雅方案 —— 深入解析 PermissionsSwiftUI 使用与集成

swiftui-permission.png

在 iOS 应用开发中,权限请求是用户体验中的关键一环。我们通常需要请求访问相机、麦克风、定位、通知等系统资源,但系统原生的权限提示机制往往不够统一,缺乏灵活控制和个性化设计。

本文将深入介绍一个极具实用价值的开源项目 —— PermissionsSwiftUI,它可以帮助我们在 SwiftUI 中以 Apple 风格的方式统一管理权限请求,为用户提供一致的交互体验,并极大提升开发效率。

🌟 为什么选择 PermissionsSwiftUI?

SwiftUI 生态下的权限请求痛点

  1. 原生权限请求需要手动管理每种权限的状态判断与请求流程,代码分散、重复性高;
  2. 界面风格不统一,系统弹窗缺乏一致性、定制性;
  3. 缺乏优雅的组合请求机制,多个权限需要分次触发,交互割裂。

PermissionsSwiftUI 提供的解决方案

特性 说明
🧩 SwiftUI 原生支持 完全基于 SwiftUI 构建,无需 UIKit
🎨 苹果风格 UI 模态弹窗(Modal)或警告框(Alert),界面统一
🧠 自动授权判断 内置权限状态判断逻辑
⚙️ 高度自定义 图标、颜色、描述、关闭策略均可配置
🎯 支持权限丰富 支持十多种常见权限类型

🛠️ 安装与配置

推荐方式:使用 Swift Package Manager

  1. 打开 Xcode 项目,选择菜单栏:
File → Add Packages...

2. 输入 GitHub 仓库地址:

https://github.com/jevonmao/PermissionsSwiftUI

3. 选择版本(建议使用 Up to Next Major,确保自动兼容更新) 4. 在 Link Binary With Libraries 中选择实际使用的权限模块,避免多余依赖和 Apple 审核拒绝

重要配置:添加 Info.plist 权限声明

如需请求相机和定位权限,需添加以下键值到 Info.plist:

<key>NSCameraUsageDescription</key>
<string>我们需要访问您的相机用于拍照上传</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>我们需要访问您的位置信息以提供本地化服务</string>

快速上手:几行代码完成权限弹窗

import SwiftUI
import PermissionsSwiftUI

struct ContentView: View {
    @State private var showPermissions = false

    var body: some View {
        Button("打开权限弹窗") {
            showPermissions = true
        }
        .JMModal(showModal: $showPermissions, for: [.camera, .microphone])
    }
}

✨ 效果说明:

  • .JMModal() 修饰符创建一个类似苹果系统设置的权限弹窗;
  • for: [.camera, .microphone] 表示一次性请求多个权限;
  • 你也可以用 .JMAlert() 创建警告框样式弹窗。

🎛 自定义弹窗内容与行为

PermissionsSwiftUI 提供多个链式方法对弹窗进行细粒度控制:

修改 UI 内容:

.JMModal(showModal: $showPermissions, for: [.photo])
.changeHeaderTo("我们需要您的照片权限")
.changeHeaderDescriptionTo("请允许访问照片库以上传您的头像")

改变主色调和样式:

.changePrimaryColor(.indigo)
.changeAccentColor(.orange)

行为控制:

.autoDismiss(true) // 请求后自动关闭
.restrictDismissal(true) // 不允许用户关闭弹窗
.autoCheckAuthorization(true) // 自动判断授权状态

📚 支持的权限类型一览

权限类型 枚举值 描述
📷 相机 .camera 拍照、扫码等场景
🎤 麦克风 .microphone 音频录制、通话
📍 位置(使用时) .locationWhenInUse 地图、定位
📍 位置(始终) .locationAlways 后台定位
🖼️ 照片 .photo 上传图片、访问相册
📅 日历 .calendar 添加、读取日程
⏰ 提醒事项 .reminder 管理提醒事项
❤️ 健康 .health Apple Health 数据
🧭 蓝牙 .bluetooth 与蓝牙设备通信
🏃‍♂️ 运动传感器 .motion 健身、步数识别
📢 通知 .notification 推送消息
🎶 音乐库 .music Apple Music 访问
🗣️ 语音识别 .speech 实时语音识别

你只需引入需要的权限模块,无需一次性导入所有依赖。

🧱 高阶用法:封装权限管理组件

可进一步封装一个可复用的权限请求组件,提升项目模块化程度:

struct PermissionRequestView: View {
    @Binding var isPresented: Bool

    var body: some View {
        EmptyView()
        .JMModal(showModal: $isPresented, for: [.camera, .microphone, .locationWhenInUse])
        .changeHeaderTo("权限说明")
        .changeHeaderDescriptionTo("为了正常使用应用,我们需要以下权限...")
        .changePrimaryColor(.green)
    }
}

在页面中调用:

PermissionRequestView(isPresented: $showPermissions)

🔄 权限状态管理与回调处理

PermissionsSwiftUI 不直接暴露授权状态回调,但你可以通过 PermissionsSwiftUI.PermissionManager 判断当前权限状态:

let status = PermissionManager.shared.getPermissionStatus(for: .camera)

switch status {
    case .authorized:
    print("已授权")
    case .denied:
    print("被拒绝")
    case .notDetermined:
    print("未决定")
    default:
    break
}

你也可以监听状态变化后动态关闭权限弹窗或执行某些操作。

🚧 常见问题与注意事项

问题 说明
App 审核被拒 请确保只请求实际使用的权限,并在 Info.plist 中添加合理描述
无法自动弹窗 请确保 .JMModal 中绑定的 @State 被正确设置
通知权限不起作用 iOS 通知授权需单独处理,可考虑手动使用 UNUserNotificationCenter 搭配

📬 总结与推荐

PermissionsSwiftUI 是一个轻量、高扩展性、易集成的权限请求库,特别适合使用 SwiftUI 技术栈的项目:

✅ Apple 风格弹窗提升用户信任感

✅ 多权限统一管理简化开发流程

✅ 高度自定义满足不同 UI 风格需求

如果你正准备开发一个需要使用多个系统权限的 SwiftUI 项目,PermissionsSwiftUI 是一个非常值得尝试的工具库。

Swift中的析构函数deinit

析构函数

在 Swift 中,deinit(析构函数)只适用于 class 类型。因为只有类是引用类型,而不像结构体 struct 或枚举 enum 是值类型。

虽然 Swift 自动管理内存(ARC,Automatic Reference Counting),你不需要经常写 deinit,但在某些关键场景中非常有用。以下是最常见且实用的使用场景:

常用场景一:移除通知监听者

使用 NotificationCenter 添加观察者时,如果不手动移除,会导致内存泄漏或重复响应事件

class MyObserver {

    init() {
        NotificationCenter.default.addObserver(self, selector: #selector(handleEvent), name: .someEvent, object: nil)
    }

    @objc func handleEvent() {
        print("Event received")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        print("Observer deinitialized")
    }
}

常用场景二:取消定时器、任务、订阅等异步资源

比如 Timer、DispatchSourceTimer、Combine 的 AnyCancellable、URLSession task 等等。

class TimerHandler {
    var timer: Timer?

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            print("Tick")
        }
    }

    deinit {
        timer?.invalidate()
        print("Timer invalidated")
    }
}

常用场景三:断开委托(Delegate)或弱引用检查

有时候你需要在 deinit 中把自己从其他类的 delegate 中移除,以避免无用引用。

class Child: SomeDelegate {
    init(parent: Parent) {
        parent.delegate = self
    }

    deinit {
        print("Child released")
        // 这里通常不强制清空 delegate,但有时你想显式处理
    }
}

常用场景四:清理自定义资源或临时文件

当类负责写入临时文件或打开文件句柄等,需要在析构时清理。

class TempFileManager {
    let filePath: String

    init() {
        filePath = NSTemporaryDirectory() + UUID().uuidString
        FileManager.default.createFile(atPath: filePath, contents: nil)
    }

    deinit {
        try? FileManager.default.removeItem(atPath: filePath)
        print("Temporary file deleted")
    }
}

常用场景五:调试或资源释放追踪

打印 deinit 是常见的调试手段,能够帮助判断是否某对象已经被释放(尤其是在处理循环引用、Retain Cycle 的时候)。

deinit {
    print("MyViewController has been released!")
}

总结:deinit 适用的典型场景

场景 用途说明
通知监听移除 防止内存泄漏
定时器/任务取消 释放异步资源
文件或自定义资源清理 避免残留资源
delegate/数据源解绑(可选) 规范化资源管理
Combine/任务订阅取消 清理绑定或订阅
打印调试(检查是否释放) 非常实用的调试手段

在 SwiftUI 或 iOS 架构设计中,有些类如 ViewModel 或自定义对象,还是推荐善用 deinit 来做资源释放和问题定位



构造链

Swift 中 enum、struct 和 class 都可以拥有构造函数(initializer),但三者在构造行为和使用方式上有 明显差异,尤其是在以下几个地方:

• 默认构造器是否自动生成

• 成员变量初始化规则

• 继承与重写支持

• 可变性

• 引用与值语义

这里主要讨论下构造链。构造在 Swift 中,class 是引用类型,支持继承。而子类初始化时需要 保证父类的属性也被正确初始化,因此构造函数需要遵循一定的调用顺序,也就是所谓的“构造链”:

• 子类的 init 必须在某一阶段调用 super.init(...),以确保父类部分正确构造。

• 所有 stored property 初始化完后,才能调用 super.init()。

• Swift 编译器会严格检查构造顺序,避免未初始化状态下访问属性。

class Animal {
    var name: String
    init(name: String) {
        self.name = name
        print("Animal init")
    }
}

class Dog: Animal {
    var breed: String
    init(name: String, breed: String) {
        self.breed = breed              // 1️⃣ 先初始化子类属性
        super.init(name: name)         // 2️⃣ 调用父类构造器
        print("Dog init")
    }
}

// 调用
let dog = Dog(name: "Buddy", breed: "Labrador")

// 输出
# Animal init
# Dog init

对于 class类型:

1. 所有存储属性必须初始化。

2. 子类构造器必须在合适时机调用 super.init(...)。

3. 若父类没有默认构造器,子类必须手动调用父类指定构造器。

4. convenience init 只能调用本类的 designated init,不能直接调用 super.init。

5. 父类的 required init 必须被子类实现。

"struct 和 enum 没有继承, 它们只有构造器,不存在构造链,甚至构造器都可以用默认自动生成的

LazyViewContainer-iOS中节省内存的UI创建方式

背景

iOS工程中,创建自定义UI组件时有一个常见的写法:

  1. UI组件初始化时,创建所有子组件实例;并将子组件添加到自定义组件层级中,再使用Autolayout做好约束布局
  2. 后续有数据时,直接为组件的每个子组件填充数据

这样的写法是会导致一定的内存浪费,比如

  • 有的场景可能仅在特定情况下才会拿到数据显示UI组件,如果没有数据,若仍然初始化了某个UI组件,则导致该组件占用了一部分长期不会用到的内存
  • 这样的浪费在像直播房等需要大量UI、非UI组件的场景下比较常见

当然,根本的解决方案也是很容易想到的:

  • 延迟加载:仅在需要某个UI组件时才去创建和使用

没错,这无疑可以从根本上解决该问题,但是,延迟加载写起来其实要更麻烦一些,所以为了方便快捷,最开始提到的写法其实更普遍

下面会提到一种折中的方案,能部分降低内存的浪费

LazyViewContainer

  • 红框为普通UI组件直接创建和内存分配过程;绿框中是LazyViewContainer工作原理
  • 普通UI组件初始化完后,内存中就有了该组件完整的对象
  • LazyViewContainer只是一个有很少轻量属性(3个)的普通类,所以LazyViewContainer初始化后,此时内存中的占用较小(约48 bytes)。仅当执行ensureView方法时,才会尝试创建实际的UI组件
  • 最右侧紫色框展示了不同情况下内存占用情况
  • 简单总结一下LazyViewContainer的工作原理,就是在实际UI组件创建之前添加了一个中间层(使用泛型和closure来做到这一点),尽可能延迟实际UI组件及子组件的创建

内存占用测试

使用了几个最常见的系统UI组件进行了一下内存占用的测试

  • 测试环境:
    • iPhone 13 mini/iOS 18.1.1, Release环境
  • 测试方法:
    • 使用不同UI组件,使用不同创建方式,验证对应情况下,看单个UI组件实例实际的内存占用

不同UI组件的创建逻辑代码如下所示:

// UILabel
let label = UILabel()
label.text = "Test Label"
label.font = .systemFont(ofSize: 14)
label.textColor = .red
label.backgroundColor = .blue
label.frame = CGRect(x: 100, y: 100, width: 100, height: 20)
// UIButton
let button = UIButton(type: .system)
button.setTitle("Test Button", for: .normal)
button.backgroundColor = .green
button.frame = CGRect(x: 100, y: 200, width: 100, height: 40)
// UIImageView
let imageView = UIImageView()
imageView.backgroundColor = .yellow
imageView.frame = CGRect(x: 100, y: 300, width: 100, height: 100)
组件类型 创建方式 单个实例内存(bytes) 节省内存(bytes) 节省比例
UILabel 直接创建 896 - -
Lazy创建 48 768 94.64%
Lazy Ensure 944 -48 -5.36%
UIButton 直接创建 768 - -
Lazy创建 48 1792 93.75%
Lazy Ensure 816 -48 -6.25%
UIImageView 直接创建 512 - -
Lazy创建 48 384 90.62%
Lazy Ensure 560 -48 -9.38%

根据测试结果,总结一下结论:

  1. LazyViewContainer单个实例(在没有通过ensureView触发实际UI组件创建时)占用的内存约为 48 bytes
  2. 基于结论1,所以当LazyViewContainer真的要创建实际UI组件时,会比直接创建组件占用的内存更多,但也仅多48 bytes,至于精确的内存占比可以参考表格中“节省比例”列的绿字数据
  3. 同时也能预见到,当自定义UI组件越复杂时,“仅Lazy创建”的方式下,带来的内存节省比例更显著

LazyViewContainer使用场景

首先要明确两点:

  1. 这并不是创建UI组件的最节省内存的方式,只是一种在编码便捷性和内存性能之间的一种折中手段
  2. 不是所有情况下都推荐使用LazyViewContainer

那么,什么场景下推荐使用?

  • 用户可能长时间进行操作的、复杂UI层级的页面(如语音、视频、直播房间)
    • 这样的页面支持的功能比较多,但又不是需要同时运行这些功能,当多数用户、多数情况下仅使用一小部分功能时,那么如果仍然在进入页面时创建所有功能的业务或UI组件,就是会造成比较严重的内存浪费
  • 现有代码中业务逻辑太复杂,不宜短时间做大重构时,适合这样的折中方案
  • 复杂的自定义UI组件
    • 子组件越多,UI层级越复杂,当该UI组件使用率低的时候,所带来的内存节省效果越明显

什么场景下不推荐使用?

  • 列表页面
    • 列表滚动时,伴随多个cell的复用,虽然每个cell的数据不尽相同,有的UI元素可能不需要显示。但可能会被复用,所以此情况下,cell初始化时将所有子UI组件都创建,合理性更强,这样会更方便滚动时的复用

源码与使用方式

源码:

//
//  LazyViewContainer.swift
//
//  Created by songgeb on 2025/1/1.
//  Copyright © 2025 songgeb. All rights reserved.
//

import UIKit
/// 将创建自定义UIView逻辑延迟到数据到来时,同时避免当数据无效时仍创建自定义UIView,导致无意义的内存占用问题
final class LazyViewContainer<T: UIView> {
    private(set) var view: T?
    private let createView: () -> T
    private weak var parentView: UIView?

    init(createView: @escaping () -> T) {
        self.createView = createView
    }

    init() {
        createView = { T() }
    }

    /// 获取或创建视图,交给调用方处理布局
    /// - Parameters:
    ///   - parent: 父视图
    ///   - customAddition: 自定义添加视图逻辑
    ///   - onFirstAddition: 首次创建、同时添加到parentView的时机
    /// - Returns: 自定义视图
    @discardableResult
    func ensureView(
        in parent: UIView,
        customAddition: ((T) -> Void)? = nil,
        onFirstAddition: ((T) -> Void)? = nil) -> T
    {
        if let existingView = view {
            return existingView
        }

        let newView = Thread.isMainThread ? createView() : DispatchQueue.main.sync { createView() }

        // 使用自定义添加方式或默认 addSubview
        if let customAddition {
            customAddition(newView)
        } else {
            parent.addSubview(newView)
        }

        onFirstAddition?(newView)
        view = newView
        parentView = parent
        return newView
    }

    /// 移除视图
    /// - Parameter customRemoval: 自定义移除视图逻辑
    func removeView(using customRemoval: ((T) -> Void)? = nil) {
        guard let view = view, view.superview != nil else { return }
        func doRemoving() {
            if let customRemoval {
                customRemoval(view)
            } else {
                view.removeFromSuperview()
            }
        }
        if Thread.isMainThread {
            doRemoving()
        } else {
            DispatchQueue.main.async { doRemoving() }
        }
        self.view = nil
        parentView = nil
    }

    // 检查视图是否创建
    var isCreated: Bool {
        view != nil
    }

    deinit {
        removeView()
    }
}

使用方式:

class TestViewController: UIViewController {
    private lazy var leftVerticalStack: UIStackView = {
        let vstack = UIStackView(frame: .zero)
        vstack.axis = .vertical
        vstack.alignment = .leading
        vstack.distribution = .equalSpacing
        return vstack
    }()
    
    private lazy var detailLabelLoader = LazyViewContainer<UILabel> {
        let label = UILabel()
        label.font = .systemFont(withSize: 14)
        label.textColor = .bSecondary
        return label
    }

    private func updateDetailLabel(_ shouldShowDetail: Bool) {
        if shouldShowDetail {
            detailLabelLoader.ensureView(in: leftVerticalStack, customAddition: { [weak leftVerticalStack] label in
                guard let leftVerticalStack else { return }
                leftVerticalStack.addArrangedSubview(label)
            })
            detailLabelLoader.view?.text = "abc"

        } else {
            detailLabelLoader.removeView { [weak self] label in
                guard let self else { return }
                leftVerticalStack.removeArrangedSubview(label)
            }
        }
    }
}
❌