Swift Macro 在业务开发中的探索与实践

简介

Swift Macro 在 Swift 5.9 版本中正式引入,且需配合 Xcode 15 使用。Swift Macro 作为一种新的设计方法,致力于帮开发者降低编写重复代码的繁琐,以更为简洁优雅的方式去实现。

在 OC 中,有大家熟知的宏 #define,但是在 Swift 5.9之前我们无法像使用 OC 一样去定义宏,在 Swift 中没有这种宏的概念,只能通过静态方法去模拟宏从而达到目的。

OC 宏的实现原理是纯文本的替换,在编译之前通过预处理器帮我们对标记了 #define 的表达式进行展开替换,但是纯文本替换会产生一系列问题,比如 命名冲突,类型检查,调试问题等。

#define SQUARE(x)  x * x 

// ❌ 展开后逻辑不符 
int a = 5; 
int result = SQUARE(a + 1);      // 结果为 5 + 1 * 5 + 1 = 11      
  
// ❌ 缺少类型检查 
int result = SQUARE("BiliBili")  // 结果为 BiliBili * BiliBili

和 OC 的不同之处在于 Swift Macro 选择在编译时进行展开以及替换,一方面是可以在编译时进行类型安全检查,另一方面更大程度的提高了宏编译对开发者的反馈力度,宏的调试和错误都可以被开发者所感知。

相对于传统的预编译,编译时 Swift Macro 具有感知上下文的能力,对上下文的理解,从而可以产生更多样化的代码扩展和逻辑处理。

例如通过 Swift Macro 构造一个宏:为某个类自动生成 Int32 类型 的 age 成员并赋值

public macro DefaultAge(_ age: Int32) 

@DefaultAge(12.0) 
class Test {}

当我们将 Double 类型的参数 12.0 传入 宏里,在编译展开阶段就会报错,此时编译器会把错误抛出,并视为编译错误

class Test {      
  var age: Int32 = ❌ 
  // Cannot convert value of type 'Double' to specified type 'Int32'    
}

Swift Macro 是在 Swift 语言特性的基础上设计出来的产物,编译时对宏进行有效性的检查,可以让开发者在 Swift 宏使用过程中,更容易的发现错误,更方便的进行调试。

原理

Swift Macro 大部分是外部宏类型 #externalMacro,它不由当前程序执行,而是在沙盒的某个独立应用程序内,交由 Compiler Plug-in 去处理宏的展开和替换。

以 @DefaultAge 为例,宏的声明分为两个部分,角色声明和方法声明。方法声明则固定通过 macro 和 #externalMacro 关键词去修饰,而角色定义可以分为很多种,后续会展开介绍。

// 宏的角色定义
@attached(member, names: arbitrary)

// 宏的方法定义
public macro DefaultAge(_ age: Int32) = #externalMacro(
    module: "宏实现的模块", 
    type:   "宏实现的类型"
)

因为宏是通过 Package 去管理的,所以这里的 module 也就是包的模块名,而 type 则是当前宏实现的具体类型。

外部宏的展开进程是独立的,在一个安全的沙盒环境下进行,与外界的其它信息进行隔绝。在编译器执行 和 Swift 宏有关的代码时,编译器会调用宏的实现来展开宏。下面从 @DefaultAge 的定义到展开,大概阐述整个过程。

Test 类添加 @DefaultAge

@DefaultAge(12)
class Test {
   var name: String = "BiliBili"
}

1.编译器读取当前类,并拿到内存中转化的 AST 语法树,当前 AST 仅有一个成员 name

ClassDecl 
        -- MemberBlock
             -- MemberBlockItemList
                  -- MemberBlockItem   
                       -- name (String) == "BiliBili

2.将上述AST 传入宏的作用域,发送给编译器插件 Compiler Plug-in (只会传 AST,不包含其它代码)

3.编译器插件通过宏的声明,去宏的模块内找到该宏的实现,获取当前宏返回的AST,且在这个展开过程中,编译器会去检查 age 的有效性,比如类型是否正确等

