普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月20日iOS

Kingfisher图像处理库

作者 山水域
2025年5月19日 22:12

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

作者 东坡肘子
2025年5月20日 07:59

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 开发体验。

昨天 — 2025年5月19日iOS

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

作者 Fatbobman
2025年5月19日 22:00

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

iOS 截取和分割音视频

作者 90后晨仔
2025年5月19日 19:00

在 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 音视频格式

作者 90后晨仔
2025年5月19日 18:58

在 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 - 宏替换声明体绑定宏

2025年5月19日 18:22

在 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 - 成员属性绑定

2025年5月19日 18:22

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 - 扩展绑定宏

2025年5月19日 18:21

在 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 - 访问器绑定宏

2025年5月19日 18:20

在 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 - 对等绑定宏

2025年5月19日 18:20

在 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 - 成员绑定宏

2025年5月19日 18:19

在 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 - 声明式独立宏

2025年5月19日 18:19

在 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 - 表达式独立宏

2025年5月19日 18:18

在 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 - 宏之协议

2025年5月19日 18:17

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

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

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

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

Swift 宏协议的共性特征

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

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

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

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

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

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

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

错误提示.png

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

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

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

3. context 宏的工具箱

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

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

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

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

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

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

3.2 诊断报告:diagnose(_:)

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

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

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

警告提醒.png

DiagnosticMessage

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

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

  • diagnosticID:诊断 ID

  • severity:诊断严重程度

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

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

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

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

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

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

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

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

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

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

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

3.4 词法作用域追踪:lexicalContext

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

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

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

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

4. node 调用现场信息

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

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

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

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

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

具体能做什么?

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

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

示例

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

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

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

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

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

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

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

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

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

6. 宏只运行于编译时

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

这为宏提供了如下保证:

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

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

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

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

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

这种强约束带来:

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

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

总结

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

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

Swift Macros - SwiftSyntax 节点指南

2025年5月19日 18:17

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

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

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

目录

1. 表达式(ExprSyntax)

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

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

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

2. 声明(DeclSyntax)

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

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

3. 语句(StmtSyntax)

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

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

4. 类型(TypeSyntax)

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

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

5. 模式(PatternSyntax)

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

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

6. 属性(AttributeSyntax)

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

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

7. 宏(MacroExpansionSyntax)

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

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

8. 其他常用节点

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

来源

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

swift-syntax源码

SwiftSyntax文档

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

Swift Macros - 宏之语法树

2025年5月19日 18:16

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

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

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

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

在 Swift 宏中:

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

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

2. Syntax 与 SwiftSyntax

2.1 什么是 Syntax?

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

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

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

2.2 常见语法节点类型举例

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

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

2.3 为什么宏操作的是 Syntax?

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

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

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

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

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

2.3 语法树结构示意

可以用一张简单图理解:

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

每个 Syntax 节点都有:

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

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

对应的 Syntax 树结构大致是:

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

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

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

2.5 SwiftSyntax 的协议体系

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

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

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

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

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

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

3.2 访问节点字段

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

3.3 遍历子节点

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

4. 如何构建语法节点?

4.1 使用字符串构造

 let expr: ExprSyntax = "1 + 2"

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

4.2 使用 SwiftSyntaxBuilder 构造复杂结构

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

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

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

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

 return "(raw: value)"

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

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

为什么推荐使用 (raw:)

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

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

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

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

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

宏声明

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

宏实现

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

使用示例

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

宏展开后:

 let total = 10

6. 小结

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

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

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

Swift Macros - 宏角色与命名控制

2025年5月19日 18:15

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

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

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

1. 宏类型

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

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

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

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

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

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

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

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

如何选择正确的宏角色?

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

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

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

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

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

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

命名说明符的种类

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

使用范围说明

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

使用时机与建议

使用时机:

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

使用建议:

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

命名说明符的示例

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

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

命名说明符示例.jpg

4. 最后

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

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

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

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

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

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

Swift Macros - 宏之全貌

2025年5月19日 18:07

1. 宏的定义

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

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

宏结构解析.png

为什么使用宏?

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

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

2. 宏的设计原则

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

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

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

3. 宏的原理

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

宏的展开流程

宏的扩展.png

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

宏的安全性与纯粹性

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

系统隔离:沙盒机制

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

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

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

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

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

设计哲学:纯粹性原则

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

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

推荐做法

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

不建议行为

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

命名说明符的种类与用途

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

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

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

5.1 宏的基础协议:Macro

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

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

5.2 宏的分类协议:FreestandingMacroAttachedMacro

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

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

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

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

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

5.3 宏的角色协议

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

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

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

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

5.4 主要宏角色协议

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

1. 表达式独立宏:ExpressionMacro

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

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

2. 声明式独立宏:DeclarationMacro

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

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

3. 对等绑定宏:PeerMacro

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

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

4. 访问器绑定宏:AccessorMacro

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

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

5. 成员属性修饰宏:MemberAttributeMacro

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

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

6. 成员绑定宏:MemberMacro

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

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

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

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

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

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

8. 扩展绑定宏:ExtensionMacro

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

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

9. 未公开使用的实验宏

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

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

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

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

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

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

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

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

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

6.2 协议是角色的具象化

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

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

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

6.3 建立宏的设计思维路径

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

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

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

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

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

→ 展开为:

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

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

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

→ 展开为:

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

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

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

→ 展开为:

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

Swift Macros - 宏之起点

2025年5月19日 18:03

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

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

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

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

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

宏展开说明.png

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

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

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

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

Swift 中的宏主要分为两类:

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

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

认识宏

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

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

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

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

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

自定义宏

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

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

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

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

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

制作宏

1. 创建工程模板

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

要求:Xcode 15+,Swift 5.9+

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

创建制作工程.png

工程模版说明

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

2. 定义宏的声明

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

3. 实现宏的逻辑

 import SwiftSyntax
 import SwiftSyntaxBuilder
 import SwiftCompilerPlugin
 import SwiftSyntaxMacros
 
 public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.arguments.first?.expression else {
            fatalError("compiler bug: the macro does not have any arguments")
        }
 
        return "((argument), (literal: argument.description))"
    }
 }

4. 注册宏的插件

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

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

使用宏

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

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

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

调试宏

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

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

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

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

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

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

这种方式能够:

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

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

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

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

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

最后

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

你可能会思考一些问题:

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

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

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

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

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

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

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

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

❌
❌