阅读视图

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

Swift学习总结——认识Swift以及Swift编译器

1.认识Swift

Swift编程语言本身是开源的‌。苹果公司于2015年12月4日宣布将Swift编程语言开放源代码‌ ,用于开发 iOSOS XwatchOS 应用程序。Swift 结合了 CObjective-C 的优点并且不受 C 兼容性的限制。

当前Swift已经发布到6.x版本。但是在发展的前期,从1.x5.x,经历了多次重大改变,版本之间不兼容,学习和开发体验不友好。不过随着ABI的稳定,Swift的版本也趋于稳定。

知识点: API 和 ABI 区别

  1. API(Application Programming Interface):应用程序编程接口,源代码和库之间的接口。这也是我们在日常应用开发中经常用到的,比如UIKitFoundation等库中提供的工具接口。
  2. ABI(Application Binary InterSace):应用程序二进制接口,应用程序与操作系统之间的底层接口。涉及的内容包括:目标文件格式、数据类型大小\布局\对齐、函数调用约定等等。

2.Swift编译器

  1. 什么是编译器?在iOS底层学习——LLVM编译流程中,对编译器有过详细介绍,编译器就是将一种语言(通常为高级语言)翻译为另一种语言(通常为低级[语言]的程序。

    传统编译器,是将前端和后端捆绑在一起的,但是随着高级语言越来越多,终端类型种类的增加,所使用的的CPU架构等也不尽相同,所以为了适配多种环境,不得不设计不同的编译器。紧密耦合,阻碍了编译逻辑的可重塑性。

  2. LLVM 编译器前端(Frontend)和后端(Backend)进行了分离,前后端完全解耦。将前端和后端针对不同的架构,按照独立的项目进行研发。当编译器决定支持多种语言或多种硬件架构时,LLVM最重要的地方就体现出来了,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端。

    image.png

  3. Swift的前端编译器是Swift Complier,前端的编译过程不是一蹴而就的,其完整的工作流程在官网中有说明。Swift Compiler

    image.png

  4. 查看本机Swift编译器的版本,在终端中输入swiftc -v

    image.png

    我这台机器的版本是Swift5.10。其所在位置就在Xcode中,参考下图: image.png

    swift-frontend‌Swift编译器的前端部分,主要负责词法分析、语法分析、语义分析和中间代码生成等任务。它是Swift编译器的重要组成部分,用于处理源代码并生成中间表示(IR)或其他形式的输出‌。

  5. swiftc指令学习

    • 生成语法树:swiftc -dump-ast main.swift
    • 生成最简洁的SIL代码:swiftc-emit-sil main.swift
    • 生成LLVM IR代码:swiftc -emit-ir main.swift -o main.ll
    • 生成汇编代码:swiftc -emit-assembly main.swift -o main.s

    以上是一些指令,以生成汇编代码为例: image.png

    分析汇编语言可以真正掌握汇编语言的本质。

3 main函数

Xcode中新建一个命令行项目,会自动生成一个main.swift文件,可知swift的扩展名是.swift

同时代码中没有main函数,但是依然可以正常执行。在官方解释中提到:Code written at global scope is used as the entry point for the program, so you don't need a main function.意思是说在main.swift中的代码是在全局作用域下,这些代码直接作为整个项目的入口,所以这里并不需要main函数。

后期再深入探索swift的启动逻辑,本阶段只对swift基础内容进行总结!

image.png

键盘管理IQKeyboardManagerSwift全解

简要:

  • IQKeyboardManagerSwift:自动管理键盘,防止覆盖输入框,无需代码,适合快速集成。
  • IQKeyboardCore:提供辅助功能,非独立使用,包含视图扩展和协议。
  • IQKeyboardNotification:轻量级键盘事件监听,易于订阅键盘变化。
  • IQKeyboardReturnManager:管理回车键,方便导航到下一个输入框。
  • IQKeyboardToolbar:自定义键盘工具栏,支持上一页、下一页和完成按钮。
  • IQKeyboardToolbarManager:全局管理工具栏,统一应用设置。
  • IQTextInputViewNotification:监听输入框事件,如聚焦和失焦。
  • IQTextView:支持占位符的文本视图,增强用户体验。

1. IQKeyboardManagerSwift

  • 概述
    IQKeyboardManagerSwift 是一个无需代码的通用库,防止键盘覆盖 UITextFieldUITextView,自动管理键盘行为。

  • 主要功能

    • 自动调整键盘与输入框的距离,无需 UIScrollView 或子类化。
    • 支持所有方向,包含工具栏(Previous、Next、Done 按钮)。
    • 可定制化:调整键盘与输入框的距离、工具栏按钮行为、导航音效等。
    • 不需要手动导入,集成简单。
  • 主要属性和方法

    • shared: 返回单例实例。
    • isEnabled: 启用或禁用管理器(Bool)。
    • keyboardDistanceFromTextField: 设置键盘与输入框的距离(默认 10,单位像素)。
    • preventShowingBottomBlankSpace: 防止键盘滑动时显示底部空白区域(默认 YES)。
    • enableAutoToolbar: 启用自动工具栏(默认 YES)。
    • toolbarManageBehaviour: 设置工具栏管理行为(IQAutoToolbarBySubviews 或 IQAutoToolbarByTag)。
    • shouldResignOnTouchOutside: 触摸外部时隐藏键盘(Bool)。
    • resignFirstResponder(): 使当前第一响应者失焦。
    • canGoPrevious, canGoNext: 检查是否能导航到前一或后一输入框(只读)。
    • goPrevious(), goNext(): 导航到前一或后一输入框(返回 Bool)。
  • 安装

    • CocoaPods: pod 'IQKeyboardManagerSwift' 或特定版本如 pod 'IQKeyboardManagerSwift', '8.0.0'
    • Carthage: 添加 github "hackiftekhar/IQKeyboardManager" 到 Cartfile,运行 carthage update --use-xcframeworks,包含 IQKeyboardManagerSwift.xcframework
    • SPM: 通过 Xcode 安装。
  • 使用示例

    import IQKeyboardManagerSwift
    
    @main
    class AppDelegate: UIResponder, UIApplicationDelegate {
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            IQKeyboardManager.shared.isEnabled = true
            IQKeyboardManager.shared.keyboardDistanceFromTextField = 20
            return true
        }
    }
    
    • 注解:启用管理器并设置键盘与输入框的距离为 20 像素。
  • 注意事项

    • 不建议作为第三方库依赖,可能与其他库冲突,需在呈现/隐藏第三方 UI 时启用/禁用。
    • 版本 7.2.0 引入子模块(如 Appearance、IQKeyboardReturnManager 等),8.0.0 移除已废弃类。

2. IQKeyboardCore

  • 概述
    IQKeyboardCore 提供 IQKeyboard 相关库的公共函数和扩展,不独立使用,包含 IQTextInputView 协议和 UIView 扩展。

  • 主要功能

    • IQTextInputView 协议:由 UITextFieldUITextViewUISearchBar 采纳。
    • IQEnableMode 枚举:支持默认、启用、禁用模式。
    • UIView 扩展:提供 viewContainingController()superviewOf(type:belowView:) 等方法。
  • 主要方法

    • viewContainingController(): 获取包含当前视图的控制器。
    • superviewOf(type:belowView:): 查找指定类型的父视图。
    • textFieldSearchBar(): 检查是否为文本字段或搜索栏。
    • isAlertViewTextField(): 检查是否为警告视图的文本字段。
  • 使用示例

    import IQKeyboardCore
    
    let controller = view.iq.viewContainingController()
    let scrollView = view.iq.superviewOf(type: UIScrollView.self)
    
    • 注解:获取当前视图的控制器和父滚动视图。

3. IQKeyboardNotification

  • 概述
    IQKeyboardNotification 是一个轻量级库,方便订阅键盘事件,获取键盘框架变化。

  • 主要功能

    • 通过唯一标识符订阅键盘事件。
    • 提供键盘事件和框架信息。
  • 主要方法

    • init(): 初始化通知实例。
    • subscribe(identifier:action:): 订阅键盘事件,闭包接收事件和框架。
    • unsubscribe(identifier:): 取消订阅。
  • 使用示例

    import IQKeyboardNotification
    
    private let keyboard: IQKeyboardNotification = .init()
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        keyboard.subscribe(identifier: "myID") { event, frame in
            print("Keyboard event: \(event), frame: \(frame)")
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        keyboard.unsubscribe(identifier: "myID")
    }
    
    • 注解:在视图出现时订阅键盘事件,消失时取消订阅。