public struct DefaultAgeMacro: MemberMacro {
    public static func expansion(of node: AttributeSyntax, 
                                 providingMembersOf declaration: some DeclGroupSyntax,
                                 conformingTo protocols: [TypeSyntax],
                                 in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        var res: DeclSyntax?
        let name = node.arguments?.firstToken(viewMode: .sourceAccurate)?.text
        if let name {
            res = """
            var age: Int32 = \(raw: name)
            """
        }
        return [res].compactMap { $0 }
    }
}
// 通过字符串字面量语法创建并返回的包含 age 的成员声明,得到 age 的 AST
// 最终这个AST对应的代码是:var age: Int32 = 12

4.  编译器拿到新增的 AST后,将其添加到原始 AST 中,最终生成新的语法树

ClassDecl 
        -- MemberBlock
             -- MemberBlockItemList
                  -- MemberBlockItem   
                       -- name (String) == "BiliBili"
                  -- MemberBlockItem
                       -- age  (Int)    == 12

5.  编译器插件将新的语法树序列化后插入到源码中,参与后续编译

过程图示:

图片

经历过这样一个简短的过程,可以得出几个结论:

1.  Swift Macro 的运行作用域是封闭的,隔绝外界无关信息,避免双向信息的交流和获取,禁止在宏内部做出一些对外界干扰的行为

2.  Swift Macro 对代码原环境的上下文感知是有限的,只感知和宏有关的 AST,我们无法对原始的 AST 做出修改和删除,从而印证了 Swift Macro 是一个增量的行为

3.  Swift Macro 会在宏的展开阶段对代码进行有效性检查,保证宏的正确性和可预测性

类别

在对宏的有了一个初步的认识后,了解一下宏的各个角色定义有什么不同。Swift 宏分为目前分为两类,独立宏和绑定宏。

独立宏

独立宏以 # 开头,创建一个表达式或者声明。独立宏类似平常开发中的纯函数,这里独立的意思是不需要感知外部环境的上下文,仅仅靠它自己就可以独立运行。

独立宏又细分为表达式宏和声明宏。

1.1 @freestanding(expression)

表达式宏:定义一个可以在表达式上下文中使用的宏,通常返回一个表达式或者值,类似系统中的 #function。使用方式一般是以字符串插值嵌入到某个表达式中,生成新的表达式,并作为表达式的一部分参与运算。

打印当前函数信息

@freestanding(expression)
public macro function()

func logInfo(function: String = #function, lineNum: UInt = #line) {}

1.2 @freestanding(declaration)

声明宏:定义一个可以在声明上下文中任何地方使用的宏,和表达式宏不同的是,它返回的是一个完整的声明,且永远不会产生值。类似系统中的 #warning,它还可以为我们声明整个类,枚举,属性等。

警告声明

@freestanding(declaration)
public macro warning(_ message: String) -> ()

#warning("插入一句警告")

生成一个方法

@freestanding(declaration)
macro LogFuction() = #externalMacro(module: "Macro", type: "LogFuction")

// 原代码
class Test {
    @LogFuction
}

// 展开后
class Test {
    func log() {
        print("log info")
    }
}

在日常开发中,我们经常会使用到色值转换,通过 ColorWithXXX 获得一个色值

func ColorWithString(_ hexString: String) -> Color 
func ColorWithHex(_ hex: UInt) -> Color

但是从设计稿复制黏贴并不能保证色值的正确性,这就可能会造成运行时的崩溃。那么此时独立宏就有了用武之地,我们可以在宏的实现内部加入色值的校验。

16进制色值正则校验

@freestanding(expression)
public macro Color(_ value: UInt) -> Color

public struct Color: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        // 正则判断
        // 1.正确 return color
        // 2.错误 throw error
    }
}

#Color(FFFFFF)  // true
#Color(123456)  // ❌ invalid color value

这样我们就可以在编译时抛出异常,避免因为少了一个字符而引发的cs。

绑定宏

以 @ 开头,和独立宏不同的点在于,它为我们提供了扩展 Swift 代码的能力,基于参数上不同角色的转换为我们创建或者扩展声明。比如可以对一个类,新增方法,新增属性,新增协议等

2.1 @attached(peer)

attached(peer) 是在原方法的作用层级上,对原有方法的增强,比如函数的重载。日常开发中,我们会在某些性能监控场景计算函数耗时,在方法前后记录当前时间戳计算差值。那么 attached(peer) 就可以提供overloaded 的能力,在原有方法的基础上,为我们自动生成一个新的方法。

函数的重载

@attached(peer, names: overloaded)
public macro NeedAPM() = #externalMacro(module: "Macro", type: "NeedAPM")

public static func expansion(of node: AttributeSyntax,
                             providingPeersOf declaration: some DeclSyntaxProtocol,
                             in context: some MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] {
    guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
        fatalError("非函数类型")
    }
    var functionSyntax: FunctionDeclSyntax?
    if let body = functionDecl.body {
        functionDecl = """
        func \(functionDecl.name)(_ needApm: Bool)  {
        if needApm {
          // 记录开始时间
        }
        \(body.statements)
        if needApm {
          // 记录结束时间 数据处理
        }
        }
       """
    }
    return [functionSyntax].compactMap { $0 }
}

// 原代码
class Test {
    @NeedAPM
    func test() {}
}

@NeedAPM 宏代码展开

class Test {
    @NeedAPM
    func test() {}
    func test(_ needApm: Bool)
}

如此一来,宏会帮我扩展出一个新的方法 func test(_ needApm: Bool),使用场景就由开发者决定。

2.2 @attached(accessor)

@attached(accessor)是对属性访问器的扩展,主要为某个属性扩展 setter,getter,didSet,willSet方法。可以把存储属性变成计算属性,通过 _Property 去接收;还可以通过这种方法去自己管理 Strcut / Class 的 Copy on Write,用来提升内存效率。

@attached(accessor, names: named(set), named(get))
public macro NameMacro() = #externalMacro(module: "Macro", type: "NameMacro")

public static func expansion(of node: AttributeSyntax,
                             providingAccessorsOf declaration: some DeclSyntaxProtocol,
                             in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
     return [
         """
         set {
            _\(identifier) = newValue
         }
         """,
         """
         get {
            return _\(identifier)
         }
         """
     ]
 }

// 原代码
class Test {
    private var _name: String = ""
    @NameMacro
    var name: String
}

@NameMacro 宏代码展开

class Test {
    private var _name: String = ""
    
    @NameMacro
    var name: String {
        set {
            _name = newValue
        }
        get {
            return _name
        }
    }
}

又或者通过该宏来达到类似 PropertyWrapper 的能力,对某个属性进行 UserDefault 的存取,那么只需在 accessor 的 set,get 中添加 UserDefault 的能力。

public macro UserDefault() = #externalMacro(module: "Macro", type: "UserDefault")

# 原代码
class UserDefaultManager {
    @UserDefault
    var name: String 
}

@UserDefault 宏代码展开后

class UserDefaultManager {
    @UserDefault
    var name: String {
        set {
            UserDefaults.standard.set(newValue, forKey: "name")
        }
        get {
            return UserDefaults.standard.object(forKey: "name") as? String ?? ""
        }
    }
}

2.3 @attached(memberAttribute)

对类 / 结构体 / 枚举等所有成员添加属性扩展

随着 UserDefaultManager 的成员日益增加,UserDefaultManager 就会变得臃肿起来

class UserDefaultManager {

    @UserDefault
    var name: String

    @UserDefault
    var title: String

    @UserDefault
    .....
}

而 @attached(memberAttribute) 就可以帮我们解决这个问题,由于宏的特性,在展开的时候是递归展开的。也就意味着,我们对 UserDefaultManager 实现 memberAttribute 宏,就可以让内部的成员实现 @UserDefault 宏。

@UserDefaultDefine
class UserDefaultManager {
    var name: String
    var title: String
}

@UserDefaultDefine 第一层展开后

@UserDefaultDefine
class UserDefaultManager {

    @UserDefault
    var name: String

    @UserDefault
    var title: String
}

@UserDefaultDefine 第二层展开后

@UserDefaultDefine
class UserDefaultManager {

    @UserDefault
    var name: String {
        set {
            UserDefaults.standard.set(newValue, forKey: "name")
        }
        get {
            return UserDefaults.standard.object(forKey: "name") as? String ?? ""
        }
    }

    @UserDefault
    var title: String {
        set {
            UserDefaults.standard.set(newValue, forKey: "title")
        }
        get {
            return UserDefaults.standard.object(forKey: "title") as? String ?? ""
        }
    }
}

2.4 @attached(member)

对类 / 结构体 / 枚举等 添加成员或者方法,如开头 @DefaultAge 所示。

2.5 @attached(extension, conformances)

@attached(extension, conformances:xxxProtocol)是以 extension 的形式去遵循某个协议。日常开发中判断2个实例是否相等 ,需要遵守 Equatable 协议,添加成员的判断,conformances 宏可以帮我们省去这些操作。