4. IQKeyboardReturnManager

  • 概述
    IQKeyboardReturnManager 管理回车键行为,支持导航到下一个响应者或隐藏键盘。

  • 主要功能

    • 自动处理回车键导航。
    • 自定义回车键类型(如 Done)。
  • 主要方法

    • init(): 初始化管理器。
    • addResponderSubviews(of:recursive:): 添加响应者子视图,递归处理。
    • dismissTextViewOnReturn: 设置是否在回车时隐藏文本视图(Bool)。
    • lastTextInputViewReturnKeyType: 设置最后一个输入视图的回车键类型。
  • 使用示例

    import IQKeyboardReturnManager
    
    let returnManager: IQKeyboardReturnManager = .init()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        returnManager.addResponderSubviews(of: self.view, recursive: true)
        returnManager.dismissTextViewOnReturn = true
        returnManager.lastTextInputViewReturnKeyType = .done
    }
    
    • 注解:启用回车键导航,设置隐藏文本视图和最后回车键为 Done。

5. IQKeyboardToolbar

  • 概述
    IQKeyboardToolbar 提供自定义键盘工具栏功能,支持添加 Previous、Next、Done 按钮。

  • 主要功能

    • 添加工具栏按钮,如 Previous、Next、Done。
    • 自定义按钮标题或图片,添加额外按钮。
  • 主要方法

    • addPreviousNextDone(target:previousAction:nextAction:doneAction:showPlaceholder:): 添加 Previous、Next、Done 按钮。
    • addPreviousNextRight(target:previousConfiguration:nextConfiguration:rightConfiguration:title:): 添加自定义按钮。
    • addDone(target:action:title:): 添加 Done 按钮。
    • hidePlaceholder: 隐藏占位符(Bool)。
    • placeholder: 设置自定义占位符。
  • 使用示例

    import IQKeyboardToolbar
    
    textField.iq.addPreviousNextDone(target: self, previousAction: #selector(previousAction), nextAction: #selector(nextAction), doneAction: #selector(doneAction), showPlaceholder: true)
    
    • 注解:为文本字段添加 Previous、Next 和 Done 按钮,显示占位符。

6. IQKeyboardToolbarManager

  • 概述
    IQKeyboardToolbarManager 全局管理键盘工具栏,提供统一设置。

  • 主要功能

    • 启用和定制工具栏,检查导航能力。
  • 主要方法

    • isEnabled: 启用或禁用管理器(Bool)。
    • toolbarConfiguration: 定制工具栏外观(如 tintColor、barTintColor)。
    • canGoPrevious, canGoNext: 检查是否能导航(只读)。
    • goPrevious(), goNext(): 导航到前一或后一输入框。
  • 使用示例

    import IQKeyboardToolbarManager
    
    IQKeyboardToolbarManager.shared.isEnabled = true
    IQKeyboardToolbarManager.shared.toolbarConfiguration.tintColor = .blue
    
    • 注解:启用工具栏管理器,设置工具栏颜色为蓝色。

7. IQTextInputViewNotification

  • 概述
    IQTextInputViewNotification 方便订阅 UITextField/UITextView 的聚焦和失焦事件。

  • 主要功能

    • 订阅输入框事件,获取事件和输入视图信息。
  • 主要方法

    • init(): 初始化通知实例。
    • subscribe(identifier:action:): 订阅事件,闭包接收事件信息。
    • unsubscribe(identifier:): 取消订阅。
  • 使用示例

    import IQTextInputViewNotification
    
    private let textInputViewObserver: IQTextInputViewNotification = .init()
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        textInputViewObserver.subscribe(identifier: "myID") { info in
            print("Event: \(info.event.name), TextInputView: \(info.textInputView)")
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        textInputViewObserver.unsubscribe(identifier: "myID")
    }
    
    • 注解:订阅输入框事件,打印事件名称和相关视图。

8. IQTextView

  • 概述
    IQTextView 是 UITextView 的子类,支持占位符功能。

  • 主要功能

    • 设置占位符文本和颜色,兼容 IQKeyboardToolbarManager。
  • 主要方法

    • placeholder: 设置占位符文本。
    • placeholderTextColor: 设置占位符颜色。
  • 使用示例

    import IQTextView
    
    let textView = IQTextView()
    textView.placeholder = "Enter your message here..."
    textView.placeholderTextColor = .lightGray
    
    • 注解:创建支持占位符的文本视图,设置占位符和颜色。

功能与方法总结表

以下是各库的主要功能和方法汇总:

库名 主要功能 主要方法
IQKeyboardManagerSwift 自动键盘管理,工具栏定制 isEnabled, keyboardDistanceFromTextField, goNext()
IQKeyboardCore 辅助功能,视图扩展 viewContainingController(), superviewOf(type:)
IQKeyboardNotification 键盘事件订阅 subscribe(identifier:action:), unsubscribe(identifier:)
IQKeyboardReturnManager 回车键导航管理 addResponderSubviews(of:recursive:), dismissTextViewOnReturn
IQKeyboardToolbar 自定义键盘工具栏 addPreviousNextDone(target:previousAction:nextAction:doneAction:), hidePlaceholder
IQKeyboardToolbarManager 全局工具栏管理 isEnabled, toolbarConfiguration, canGoPrevious
IQTextInputViewNotification 输入框事件监听 subscribe(identifier:action:), unsubscribe(identifier:)
IQTextView 支持占位符的文本视图 placeholder, placeholderTextColor

iOS 实现自定义对象深拷贝(OC/Swift)

iOS 实现自定义对象深拷贝(OC/Swift)

在 OC 中,对象的赋值操作是浅拷贝(指针复制),创建一个指针,旧对象引用对象计数加 1。在 Swift 中,类是引用类型,引用类型在赋值和传参时都是操作的引用,这些引用指向同一个对象。如下,对 classA 的修改也会影响到 classB:


classTestClass{        

  var str = "my class"

}

let classA =TestClass()

let classB =classA

classA.str="your class"

print(classB.str)    //your class

接下来介绍下如何分别在 OC 和 Swift 中实现自定义对象深拷贝

OC 方式:

遵守 NSCopying 协议,实现 copyWithZone 方法,通过 Runtime 实现自定义类的深拷贝,核心思路是 动态遍历类的所有属性并递归复制其值


@interface TestObject : NSObject<NSCopying>

@property (nonatomic, strong) NSString *name;

@property (nonatomic, assign) int age;

@end

  


@implementation TestObject

- (id)copyWithZone:(NSZone *)zone {

    TestObject *copy = [[[self class] alloc] init];

    unsigned int propertyCount = 0;

    objc_property_t *propertyList = class_copyPropertyList([self class], &propertyCount);

    for (int i = 0; i < propertyCount; i++ ) {

        objc_property_t thisProperty = propertyList[i];

        const char* propertyCName = property_getName(thisProperty);

        NSString *propertyName = [NSString stringWithCString:propertyCName encoding:NSUTF8StringEncoding];

        id value = [self valueForKey:propertyName];

        [copy setValue:value forKey:propertyName];

    }

    return copy;

}

@end

  


//调用  

TestObject *testObject = [[TestObject alloc] init];

testObject.name = @"test";

testObject.age = 10;

  


TestObject *copyObject = [testObject copy];

NSLog(@"copyObject:%@ %ld", copyObject.name,copyObject.age);// 打印结果:test&10

Swift 方式:

1. 遵循 NSCopying 协议(适用于继承自 NSObject 的类)

在下述示例中,Person 类和其属性 Address 类都遵循了 NSCopying 协议,并在 copy(with:) 方法中   实现了各自的深拷贝逻辑。


class Address: NSObject, NSCopying {

    var city: String

  


    init(city: String) {

        self.city = city

    }

  


    func copy(with zone: NSZone? = nil) -> Any {

        return Address(city: self.city)

    }

}

  


class Person: NSObject, NSCopying {

    var name: String

    var address: Address

  


    init(name: String, address: Address) {

        self.name = name

        self.address = address

    }

  


    func copy(with zone: NSZone? = nil) -> Any {

        let addressCopy = self.address.copy() as! Address

        return Person(name: self.name, address: addressCopy)

    }

}

2. 使用 Codable 协议进行序列化与反序列化(适用于纯 Swift 类)

如果你的类不继承自 NSObject,可以通过让类遵循 Codable 协议,利用 JSON 编码与解码来实现深拷贝。


extension Encodable where Self: Decodable {

    func deepCopy() -> Self? {

        do {

            let data = try JSONEncoder().encode(self)

            return try JSONDecoder().decode(Self.self, from: data)

        } catch {

            print("深拷贝失败: \(error)")

            return nil

        }

    }

}


// 要求类实现Codable协议

class Person: Codable {

    var name: String

    var friends: [Person]

    

    init(name: String, friends: [Person]) {

        self.name = name

        self.friends = friends

    }

}

//调用

let alice = Person(name: "Alice", friends: [])

let bob = Person(name: "Bob", friends: [alice])


if let bobCopy = bob.deepCopy() {

    print(bobCopy.name,bobCopy.friends) // "Bob [Person]"

}

3. 自定义 Copyable 协议(适用于纯 Swift 类)


protocol Copyable {

    func copy()-> Self

}


class MyClass:Copyable{

    var des = " "

    func copy() -> Self{


    let obj = self.dynamicType.init(self.des)

        return obj

    }

    required init(_ des:String){

        self.des = des

    }

}


let myClass = MyClass("my class")

let yourClass = myClass.copy()

yourClass.des = "your class"

print(yourClass.des)//your class

print(myClass.des)// my class

  


class Son:MyClass{

}

let sonA = Son("sonA")

let sonB = sonA.copy()

sonB.des = "sonB"

print(sonA.des)//sonA

print(sonB.des)//sonB

音视频学习笔记十七——图像处理之OpenCV基础二

题记:前文介绍OpenCV的基本操作,本节会继续介绍OpenCV的更多效果。图像处理需要用到很多专业的算法,本人业余学习略知皮毛,只是庶竭驽钝叙其所得,在音视频学习Demo有一些的示例。文章或代码若有错误,也希望大佬不吝赐教。

一、滤波

编码原理中介绍了滤波效果,前文中是Python代码,C++代码类似,高通滤波如下

    cv::Mat gray;
    cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);

    // 强制对齐到偶数尺寸
    int evenRows = (gray.rows + 1) & -2;
    int evenCols = (gray.cols + 1) & -2;

    cv::Mat channelPadded;
    cv::copyMakeBorder(gray, channelPadded,
                     0, evenRows - gray.rows,
                     0, evenCols - gray.cols,
                     cv::BORDER_CONSTANT, cv::Scalar::all(0));
    cv::Mat filter = cv::Mat::ones(channelPadded.size(), CV_32F);
    for(int i=0; i<100; i++) {
        for(int j=0; j<100; j++) {
            filter.at<float>(i,j) = 0.0;
        }
    }

    // DCT变换处理
    cv::Mat dctMat;
    channelPadded.convertTo(dctMat, CV_32F);
    
    // 前向DCT变换
    cv::dct(dctMat, dctMat);
    
    // 应用DCT域滤波器
    cv::multiply(dctMat, filter, dctMat);
    
    // 逆向DCT变换
    cv::idct(dctMat, dctMat);
    
    // 转换为8位并裁剪
    cv::Mat result;
    dctMat.convertTo(result, CV_8U);
    result = result(cv::Rect(0, 0, gray.cols, gray.rows));
    cv::bitwise_not(result, result);
    return result;