public macro Equatable() = #externalMacro(module: "Macro", type: "EquatableMacro")

public struct EquatableMacro: 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] {
        var conditions: [ExprSyntax] = []
        // 遍历成员,找到属性
        // VariableDeclSyntax 代表变量的 let / var 声明节点
        for member in declaration.memberBlock.members {
            if let varDecl = member.decl.as(VariableDeclSyntax.self) {
                for binding in varDecl.bindings {
                    if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {
                        let propName = identifier.identifier.text
                        conditions.append("lhs.\(raw: propName) == rhs.\(raw: propName)")
                    }
                }
            }
        }
        // 拼接成员,得到 "==" 的判断表达式
        var conditionString = ""
        for (index, condition) in conditions.enumerated() {
            if index > 0 {
                conditionString += " && "
            }
            conditionString += condition.description
        }
        return [try ExtensionDeclSyntax("extension \(type.trimmed): Equatable { static func ==(lhs: \(type.trimmed), rhs: \(type.trimmed)) -> Bool { return \(raw: conditionString)}}")]
    }
}

# 原代码
@EquatableMacro
class Test {
    var name: String = ""
}

@EquatableMacro 宏代码展开后

@EquatableMacro
class Test {
    var name: String = ""
}

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

上面简述了一些不同宏的基础用法,但是想要应用在项目内,还需要结合实际场景。不难看出,Swift Macro 的构造相对来说是比较麻烦的,我们需要按照 AST 的结构去拆解,编写代码,包括宏的单元测试。

不过我们需要透过繁琐的过程看到本质,宏的本质就是将繁琐的代码简化,在编译时帮助我们自动去生成或者补齐代码。虽然写宏的过程是比较痛苦的,但是在@出宏的那一刻就会被延迟满足。

Swift宏 应用

模块化场景下的应用

目前在大会员中心业务下维护的 番剧影视 和 大会员收银台 都采用了 MVVP的模块化架构,那么模块化的东西势必会产生一些模块化的模版。每次新增模块,就不得不把一些模版化的代码 CV 过来修改,虽然不复杂,但是不想写。

以模块声明为例,我们需要单个模块内绑定模块的视图,模块的逻辑,以及一些模块固定的成员。

protocol BiliModule: AnyObject {

    associatedtype ModuleView: BiliModuleView
    associatedtype ModulePresenter: BiliModulePresenter 

    static var moduleIdentifier: BiliModuleIdentifier { get }
    var view: ModuleView? { get set }
    var presenter: ModulePresenter { get }
    var moduleSize: CGSize { get }
    ...
}

那么新增一个 TAB 模块,就需要遵循 BiliModule,并实现以下代码:

class TabModule: BiliModule {

    typealias ModuleView = TabView
    typealias ModulePresenter = TabPresenter

    static var moduleIdentifier: BiliModuleIdentifier {
        .TAB
    }

    var view: TabView?
    var presenter: TabPresenter

    required init(context: BiliContext, data: BiliModuleInfo) {
        self.presenter = TabPresenter(context: context, data: data)
    }
}

同上,在逻辑层 Presenter 和 视图层 View 都需要进行模版化的绑定。CV 的工作量并不大,但是需要人眼去纠错,因为很容易漏改某个地方。所以尝试用 Swift Macro 来简化模版代码,让开发重心更倾向于业务。

于是用到了 @attached(extension, conformances):自动遵循 BiliModule 协议@attached(member):自动生成模版属性和方法

@attached(member, names: arbitrary)
@attached(extension, conformances: BiliModule)
public macro BiliModuleDefine<P, V>(_ presenter: P, _ view: V, _ type: BiliModuleIdentifier)

public struct BiliModuleMacro: MemberMacro {
    public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        guard let _ = declaration.as(ClassDeclSyntax.self) else {
            throw ClassMacroError.onlyApplicableToClass
        }
        // 获取参数列表 arguments
        guard case .argumentList(let arguments) = node.arguments else {
            throw ClassMacroError.noArguments
        }
        // 获取 arguments 参数类型
        let viewType = arguments.compactMap { $0.expression.as(DeclReferenceExprSyntax.self) }.last?.baseName.text ?? ""
        let moduleViewDecl = """
        typealias ModuleView = \(TypeSyntax(stringLiteral: viewType))
        var view: \(TypeSyntax(stringLiteral: viewType))?
        """
        ...
        ...
        // 生成 init 方法
        let initDecl = """
        required init(context: BiliContext, data: BiliModuleInfo) {
            self.presenter = \(TypeSyntax(stringLiteral: xxx))(context: context, data: data)
        }
        """
    }
}