原图 opencv原图.jpg
高通 opencv高通.jpg
低通 opencv低通.jpg

二、直方图均衡

2.1 直方图均衡

直方图均衡,可以先看效果,再说原理,均衡代码如图:

cv::Mat gray, equalized;
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
cv::equalizeHist(gray, equalized);

效果如图,上图是原图,下图是均衡化效果:

opencv直方图.jpg

由上图可见,在偏暗或者偏亮的环境下,色值会集中在一部分。如右边是直方图的分布,横坐标是0-255的色值,纵坐标是统计值。均衡化的目标是将直方图的分布均匀一些,如下方的图。

cv::Mat origHist, eqHist;
int histSize = 256;
float range[] = {0,256};
const float* histRange = {range};
cv::calcHist(&gray, 1, 0, cv::Mat(), origHist, 1, &histSize, &histRange);
cv::calcHist(&equalized, 1, 0, cv::Mat(), eqHist, 1, &histSize, &histRange);

2.2 均衡化原理

基本思想是把图像的直方图分布变得更均匀,图像的对比度就会提高,效果如上图。

说明 公式
统计图像中每个灰度级 image.png
每个灰度级的累积分布 image.png
将CDF线性映射到0-255范围,生成像素值的映射表:M×N为图像总像素数, image.png
应用映射表 --

上述公式其实解释起来很简单,就是把色值转换为累计分布,如下图0-4最后映射的结果。密度大的色值范围会变大,密度小的色值范围变小,从而达到均衡的目的。

原始值色 0 1 2 3 4
概率 0.3 0.3 0.2 0.1 0.1
累积概率 0.3 0.6 0.8 0.9 1.0
映射 1.2 2.4 3.2 3.6 4.0

2.3 自适应均衡化

上述属于全图均衡化,但图片如果偏大的情况下,自适应均衡化效果会更好一些。自适应实际上是对图块划分为格子,如下代码中的 8x8大小格子。对每个格子进行均衡化处理。最后,格子之间的分界进行线性插值得到最后结果。

cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(4.0);
clahe->setTilesGridSize(cv::Size(8, 8));

自适应均衡化效果:

opencv自适应均衡.jpg

三、特征匹配

3.1 特征匹配

本文主要介绍一下SIFT算法,其他改进算法还有SURF,此外还有ORB和AKAZE。SIFT广泛应用在图像匹配与拼接、物体识别与跟踪等。SIFT在目前版本已经免费可用(专利过期),opencv需要包含opencv_contrib模块,pod支持的版本不包含,需要自己编译一下。

git clone https://github.com/opencv/opencv.git # 切换到合适版本
git clone https://github.com/opencv/opencv_contrib.git # 切换到合适版本
cd opencv/platforms/ios
python build_framework.py ios --contrib ../../opencv_contrib --disable-swift

特征匹配效果(SIFT特征+暴力匹配),相关代码音视频学习Demo

特征匹配.jpg

相关代码如下:

// 初始化SIFT检测器
 auto detector = cv::SIFT::create();

 // 检测关键点和计算描述子
 std::vector<cv::KeyPoint> queryKpts, sceneKpts;
 cv::Mat queryDesc, sceneDesc;
 detector->detectAndCompute(queryMat, cv::noArray(), queryKpts, queryDesc);
 detector->detectAndCompute(sceneMat, cv::noArray(), sceneKpts, sceneDesc);

 // 特征匹配
 auto matcher = cv::BFMatcher::create(cv::NORM_L2);
 std::vector<cv::DMatch> matches;
 matcher->match(queryDesc, sceneDesc, matches);

 // 筛选最佳匹配
 std::sort(matches.begin(), matches.end());
 const int keepMatches = matches.size() * 0.05;
 std::vector<cv::DMatch> goodMatches(matches.begin(), matches.begin() + keepMatches);

 // 可视化匹配结果
 cv::Mat resultMat;

 cv::drawMatches(queryMat, queryKpts,
                sceneMat, sceneKpts,
                goodMatches, resultMat,
                cv::Scalar::all(-1), cv::Scalar::all(-1),
                std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);

 return resultMat;

SIFT(Scale-Invariant Feature Transform)是计算机视觉中经典的局部特征检测与描述算法。其核心优势在于对尺度缩放、旋转、光照变化、视角变化等具有强鲁棒性。

  • 关键点检测的核心思想通过把图片模糊、缩放得到几组图片,然后通过差值得到能够识别图片特征的点。 DOG.jpg

  • 利用三维泰勒展开调整极值点位置,提高定位精度。

  • 在关键点所在尺度的高斯图像中,计算邻域像素的梯度幅值和方向。生成36-bin直方图(每10°一bin),使用σ=1.5×关键点尺度的高斯窗口加权梯度幅值。主方向为最高峰,辅方向为超过主峰80%的次峰。

  • 将坐标轴旋转至主方向,确保描述符与方向无关。这一步是方向不变的关键

  • 区域划分,16×16窗口分为4×4子区域,每个子区域计算8方向梯度直方图,形成128维向量。

3.2 匹配

关于匹配方法,一般下列几种:

3.2.1 暴力匹配(Brute-Force Matcher)

  • 原理:遍历第一幅图像的所有特征描述符,与第二幅图像的每个描述符计算距离,选择最优匹配。

  • 用途:小规模数据集、精确匹配。

  • 代码示例

    import cv2
    
    # 创建BFMatcher对象(使用汉明距离,适用于ORB、BRISK等)
    bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
    
    # 匹配描述符
    matches = bf.match(des1, des2)
    
    # 按距离排序
    matches = sorted(matches, key=lambda x: x.distance)
    

3.2.2 快速近似最近邻(FLANN)

  • 原理:基于高维数据的近似最近邻搜索,通过树结构加速匹配。

  • 用途:大规模数据集、实时性要求高的场景。

  • 代码示例

    # 设置FLANN参数(适用于SIFT、SURF等)
    FLANN_INDEX_KDTREE = 1
    index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
    search_params = dict(checks=50)  # 搜索次数
    
    flann = cv2.FlannBasedMatcher(index_params, search_params)
    matches = flann.knnMatch(des1, des2, k=2)
    

3.2.3 比率测试(Ratio Test)

  • 原理:对每个关键点的两个最佳匹配计算距离比值(如0.7),过滤模糊匹配。

  • 用途:去除错误匹配,提高匹配质量。

  • 代码示例

    good_matches = []
    for m, n in matches:
        if m.distance < 0.7 * n.distance:
            good_matches.append(m)
    

3.2.4 基于几何约束的筛选(RANSAC)

  • 原理:通过随机采样一致性算法(RANSAC)估计基础矩阵或单应性矩阵,剔除不符合几何约束的匹配。

  • 用途:图像拼接、相机位置估计。

  • 代码示例

    src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1,1,2)
    dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1,1,2)
    
    # 使用RANSAC计算单应性矩阵
    H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
    
    # 保留内点(符合几何约束的匹配)
    inlier_matches = [m for i, m in enumerate(good_matches) if mask[i]]
    

3.2.5 总结

方法 优点 缺点 适用场景
暴力匹配 精确度高 计算复杂度高,速度慢 小规模数据、精确匹配需求
FLANN 速度快,适合高维数据 近似匹配,可能存在误差 大规模数据、实时性要求高
比率测试 有效过滤模糊匹配 可能丢失部分正确匹配 提高匹配质量
RANSAC 鲁棒性强,抗噪声 依赖初始匹配的准确性 几何模型估计(如单应性)

iOS 开发者必备:深入理解 for-in 循环的实现原理

前言

在 iOS 开发中,for-in 循环以其简洁优雅的语法和高效的遍历性能,成为了开发者遍历集合对象的首选方式。然而,你是否曾好奇过:为什么 for-in 循环能够如此高效地遍历集合对象?它的底层究竟是如何实现的?本文将深入剖析 ObjC 中 for-in 遍历的底层实现原理,从 NSFastEnumeration 协议到快速枚举器的具体实现,带你揭开 for-in 遍历的神秘面纱。通过理解其底层机制,你将能够更好地运用这一重要技术,并在实际开发中做出更优的技术选择。

注:虽然 Swift 中的 for-in 语法与 ObjC 类似,但它们的底层实现原理并不相同,本文主要聚焦于 ObjC 中的实现细节。

for-in 底层实现原理

让我们通过一个简单的示例代码,来开始探索 for-in 遍历的底层实现原理:

int main(int argc, const char * argv[]) {
    NSArray *arr = @[@1, @2, @3];
    for (NSNumber *number in arr) {
        printf("num: %ld\n", [number integerValue]);
    }
    return 0;
}

为了深入理解 for-in 循环的底层实现原理,我们可以使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-18.0.0 main.m 命令将 ObjC 代码转换为 C++ 代码。

struct __objcFastEnumerationState {
    /*
    一个用于追踪迭代状态的值,一般是内部已经遍历过的对象数量。
    在每次调用协议方法时会更新。
    */
unsigned long state;
// 一个指向包含当前批次对象的 C 数组指针
void **itemsPtr;
/*
    在一个可变子类中,mutationsPtr 被设置为指向一个值,每当容器被修改(添加、删除、重新排序)时,这个值将被改变。
    这个值在调用者处被缓存,并在每次迭代时进行比较。如果在迭代过程中发生变化,则调用相应函数处理(一般是抛出异常)。
    */
unsigned long *mutationsPtr;
// 保留数组,用于存储额外的状态信息。
unsigned long extra[5];
};

int main(int argc, const char * argv[]) {
    // 这里省略了 arr 数组的初始化相关代码

    {
        NSNumber *number;
        struct __objcFastEnumerationState enumState = { 0 };
        // 一个用于存储当前批次对象的 C 数组
        id __rw_items[16];

        /*
        等同于:[arr countByEnumeratingWithState:&enumState objects:__rw_items count:16]
        这个方法会返回当前批次需要遍历的对象数量。
        */
        unsigned long long limit = objc_msgSend(arr,
                                                sel_registerName("countByEnumeratingWithState:objects:count:"),
                                                &enumState,
                                                __rw_items,
                                                16);
        // 如果 limit 大于 0,说明有对象需要遍历
        if (limit) {
            // 缓存当前集合的修改状态,用于检测遍历过程中是否对集合进行了修改操作。
            unsigned long startMutations = *enumState.mutationsPtr;

            do {
                // 当前批次对象的索引,从 0 开始
                unsigned long counter = 0;
                
                do {
                    // 检查遍历过程中是否对集合进行了修改操作,如果修改了则抛出异常
                    if (startMutations != *enumState.mutationsPtr) {
                        objc_enumerationMutation(arr);
                    }
                    
                    // 取出当前批次的对象,并执行循环体内的代码
                    number = (NSNumber *)enumState.itemsPtr[counter++];
                    {
                        printf("num: %ld\n", objc_msgSend(number, sel_registerName("integerValue")));
                    }
                // 遍历当前批次的对象,直到遍历完当前批次的所有对象
                } while (counter < limit);
            
            // 获取下一批次需要遍历的对象,直到没有需要遍历的对象为止。
            } while (limit = objc_msgSend(arr,
                                        sel_registerName("countByEnumeratingWithState:objects:count:"),
                                        &enumState,
                                        __rw_items,
                                        16));
            number = (NSNumber *)0;
        } else {
            number = (NSNumber *)0;
        }
    }
}

通过分析转换后的代码,我们可以清晰地看到 for-in 遍历的实现原理是依靠 2 层 do-while 循环加上 countByEnumeratingWithState:objects:count: 方法来实现的。总结如下:

  1. 调用 countByEnumeratingWithState:objects:count: 方法获取当前批次需要遍历的对象;
  2. 遍历当前批次的所有对象,并执行 for-in 循环体内的代码;
  3. 重复执行步骤一和步骤二,直到没有需要遍历的对象为止。

NSArray 的 for-in 实现

虽然苹果没有开源 NSArray 的 countByEnumeratingWithState:objects:count: 方法实现,但我们可以通过分析 GNUStep 开源项目中的实现来理解其工作原理。GNUStep 是一个开源库,它将 ObjC 的 Cocoa 库重新实现了一遍,虽然它不是苹果官方源码,但还是具有一定的参考价值。

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(__unsafe_unretained id [])stackbuf
                                    count:(NSUInteger)len {
    NSInteger count;
    
    state->mutationsPtr = (unsigned long *)&state->mutationsPtr;
    // 计算当前批次需要遍历的对象数量
    count = MIN(len, [self count] - state->state);
    
    // 检查数组中是否还有未遍历的对象
    if (count > 0) {
        IMP imp = [self methodForSelector:@selector(objectAtIndex:)];
        // 当前遍历的索引
        int p = state->state;
        
        for (int i = 0; i < count; i++, p++) {
            // 获取数组中的对象,并填充到缓冲区。
            stackbuf[i] = (*imp)(self, @selector(objectAtIndex:), p);
        }
        // 更新 state,表示已经遍历过的对象数量。
        state->state += count;
    } else {
        count = 0;
    }
    
    // 将缓冲区赋值给 state->itemsPtr,表示当前批次需要遍历的对象
    state->itemsPtr = stackbuf;

    // 返回当前批次需要遍历的对象数量
    return count;
}