// 遵循 BiliModule
extension BiliModuleMacro: 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 extensionDecl = try ExtensionDeclSyntax("extension \(type.trimmed): BiliModule {}")
        return [extensionDecl]
    }
}

最后,得到了下面这样一句话模块声明

@BiliModuleDefine(TabPresenter, TabView, .TAB)
class TabModule {}

当然了并非所有的模版定义都是一层不变的,这需要建立在一套固有的模版准则上,对样板内部的静态代码进行抽离。在大会员收银台的业务场景,可以轻轻敲下 @BiliModuleDefine,快速的构造一个模块声明,来聚焦业务逻辑的开发。为了不写模版,不得不为这个模版新写一个模版。

曝光场景下的应用

曝光在各个平台都有自己的实现方案,在大会员中心你想要曝光某个 ListView,你需要实现曝光 4 要素。

1.  注册曝光容器(Container),该容器视图需要 遵循 BiliExposureContainer 容器协议,以 ColletionView 为例。

// 容器视图
class ExposureCollectionView: UICollectionView, BiliExposureContainer {
    // 容器标记位
    var exposureCheckFlag: Int32 = 0
    var exposureContext: BiliExposureContext = BiliExposureContext()
    
    public func visibleExposureTargets() -> [UIView]? {
        return self.visibleCells
    }
}

2.  曝光对象(Target), 负责控制曝光的属性(曝光区域,是否重复曝光等),需要遵守 BiliExposureTarget 协议

class ExposeData: NSObject, BiliExposureTarget {
    // 曝光标记位
    var exposureCheckFlag: Int32 = 0
    // 曝光比例 (0 - 100%)
    var exposurePercent: Int32 = 100
    // 是否重复曝光
    var repeatExposure: Bool = false

    var name: String = ""
}

3.  曝光视图(View),真正曝光的区域遵循 BiliExposureRegion 协议

4.  曝光回调(Report ), 表该视图成功曝光,实现 BiliExposureReporter 协议

class ExsoureCell: UICollectionViewCell, BiliExposureRegion, BiliExposureReporter {

    // 3.1 曝光对象
    private var data: ExposeData?

    // 3.2 返回曝光对象
    func exposureTarget() -> BiliExposureTarget? {
        return data
    }

    // 4.1 曝光成功
    func reportExposure(with context: BiliExposureContext?) -> Bool {
        // 此处埋点上报
        return true
    }
}

这是没有嵌套滚动视图下的情况,如果 Cell 内部又嵌套了其它的滚动视图,那么整个曝光流程,在不熟悉的情况下,会让你手忙脚乱。所以想通过宏让刚接触的人可以快速上手,不用过多关心曝光组件,减少认知成本,同时也避免协议满天飞。

整个流程可以精简为3个步骤:

  • 第一步:标记曝光容器

@ExposureContainer:标记曝光容器

@ExposureView: 标记需要被曝光的视图 (非必须,可选)

情况A:该曝光视图是 ListView

@ExposureContainer
class ExposureCollectionView: UICollectionView {}

@ExposureContainer 宏代码展开后

@ExposureContainer
class ExposureCollectionView: UICollectionView, BiliExposureContainer {
    func visibleExposureTargets() -> [UIView]? {
        return self.visibleCells
    }
}

情况B:该曝光视图是常规 View(视图内多个元素需要曝光),需配合 @ExposureView 使用。其中 collectionView 和 headerView 都需要曝光检测。

@ExposureContainer
class ExposureView: UIView {

    @ExposureView
    var collectionView: UICollectionView

    @ExposureView
    var headerView: UIView
}

@ExposureContainer 宏代码展开后

@ExposureContainer
class ExposureView: UIView, BiliExposureContainer {

    @ExposureView
    var collectionView: UICollectionView

    @ExposureView
    var headerView: UIView
      
    // 生成 Targets方法,并添加标记了 @ExposureView 的View
    func visibleExposureTargets() -> [UIView]? {
        var views: [UIView] = []
        views.append(headerView)
        views.append(contentsOf: collectionView.visibleCells)
        return views
    }
}

  • 第二步:设置曝光参数