通过分析 GNUStep 的实现,我们可以大致理解 NSArray 是如何实现快速枚举的。countByEnumeratingWithState:objects:count: 方法是 for-in 循环的核心,它的实现逻辑可以分为以下几个关键步骤:

  1. 批量计算:根据缓冲区大小和剩余未遍历对象数量,计算当前批次可以返回的对象数量,避免一次性加载过多数据;
  2. 批量获取:通过 objectAtIndex: 方法批量获取数组元素,并填充到缓冲区中,减少方法调用开销;
  3. 状态更新:更新遍历状态,记录已遍历的对象数量,确保遍历的连续性;
  4. 指针设置:将缓冲区的起始地址赋值给 state->itemsPtr,供 for-in 循环直接访问,提高访问效率;
  5. 数量返回:返回当前批次实际获取的对象数量。

这种批量获取的设计是 for-in 循环性能优异的关键。与传统的 for 循环相比,for-in 循环通过一次性返回多个元素的方式,显著减少了方法调用的次数,从而提升了遍历效率。同时,for-in 循环还实现了完善的修改检测机制,确保遍历过程的安全性:

  • 集合对象在 state->mutationsPtr 中维护一个修改计数器,用于追踪集合的修改状态
  • 每次对集合进行修改操作时,这个计数器的值都会自动更新
  • for-in 循环在每次迭代时都会检查这个值,确保遍历过程中集合未被修改
  • 一旦检测到集合在遍历过程中被修改,立即抛出异常,防止数据不一致

这种设计巧妙地平衡了性能和安全性,使得 for-in 循环既能高效地遍历集合对象,又能保证遍历过程的安全性,这也是它成为 iOS 开发中首选遍历方式的重要原因。

你可能会有这样的疑问:countByEnumeratingWithState:objects:count: 方法内部不也是通过遍历获取元素吗?为什么把外面的遍历操作挪到里面就能提高性能呢?

你可以这样去想,假设现在有 1000 个货物要从上海运到北京,如果是普通的 for 循环,它的逻辑大致是这样的:每次遍历到 1 个对象时,就安排一辆车从北京开到上海,然后把货物从上海运到北京再使用。相当于你要从北京-上海往返 1000 次,才能把所有货物运到北京。

而 for-in 循环的逻辑是这样的:当你需要遍历时,安排一辆车从北京开到上海,与之前不一样的是这次拉 16 个货物(这个 16 就是缓冲区大小)运到北京。当你需要使用第 2~16 个货物时,直接从车上拿就行,不需要再安排车从北京开到上海。通过对比可以发现,前者需要往返 1000 次,而这种方案只需要往返 1000 / 16 ≈ 63 次。这就是 for-in 循环性能优异的关键。遍历的数量越多,for-in 循环的性能优势就越明显。

下面是我在单线程下对不同遍历方式做的一个性能基准测试(测试机型:iPhone 14,系统版本:iOS 18.0):

性能对比图表

可以看到 for-in 循环的性能是最好的,而 while 循环的性能是最差的,两者差了 3 倍左右。

测试代码我放在这里,感兴趣的可以自己运行看看:iOS 不同遍历方式性能测试

实战应用

通过前面的分析,我们已经深入理解了 for-in 循环的底层实现机制。现在,让我们动手实现一个支持 for-in 循环的自定义类,在实践中加深对 NSFastEnumeration 协议的理解。

@interface WXLFastEnumeration : NSObject<NSFastEnumeration>
- (void)addObject:(id)obj;
@end

@implementation WXLFastEnumeration {
    id _arr[34];
    int _idx;
    NSInteger _changeCount;
}

- (void)addObject:(id)obj {
    _arr[_idx++] = obj;
    changeCount += 1;
}

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(__unsafe_unretained id [])buffer
                                    count:(NSUInteger)len {
    NSInteger count = 0;
    state->mutationsPtr = (unsigned long *)&changeCount;
    count = MIN(len, _idx - state->state);
    
    if (count > 0) {
        memcpy(buffer, (const void *)&_arr[state->state], sizeof(id) * count);
        state->state += count;
    }
    
    state->itemsPtr = buffer;
    return count;
}

@end

int main(int argc, const char * argv[]) {
    WXLFastEnumeration *fast = [[WXLFastEnumeration alloc] init];
    for (int i = 1; i <= 33; i++) {
        [fast addObject:@(i)];
    }

    for (NSNumber *num in fast) {
        NSLog(@"num: %@", num);
        if ([num integerValue] == 5) {
            // 删除以下注释测试崩溃场景
            // [fast addObject:@(34)];
        }
    }
}

在这个示例中,我使用了 memcpy 函数替代了 GNUStep 实现中的逐元素复制。通过这个对比,你应该能更直观地理解为什么 for-in 循环在遍历大量数据时比传统的 for 循环性能更好。

总结

通过本文的深入分析,我们可以看到 for-in 循环的实现原理主要包含以下几个方面:

  1. NSFastEnumeration 协议:作为 for-in 循环的核心,NSFastEnumeration 协议定义了 countByEnumeratingWithState:objects:count: 方法,使集合对象能够批量返回元素。这种设计避免了频繁的方法调用,为性能优化奠定了基础。

  2. 批量处理机制:for-in 循环采用缓冲区(buffer)批量获取元素,而不是传统的逐个获取。这种批量处理方式显著提升了遍历效率,特别是在处理大规模数据时,性能优势更为明显。

  3. 状态管理:通过 NSFastEnumerationState 结构体,for-in 循环实现了遍历状态的精确管理。它不仅记录当前遍历位置,还通过 mutationsPtr 实现了对集合修改的实时检测,确保了遍历过程的可靠性。

  4. 安全性保障:for-in 循环在每次遍历开始时都会进行集合修改检测,一旦发现集合被修改,立即抛出异常。这种机制有效防止了遍历过程中的数据不一致问题,为开发者提供了可靠的安全保障。

  5. 双层循环设计:for-in 循环采用了两层 do-while 循环的巧妙设计。外层循环负责批量获取数据到缓冲区,内层循环则专注于处理缓冲区中的元素。这种设计既保证了遍历的连续性,又充分利用了批量处理的性能优势。

理解这些实现原理,对于 iOS 开发者来说至关重要。它不仅帮助我们更好地使用 for-in 循环,还能指导我们在实际开发中做出更明智的技术选择。例如,在处理大量数据时,我们可以充分利用 for-in 循环的批量处理优势;而在需要修改集合的场景下,我们则需要特别注意避免在遍历过程中修改集合,以防止异常发生。

总的来说,for-in 循环是 ObjC 中一个设计精妙的语法特性。它通过批量处理、状态管理和安全检测等机制,在保证使用便利性的同时,也兼顾了性能和安全性。深入理解其实现原理,能够帮助我们在 iOS 开发中更好地运用这一特性,写出更高效、更可靠的代码。

Kingfisher图像处理库

Kingfisher 是一个功能强大的 Swift 图像处理库,专注于从网络加载、缓存和显示图像,广泛用于 iOS 开发。其 GitHub 仓库提供了丰富的文档和示例,方便开发者快速集成和使用。

方法和功能分类

Kingfisher 的方法主要通过扩展现有 UIKit 和 SwiftUI 组件实现,提供了便捷的链式调用和回调机制。以下按功能分类整理:

1. 基本图像加载
  • 方法: imageView.kf.setImage(with: url)
  • 描述: 将图像从 URL 异步下载并设置到 UIImageView,支持占位符、处理器、过渡动画和完成回调。
  • 代码示例:
    let url = URL(string: "https://example.com/image.png")
    imageView.kf.setImage(with: url, placeholder: UIImage(named: "placeholder"), options: [.transition(.fade(0.25))], completionHandler: { result in
        switch result {
        case .success(let value):
            print("Image loaded: \(value.image)")
        case .failure(let error):
            print("Error: \(error)")
        }
    })
    
  • 注解: 这是最常用的方法,支持多种选项配置,适合简单场景。
2. 方法链式调用(KF.url)
  • 方法: KF.url(url)
  • 描述: 通过方法链式调用配置图像加载选项,然后应用到目标视图(如 UIImageView),提供更灵活的控制。
  • 代码示例:
    KF.url(URL(string: "https://example.com/image.png"))
      .placeholder(UIImage(named: "placeholder"))
      .setProcessor(DownsamplingImageProcessor(size: imageView.bounds.size))
      .transition(.fade(1))
      .cacheOriginalImage()
      .onProgress { receivedSize, totalSize in
          print("\(receivedSize)/\(totalSize)")
      }
      .onSuccess { result in
          print("Success: \(result.image)")
      }
      .onFailure { error in
          print("Error: \(error)")
      }
      .set(to: imageView)
    
  • 注解: 支持占位符、图像处理器、过渡动画、缓存策略、进度和完成回调,适合复杂场景。
3. SwiftUI 支持(KFImage)
  • 方法: KFImage.url(url)
  • 描述: 在 SwiftUI 中加载和显示图像,支持链式调用配置,类似于 KF.url
  • 代码示例:
    KFImage(URL(string: "https://example.com/image.png"))
      .placeholder {
        Image("placeholder")
      }
      .resizable()
      .scaledToFit()
      .onProgress { receivedSize, totalSize in
          print("\(receivedSize)/\(totalSize)")
      }
      .onSuccess { result in
          print("Success: \(result.image)")
      }
      .onFailure { error in
          print("Error: \(error)")
      }
    
  • 注解: 适用于 SwiftUI 项目,提供类似的功能和配置选项。
4. 图像处理器
  • 方法: .setProcessor(processor)
  • 描述: 应用图像处理器,例如缩放、圆角等,内置多种处理器如 DownsamplingImageProcessorRoundCornerImageProcessor
  • 代码示例:
    let processor = RoundCornerImageProcessor(cornerRadius: 20)
    imageView.kf.setImage(with: URL(string: "https://example.com/image.png"), options: [.processor(processor)])
    
  • 注解: 适合对图像进行预处理,如调整大小或添加圆角。
5. 缓存管理
  • 方法: KingfisherManager.shared.cache
  • 描述: 访问和管理 Kingfisher 的缓存系统,包括内存缓存和磁盘缓存,支持清理、检查状态等。
  • 代码示例:
    let cache = KingfisherManager.shared.cache
    cache.clearDiskCache()
    cache.calculateDiskCacheSize { size in
        print("Disk cache size: \(size) bytes")
    }
    
  • 注解: 提供细粒度的缓存控制,适合优化性能。
6. 预加载图像
  • 方法: ImagePrefetcher(urls: [url])
  • 描述: 预加载一组图像以提升加载速度,适合在应用启动时或预期需要时使用。
  • 代码示例:
    let urls = [URL(string: "https://example.com/image1.png"), URL(string: "https://example.com/image2.png")]
    ImagePrefetcher(urls: urls).start()
    
  • 注解: 提高用户体验,减少首次加载延迟。
7. 其他 UI 组件扩展
  • 方法: button.kf.setImage(with: url, for: .normal)
  • 描述: 为其他 UI 组件(如 UIButtonNSButton)提供图像加载扩展。
  • 代码示例:
    button.kf.setImage(with: URL(string: "https://example.com/image.png"), for: .normal)
    
  • 注解: Kingfisher 支持多种 UI 组件的扩展,增强灵活性。
8. 过渡动画
  • 方法: .transition(.fade(duration))
  • 描述: 设置图像加载完成时的过渡动画,支持多种效果如淡入、缩放。
  • 代码示例:
    imageView.kf.setImage(with: URL(string: "https://example.com/image.png"), options: [.transition(.fade(1))])
    
  • 注解: 提升视觉效果,适合用户界面优化。
9. 占位符和指示器
  • 方法: .placeholder(image)
  • 描述: 在图像加载过程中显示占位符或指示器,支持自定义图像或系统指示器。
  • 代码示例:
    imageView.kf.setImage(with: URL(string: "https://example.com/image.png"), placeholder: UIImage(named: "placeholder"))
    
  • 注解: 提供加载过程中的用户反馈,增强体验。
10. 低数据模式支持
  • 方法: .lowDataModeSource(.network(lowResolutionURL))
  • 描述: 在低数据模式下,使用低分辨率图像,优化流量和性能。
  • 代码示例:
    KF.url(URL(string: "https://example.com/image.png"))
      .lowDataModeSource(.network(URL(string: "https://example.com/low-res.png")))
      .set(to: imageView)
    
  • 注解: 适合移动设备在低数据模式下的优化。
11. 进度和完成回调
  • 方法: .onProgress.onSuccess.onFailure
  • 描述: 分别用于监控下载进度、处理成功和失败情况,提供对下载过程的全面控制。
  • 代码示例:
    KF.url(URL(string: "https://example.com/image.png"))
      .onProgress { receivedSize, totalSize in
          print("\(receivedSize)/\(totalSize)")
      }
      .onSuccess { result in
          print("Success: \(result.image)")
      }
      .onFailure { error in
          print("Error: \(error)")
      }
      .set(to: imageView)
    
  • 注解: 适合需要实时反馈的场景,如进度条显示。
12. Live Photo 支持
  • 方法: .loadLivePhoto
  • 描述: 加载和缓存 Live Photo,适用于支持 Live Photo 的场景。
  • 代码示例:
    imageView.kf.setImage(with: URL(string: "https://example.com/livephoto"), options: [.loadLivePhoto], completionHandler: nil)
    
  • 注解: 扩展了 Kingfisher 的功能,适合动态图像场景。

方法总结表

以下表格汇总了所有方法及其主要功能,便于快速查阅:

方法 主要功能
imageView.kf.setImage 基本图像加载,支持占位符和动画
KF.url 方法链式调用,灵活配置加载选项
KFImage.url SwiftUI 支持,加载和显示图像
.setProcessor 应用图像处理器,如缩放、圆角
KingfisherManager.shared.cache 管理缓存,清理和检查状态
ImagePrefetcher 预加载图像,提升加载速度
button.kf.setImage 扩展支持其他 UI 组件
.transition 设置加载完成时的过渡动画
.placeholder 设置加载过程中的占位符
.lowDataModeSource 低数据模式下使用低分辨率图像
.onProgress 监控下载进度
.onSuccess 处理加载成功
.onFailure 处理加载失败
.loadLivePhoto 加载和缓存 Live Photo

其他重要功能

  • 异步下载和缓存: Kingfisher 支持高效的异步下载和多级缓存(内存和磁盘),提升性能。
  • 自定义处理器: 用户可以扩展 Kingfisher 添加自定义图像处理器,满足特定需求。
  • 独立组件: 下载器、缓存系统和处理器可以独立使用,灵活性高。
  • SwiftUI 兼容性: 通过 KFImage 支持 SwiftUI,确保现代化开发支持。
  • Swift 6 和 Swift Concurrency 支持: 确保未来兼容性,适合长期项目。

在平淡中等待 WWDC 2025 | 肘子的 Swift 周报 #084

issue84.webp

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

在平淡中等待 WWDC 2025

不知不觉,距离 WWDC 2025 开始只有 20 天了。在过去的几年中,每当此时我都会写几篇文章畅想 WWDC 上会带来的新功能和我期待的一些变化。

然而,或许是因为最近两年 WWDC 上展示的许多新功能并未完全落地,就我个人而言,今年似乎少了往日的热情和渴望。希望这只是我个人的情况。

期待越高,失望越大。避免失望的最好方式莫过于调整期待值。今年,我选择以更加平和的心态迎接 WWDC 的到来,不过分期待,不盲目乐观,但也不放弃对技术进步的关注与思考。

你呢?对即将到来的 WWDC 2025,你有什么期待或想法?

前一期内容全部周报列表

原创

揭秘 .ignoredByLayout():让视觉变换“隐形”于布局之外

在 SwiftUI 的众多 API 中,.ignoredByLayout() 算是一位“低调的成员”。相关资料稀少,应用场景也不常见,其名称本身就容易引发困惑。它似乎暗示着某种对布局的“忽略”,但这与我们熟知的 offsetscaleEffect 等修饰符默认不影响父布局的行为有何不同? ignoredByLayout 究竟在什么时机工作?它到底“忽略”或“隐瞒”了什么?本文将为你揭开这个 SwiftUI 布局机制中微妙 API 的面纱。

近期推荐

为上下文菜单加点料 (Accessorise Your Context Menu Interactions)

在 iOS 信息应用中,用户点击消息后会弹出一个包含多种表情的辅助视图,其精巧的设计和自然的展开动画令人印象深刻。遗憾的是,苹果始终未开放这一实现方式的相关 API。在本文中,Seb Vidal 详尽介绍了如何通过 UIKit 的私有类 _UIContextMenuAccessoryView,为自己的 App 添加类似的交互体验。由于涉及私有 API,该方案存在 App Store 审核风险。为此,作者还提供了一个 App Review 安全的 Swift 实现分支,通过更动态和间接的方式实现类似效果,适合在测试或内部工具中使用。