@ExposureTarget(_ percent: Int32, _ repeat: Bool ),通过 @attached(member) 生成成员。

@ExposureTarget(100, true)
class ExposeData: NSObject {}

@ExposureTarget(100, true) 宏代码展开后

@ExposureTarget(100, true)
class ExposeData: NSObject {
    var exposureCheckFlag: Int32 = 0
    var exposurePercent: Int32 = 100
    var repeatExposure: Bool = true
}

  • 第三步:标记曝光视图

@ExposureRegion:需要曝光的具体视图,接收曝光回调,@ExposureTargetMember:标记需要被返回的曝光的对象实例。

真正需要被曝光的视图在当前场景下是 UICollectionViewCell,而其内部的exposureData 用来提供曝光配置给 Cell。

@ExposureRegion 和 @ExposureTargetMember 需绑定使用

@ExposureRegion
class VipCollectionViewCell: UICollectionViewCell {

    @ExposureTargetMember
    var exposureData: ExposureData?
}

@ExposureRegion 宏代码展开后

@ExposureRegion
class VipCollectionViewCell: UICollectionViewCell, BiliExposureRegion {

    @ExposureTargetMember
    var exposureData: ExposureData?

    func exposureTarget() -> BiliExposureTarget? {
        return exposureData
    }
}

其中 @ExposureTargetMember 和 @ExposureView 是通过 @Peer 宏来进行标记,从而确定哪些成员是需要被找到的,Peer 宏内并无任何实现,以 @ExposureView 举例:

@attached(peer)
public macro ExposureView() = #externalMacro(module: "ExposurePlugin", type: "ExposureViewMacro")

public struct ExposureViewMacro: PeerMacro {
    public static func expansion(
        of node: SwiftSyntax.AttributeSyntax,
        providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
        in context: some SwiftSyntaxMacros.MacroExpansionContext
    ) throws -> [SwiftSyntax.DeclSyntax] {
        return []
    }
}

// 在 ExposureContainer 实现中配合使用
public struct ExposureContainer: MemberMacro {

    public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        // 遍历成员寻找是否存在 ExposureView 标识符 
        var propertyString: String = ""
        declaration.memberBlock.members.forEach { item in
            let variableDecl = item.decl.as(VariableDeclSyntax.self)?.attributes.first
            let identifier = variableDecl?.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text
            if let identifier, identifier == "ExposureView" {
                let binding = item.decl.as(VariableDeclSyntax.self)?.bindings.first
                let typeName = binding?.typeAnnotation?.as(TypeAnnotationSyntax.self)?.type
                if let propertyName = binding?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text  {
                    // 记录当前成员名称
                    propertyString += "\(propertyName),"
                }
            }
        } 
    }
}

那么对上面的代码进行宏简化,在 List VC 就得到了下代码

class ExposureVC {
    private lazy var collectionView: ExposureCollectionView = {
        return ExposureCollectionView()
    }()

    func viewDidload() {
        super.viewDidload()
        BiliExposer.registerContainer(collectionView, containerDelegate: self, context: exposureContext)
    }
}

// 标记曝光容器
@ExposureContainer
class ExposureCollectionView: UICollectionView {}

// 标记曝光对象
@ExposureTarget(100)
class ExposeData: NSObject {
    var name: String = ""
}

// 标记曝光视图
@ExposureRegion
class ExsoureCell:  UICollectionViewCell, BiliExposureReporter {
    // 标记曝光对象
    @ExposureTargetMember
    private var data: ExposeData?
    
    func reportExposure(with context: BiliExposureContext?) -> Bool {
        // 此处埋点上报
        return true
    }
}
# reportExposure 是使用者的动态上报行为,没有对其进行宏处理

整个流程翻译成图:


在曝光的接入流程上,使用者只需要明确自己需要标记宏的位置即可,无需感知什么角色需要遵守什么协议,将曝光流程职责化在具体的某个点上,让曝光的接入变得更为轻量化。

小结

以上两个场景应用,模块宏是为了解决模块化通用模版定义的繁琐, 曝光宏是为了降低整个曝光体系接入的复杂度,本质上都是让 Swift Macro 帮助我们减少重复样板代码的编写,提供快速接入的能力。接入 Swift Macro 给我的感受是它可以玩出很多花样,不仅增加了代码的趣味性,也增加的代码的可读性,降低了维护成本。目前对宏的理解和应用或存在瑕疵,后续会对宏继续探索,更大化的增强宏的实用性。有同学对代码实现有更好的建议也可以提出,一起交流进步。

-End-

作者丨 tit

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/903060.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Pseudo Multi-Camera Editing 数据集:通过常规视频生成的伪标记多摄像机推荐数据集,显著提升模型在未知领域的准确性。

2024-10-19&#xff0c;由伊利诺伊大学厄巴纳-香槟分校和香港城市大学的研究团队提出了一种创新方法&#xff0c;通过将常规视频转换成伪标记的多摄像机视角推荐数据集&#xff0c;有效解决了在未知领域中模型泛化能力差的问题。数据集的创建&#xff0c;为电影、电视和其他媒体…

练习LabVIEW第二十三题

学习目标&#xff1a; 刚学了LabVIEW&#xff0c;在网上找了些题&#xff0c;练习一下LabVIEW&#xff0c;有不对不好不足的地方欢迎指正&#xff01; 第二十三题&#xff1a; 建立一个枚举控件&#xff0c;其内容为张三、李四、王五共三位先生&#xff0c;要求当枚举控件显…

Spring Boot 实现文件分片上传和下载

文章目录 一、原理分析1.1 文件分片1.2 断点续传和断点下载1.2 文件分片下载的 HTTP 参数 二、文件上传功能实现2.1 客户端(前端)2.2 服务端 三、文件下载功能实现3.1 客户端(前端)3.2 服务端 四、功能测试4.1 文件上传功能测试4.2 文件下载功能实现 参考资料 完整案例代码&…

分类预测|基于WOA鲸鱼优化K近邻KNN的数据分类预测Matlab程序 多特征输入多类别输出GWO-KNN

文章目录 一、基本原理原理流程总结 二、实验结果三、核心代码四、代码获取五、总结 一、基本原理 鲸鱼优化算法&#xff08;WOA&#xff0c;Whale Optimization Algorithm&#xff09;是一种模拟座头鲸捕猎行为的启发式优化算法&#xff0c;适用于解决各种优化问题。在K近邻&…

深度探索:超实用阿里云应用之低功耗模组AT开发示例

今天我们讲解一款低功耗4G全网通模组作为例子&#xff0c; 基于Air780EP模组AT开发的阿里云应用教程&#xff0c; 本文同样适用于以下型号&#xff1a; Air700ECQ/Air700EAQ/Air700EMQ Air780EQ/Air780EPA/Air780EPT/Air780EPS Air780E/Air780EX/Air724UG… 1、相关准备工作 …

大白话讲解分布式事务-SEATA事务四种模式(内含demo)

因为这里主要是讲解分布式事务&#xff0c;关于什么是事务&#xff0c;以及事务的特性&#xff0c;单个事务的使用方式&#xff0c;以及在Spring框架下&#xff0c;事务的传播方式&#xff0c;这里就不再赘述了。但是我这里要补充一点就是&#xff0c;一提到事务大家脑子里第一…

假如浙江与福建合并为“浙福省”

在中国&#xff0c;很多省份之间的关系颇有“渊源”&#xff0c;例如河南与河北、湖南与湖北、广东与广西等等&#xff0c;他们因一山或一湖之隔&#xff0c;地域相近、文化相通。 但有这么两个省份&#xff0c;省名没有共通之处&#xff0c;文化上也有诸多不同&#xff0c;但…

[简易版] 自动化脚本

前言 uniapp cli项目中没办法自动化打开微信开发者工具&#xff0c;需要手动打开比较繁琐&#xff0c;故此自动化脚本就诞生啦~ 实现 const spawn require("cross-spawn"); const chalk require("picocolors"); const dayjs require("dayjs&quo…

7.使用Redis进行秒杀优化

目录 1. 优化思路 总结之前实现的秒杀过程 下单流程 2. 使用Redis完成秒杀资格判断和库存 0. Redis中数据类型的选用 1.将优惠券信息保存到Redis中 2.基于Lua脚本&#xff0c;判断秒杀库存、一人一单&#xff0c;决定用户是否抢购成功 3. 开启新协程&#xff0c;处理数…

MongoDB-Plus