Aether 基于该研究开发了 MenuWithAView,使 SwiftUI 开发者也能轻松为上下文菜单添加自定义辅助视图。


图表对齐之谜,终于解开了 (Finally Fixing My Swift Charts Alignment Problem)

在使用 Swift Charts 时,Chris Wu 遇到了一个奇怪的问题:基于 LineMark 绘制的图表始终无法精确对齐午夜,往往略晚开始、略早结束。经过长时间查阅文档、向 AI 求助无果后,最终他在 Stack Overflow 中一条仅有两赞的评论中找到了关键线索。

问题出在 .value(_:unit:)unit 参数上——它会让绘图点落在两个单位之间的中点(例如 .hour 会将 8:00 显示在 8:30),这虽然适用于柱状图,却会使折线图产生对齐偏差。移除 unit:calendar: 后,图表终于与午夜轴线完美对齐。

AI 的知识边界受限于语料覆盖面,对于这种缺乏广泛讨论的问题,反而不如一个冷门但关键的手动搜索来得管用。


让 Picker 支持“未选择” (SwiftUI Picker With Optional Selection)

SwiftUI 中有不少实用却鲜为人知的 API 细节——至少在读这篇文章之前,我并不知道 .tag 可以直接支持 nil。在本文中,Keith Harrison 展示了一个简单而实用的技巧:如何让 Picker 与可选类型的 Binding 协同工作,并支持“不选任何值”这一场景,适用于诸如“无项目”或“重置选择”等常见需求。


Swift 6.2 默认隔离机制解析 (Default isolation with Swift 6.2)

在 Swift 6 对并发的严格检查下,即便代码只在单线程中运行,开发者仍需添加大量显式标注以满足类型系统的安全要求。为此,Swift 6.2 引入了一个重要改进:默认隔离(default isolation)Matt Massicotte 在本文中介绍了如何通过 .defaultIsolation(MainActor.self) 在模块级设置默认隔离策略,从而减少冗余标注,并改善 Swift 并发的开发体验。

这是一次给予开发者更多控制权的更新,但也意味着更大的设计抉择:是将整个项目默认隔离为 @MainActor,还是继续使用显式标注?Swift 正在迈向更安全的并发模型,而如何选择默认值,将成为每个团队的重要决策。


别把 SQLite 放进 App Group (SQLite Databases in App Group Containers: Just Don't)

为了在 Widget 和 App Intents 中共享数据,许多开发者选择将 SQLite 数据库存放在 App Group 容器中。然而,Ryan Ashcraft 指出,这种看似合理的做法可能导致难以调试的系统崩溃,最典型的是神秘的 0xDEAD10CC 错误。该错误并非死锁,而是 iOS 为防止挂起进程长期持有文件锁、阻塞其他进程访问数据库,而强制终止 App 的机制。Ryan 在文中详解了触发机制及多个缓解方案,但也坦言这些策略实现复杂、效果有限。

0xDEAD10CC 是 iOS 系统层面的老问题,Michael Tsai 也就此整理了一个讨论汇总帖,欢迎加入交流。


用自定义 Modifier 优雅管理焦点 (Simplifying Focus Management in SwiftUI with a Custom ViewModifier)

SwiftUI 的 @FocusState 虽然为聚焦控制提供了便利,但其局限也很明显:无法与 @Binding 直接联动、难以在视图之间传递,且无法用于 ViewModel 中的状态管理。在复杂表单或状态驱动的 UI 中,这些问题尤其突出。Artem Mirzabekian 在本文中提出了一个更灵活的替代方案——FocusModifier,它通过可选绑定(Binding<T?>)管理聚焦状态:当值匹配时自动聚焦视图,失焦时清除绑定。这种做法使焦点控制更加可组合、可测试,也便于将逻辑抽离至 ViewModel。

工具

Swift 6.1 编程指南中文版

在过去的两个月里,SwiftGG 翻译组对《Swift 编程指南》进行了重要升级:不仅将手册内容同步至 Swift 6.1,还对中文官网的设计风格进行了调整,使其与 Swift 官方文档保持一致,带来更加统一和现代的阅读体验。

访问 SwiftGG 在 GitHub 上的仓库,了解如何参与《Swift 编程指南》中文版的维护工作。


RedLine

Redline 是由 Robb Böhnke 开发的 SwiftUI 视图 Modifier 合集,提供了丰富的可视化工具,用于标示视图的位置、尺寸、间距和对齐方式,帮助开发者快速验证布局实现或排查界面问题。

小提示:Robb 为每个 Modifier 都提供了代码预览,不仅便于理解和使用,也是一份出色的 SwiftUI 布局教学资源。

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

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

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

iOS 截取和分割音视频

在 iOS 开发中,截取或分割音视频是常见需求,适用于短视频剪辑、语音消息裁剪、媒体内容编辑等场景。使用 AVFoundation 框架可以高效实现这一功能。下面将详细介绍如何在 iOS 中截取或分割音视频,并提供完整的代码示例和使用方法。


✅ 核心思路

截取或分割音视频的核心步骤如下:

  1. 加载原始音视频文件AVURLAsset
  2. 设置时间范围CMTimeRange)指定要截取的起始时间与持续时间
  3. 创建导出会话AVAssetExportSession
  4. 导出目标文件(支持 .mp4.m4a 等格式)
  5. 处理异步导出完成回调

🎬 视频截取示例(Objective-C)

- (void)trimVideoFromURL:(NSURL *)inputURL startTime:(NSTimeInterval)startTime duration:(NSTimeInterval)duration completion:(void (^)(NSURL *outputURL, NSError *error))completion {
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:inputURL options:nil];
    
    // 1. 创建导出会话
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetHighestQuality];
    
    // 2. 设置输出路径和文件格式
    NSString *outputPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"trimmedVideo.mp4"];
    exportSession.outputURL = [NSURL fileURLWithPath:outputPath];
    exportSession.outputFileType = AVFileTypeMPEG4;
    
    // 3. 设置时间范围(start ~ start + duration)
    CMTime startCMTime = CMTimeMakeWithSeconds(startTime, 600);
    CMTime durationCMTime = CMTimeMakeWithSeconds(duration, 600);
    CMTimeRange timeRange = CMTimeRangeMake(startCMTime, durationCMTime);
    exportSession.timeRange = timeRange;
    
    // 4. 异步导出
    [exportSession exportAsynchronouslyWithCompletionHandler:^{
        if (exportSession.status == AVAssetExportSessionStatusCompleted) {
            NSLog(@"视频截取成功: %@", outputPath);
            if (completion) completion([NSURL fileURLWithPath:outputPath], nil);
        } else {
            NSError *error = exportSession.error;
            NSLog(@"视频截取失败: %@", error.localizedDescription);
            if (completion) completion(nil, error);
        }
    }];
}

✅ 使用方法

NSURL *videoURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"myVideo" ofType:@"mp4"]];
[self trimVideoFromURL:videoURL startTime:5.0 duration:10.0 completion:^(NSURL *outputURL, NSError *error) {
    if (outputURL) {
        NSLog(@"截取后的视频路径: %@", outputURL.path);
    }
}];

🎵 音频截取示例(Objective-C)

- (void)trimAudioFromURL:(NSURL *)inputURL startTime:(NSTimeInterval)startTime duration:(NSTimeInterval)duration completion:(void (^)(NSURL *outputURL, NSError *error))completion {
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:inputURL options:nil];
    
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetAppleM4A];
    
    NSString *outputPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"trimmedAudio.m4a"];
    exportSession.outputURL = [NSURL fileURLWithPath:outputPath];
    exportSession.outputFileType = AVFileTypeAppleM4A;
    
    CMTime startCMTime = CMTimeMakeWithSeconds(startTime, 600);
    CMTime durationCMTime = CMTimeMakeWithSeconds(duration, 600);
    CMTimeRange timeRange = CMTimeRangeMake(startCMTime, durationCMTime);
    exportSession.timeRange = timeRange;
    
    [exportSession exportAsynchronouslyWithCompletionHandler:^{
        if (exportSession.status == AVAssetExportSessionStatusCompleted) {
            NSLog(@"音频截取成功: %@", outputPath);
            if (completion) completion([NSURL fileURLWithPath:outputPath], nil);
        } else {
            NSError *error = exportSession.error;
            NSLog(@"音频截取失败: %@", error.localizedDescription);
            if (completion) completion(nil, error);
        }
    }];
}

✅ 使用方法

NSURL *audioURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"myAudio" ofType:@"mp3"]];
[self trimAudioFromURL:audioURL startTime:3.0 duration:5.0 completion:^(NSURL *outputURL, NSError *error) {
    if (outputURL) {
        NSLog(@"截取后的音频路径: %@", outputURL.path);
    }
}];

📌 注意事项

项目 说明
时间单位 使用 CMTimeMakeWithSeconds 将秒数转换为 CMTime
输出路径 使用 NSTemporaryDirectory() 可避免存储问题
输出格式 视频推荐 .mp4,音频推荐 .m4a.caf
导出性能 使用 AVAssetExportPresetLowQuality 可提升处理速度
错误处理 检查 exportSession.statusexportSession.error

🚀 扩展建议

  • 多片段拼接:可结合 AVMutableComposition 实现多段裁剪后的内容拼接。
  • 后台导出:大文件建议在后台线程执行,避免阻塞主线程。
  • 第三方库:如需更复杂剪辑功能,可使用 FFmpeg-iOSGPUImage

✅ 总结

通过 AVAssetExportSessiontimeRange 属性,你可以轻松地从音视频文件中截取任意时间段的内容。这个方法既适用于音频也适用于视频,具有良好的兼容性和性能表现,是 iOS 音视频处理中的基础技能之一。

iOS 音视频格式

在 iOS 开发中,音频和视频的格式选择直接影响性能、兼容性和用户体验。以下是常见的音频和视频格式,以及实际开发中常用的场景:


一、音频格式

1. 常见音频格式

格式 特点 使用场景
AAC(Advanced Audio Codec) 高压缩率、音质好,iOS 原生支持(如 AVAudioRecorder 默认输出格式)。 音频录制、流媒体(如 Apple Music)、视频配音。
MP3(MPEG-1 Audio Layer III) 兼容性极广,压缩率中等,但音质略逊于 AAC。 老旧项目兼容性需求(如播放本地 MP3 文件)。
WAV(Waveform Audio File Format) 无损格式,文件体积大,保留原始音质。 音频处理工具(如波形分析)、录音后处理。
PCM(Pulse Code Modulation) 未压缩的原始音频数据,常用于实时处理。 音频采集(如麦克风输入)、音频算法开发(如 FFT 分析)。
Opus 低延迟、高压缩率,适合实时通信(如 VoIP)。 实时语音通话(如 WebRTC 集成)。

2. 实际开发中的使用

  • 录制音频
    使用 AVAudioRecorder 录制音频时,默认使用 AAC 格式(kAudioFormatMPEG4AAC),并通过 AVAudioSettings 设置采样率(如 44.1kHz)、位深度(16-bit)等参数。

    let settings: [String: Any] = [
        AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
        AVSampleRateKey: 44100,
        AVNumberOfChannelsKey: 1,
        AVEncoderBitRateKey: 128000,
        AVLinearPCMIsBigEndianKey: false
    ]
    do {
        audioRecorder = try AVAudioRecorder(url: fileURL, settings: settings)
        audioRecorder.record()
    } catch {
        print("录音失败: $error)")
    }
    
  • 播放音频
    使用 AVAudioPlayer 播放本地或网络音频文件(支持 AAC、MP3、WAV 等格式)。

    do {
        audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
        audioPlayer.play()
    } catch {
        print("播放失败: $error)")
    }
    
  • 实时音频处理
    使用 AudioUnitAccelerate 框架处理 PCM 数据(如降噪、混响)。

    // 通过 AudioUnit 回调处理音频缓冲区
    func audioProcessingCallback(
        _ inRefCon: UnsafeMutableRawPointer,
        _ ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
        _ inTimestamp: UnsafePointer<AudioTimeStamp>,
        _ inBusNumber: UInt32,
        _ inNumberFrames: UInt32,
        _ ioData: UnsafeMutablePointer<AudioBufferList>
    ) -> OSStatus {
        // 处理 PCM 数据(如 FFT 变换)
        return noErr
    }
    

二、视频格式

1. 常见视频格式

格式 特点 使用场景
H.264(MPEG-4 AVC) 广泛兼容,压缩率高,iOS 原生支持(AVAssetExportSession 默认输出格式)。 视频录制、播放、流媒体(如 HLS)。
H.265(HEVC) 比 H.264 压缩率更高,但兼容性稍差(需 iOS 10+)。 4K/8K 视频存储(如相机 App)。
ProRes 无损压缩,高质量但体积大,适合专业编辑。 视频剪辑工具(如 Final Cut Pro 导出)。
MOV(QuickTime Movie) 容器格式,可封装 H.264/AAC 等数据,iOS 原生支持。 视频预览、本地存储。
MP4(MPEG-4 Part 14) 容器格式,兼容性极广,适合网络传输。 视频上传、跨平台播放。

2. 实际开发中的使用

  • 视频录制
    使用 AVCaptureSession 捕获视频流,并通过 AVAssetWriter 将 H.264 编码的视频写入 MP4 文件。

    let videoOutput = AVCaptureMovieFileOutput()
    captureSession.addOutput(videoOutput)
    let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("output.mp4")
    videoOutput.startRecording(to: fileURL, recordingDelegate: self)
    
  • 视频播放
    使用 AVPlayer 播放本地或网络视频(支持 H.264、H.265、MP4、MOV 等格式)。

    let player = AVPlayer(url: videoURL)
    let playerViewController = AVPlayerViewController()
    playerViewController.player = player
    present(playerViewController, animated: true) {
        player.play()
    }
    
  • 视频编辑
    使用 AVMutableComposition 合并多个视频片段,并通过 AVAssetExportSession 导出为 H.264 编码的 MP4 文件。

    let composition = AVMutableComposition()
    let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
    try? videoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: asset.tracks(withMediaType: .video)[0], at: .zero)
    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
    exporter.outputURL = outputURL
    exporter.outputFileType = .mp4
    exporter.exportAsynchronously {
        if exporter.status == .completed {
            print("视频导出成功")
        }
    }
    
  • 硬件编码
    使用 VideoToolbox 进行 H.264/H.265 的硬件编码(适用于高性能需求场景)。

    var compressionSession: VTCompressionSession?
    VTCompressionSessionCreate(
        allocator: kCFAllocatorDefault,
        width: width,
        height: height,
        codecType: kCMVideoCodecType_H264,
        encoderSpecification: nil,
        imageBufferAttributes: nil,
        compressedDataAllocator: nil,
        outputCallback: videoEncodeCallback,
        refcon: nil,
        compressionSessionOut: &compressionSession
    )
    

三、开发中的注意事项

  1. 兼容性

    • 使用 H.264 和 AAC 格式以确保最大兼容性(尤其是支持 iOS 9 及以下设备)。
    • 对于 HEVC(H.265),需检查设备系统版本(iOS 10+)和解码能力。
  2. 性能优化

    • 使用硬件编码(VideoToolbox/AudioToolbox)提升效率,减少 CPU 开销。
    • 对视频进行动态分辨率适配(如 1080p vs 720p)以平衡画质和流量。
  3. 容器格式选择

    • 本地存储优先使用 .mov(QuickTime 容器),网络传输优先使用 .mp4(兼容性更好)。
  4. 音视频同步

    • 在编辑或播放时,确保音频和视频的 PTS(显示时间戳)对齐,避免卡顿或音画不同步。

四、典型场景示例

场景 使用的格式 框架
实时视频通话 H.264 + AAC WebRTC/AVFoundation
视频剪辑 App H.264 + AAC(MP4 容器) AVFoundation
高清视频录制 H.265 + AAC(MOV 容器) AVCaptureSession + AVAssetWriter
语音备忘录 AAC(.m4a 容器) AVAudioRecorder
实时语音通信 Opus WebRTC

总结

在 iOS 开发中,H.264/AAC 是最常用的核心组合,兼顾兼容性和性能;MP4/MOV 是最常见的容器格式;Opus 在实时通信中表现优异。实际开发中,通过 AVFoundationVideoToolbox 等框架,开发者可以灵活处理这些格式,并结合硬件加速优化性能。

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 宏。

❌