MongoDB-Plus是一款功能强大的数据库工具&#xff0c;它基于MongoDB&#xff0c;提供了更丰富的功能和更便捷的操作方式。以下是一篇关于MongoDB-Plus轻松上手的详细指南&#xff0c;旨在帮助初学者快速掌握其安装、配置和基础操作。 一、MongoDB-Plus概述 MongoDB是一款由C编…

鸿蒙next之导航组件跳转携带参数

官方文档推荐使用导航组件的形式进行页面管理&#xff0c;官方文档看了半天也没搞明白&#xff0c;查了各种文档才弄清楚。以下是具体实现方法&#xff1a; 在src/main/resources/base/profile下新建router_map.json文件 里边存放的是导航组件 {"routerMap" : [{&q…

鸿蒙API12 端云一体化开发——云函数篇

大家好&#xff0c;我是学徒小z&#xff0c;我们接着上次的端云一体化继续讲解&#xff0c;今天来说说云函数怎么创建和调用 文章目录 云函数1. 入口方法2. 编写云函数3. 进行云端测试4. 在本地端侧调用云函数5. 云函数传参6. 环境变量 云函数 1. 入口方法 在CloudProgram中…

软硬件开发面试问题大汇总篇——针对非常规八股问题的提问与应答(代码规范与生态管理)

软硬件开发&#xff0c;对于编码规范、生态管理等等综合问题的考察尤为重要。 阐述下环形缓冲区的用途 环形缓冲区&#xff08;Ring Buffer&#xff09;是一种固定大小的数据结构&#xff0c;常用于实现数据的流式传输或临时存储。在环形缓冲区中&#xff0c;当到达缓冲区的末尾…

Java Lock CyclicBarrier 总结

前言 相关系列 《Java & Lock & 目录》&#xff08;持续更新&#xff09;《Java & Lock & CyclicBarrier & 源码》&#xff08;学习过程/多有漏误/仅作参考/不再更新&#xff09;《Java & Lock & CyclicBarrier & 总结》&#xff08;学习总结…

什么是排列树?

一、排列树的定义 排列树就是一个能表示全排列的树形结构。全排列咱们都学过&#xff0c;就是所有可能的排列。 当问题的解是n个元素的某个排列时&#xff0c;其解空间&#xff08;全部可能解构成的集合&#xff09;就是n个元素的全排列&#xff0c;称为排列树。 以3个元素{…

1 环境配置、创建功能包、编译、Cmake文件及package文件学习笔记

1 基本结构 放张 赵虚左老师的pdf截图 2 环境配置 //每次都需配置 . install/setup.bash//或者一次配置echo "source /path/to/your/workspace_name/install/setup.bash" >> ~/.bashrcsource ~/.bashrc3 创建功能包 ros2 pkg create 包名--build-type 构建类…

ClickHouse 5节点集群安装

ClickHouse 5节点集群安装 在此架构中&#xff0c;配置了五台服务器。其中两个用于托管数据副本。其他三台服务器用于协调数据的复制。在此示例中&#xff0c;我们将创建一个数据库和表&#xff0c;将使用 ReplicatedMergeTree 表引擎在两个数据节点之间复制该数据库和表。 官…

简单易用的Android主线程耗时检测类 MainThreadMonitor

适用场景 debug 本地测试 文章目录 代码类 MainThreadMonitor.java使用方式 Application的attachBaseContextlog输出示例 代码类 MainThreadMonitor.java public class MainThreadMonitor {private static final String TAG "MainThreadMonitor";private static Sc…

uniapp的IOS证书申请(测试和正式环境)及UDID配置流程

1.说明 本教程只提供uniapp在ios端的证书文件申请&#xff08;包含正式环境和开发环境&#xff09;、UDID配置说明&#xff0c;请勿用文档中的账号和其他隐私数据进行测试&#xff0c;请勿侵权&#xff01; 2.申请前准备 证书生成网站&#xff1a;苹果应用上传、解析&#x…

iOS Block 详解(Object-C)

Block 是苹果公司较晚推出的一个语法,与很多语法的闭包差不多意思 一:Block声明 PS:很多人学不好Block,大概率是被它的声明写法给吓到了,写法确实有点奇怪 返回值类型(^block变量名)(参数列表) 例如: int(^personBlock)(NSString *,int) 返回值类型(^block变量名)(参数列表…