在平淡中等待 WWDC 2025 - 肘子的 Swift 周报 #84
不知不觉,距离 WWDC 2025 开始只有 20 天了。在过去的几年中,每当此时我都会写几篇文章畅想 WWDC 上会带来的新功能和我期待的一些变化。然而,或许是因为最近两年 WWDC 上展示的许多新功能并未完全落地,就我个人而言,今年似乎少了往日的热情和渴望。希望这只是我个人的情况。
不知不觉,距离 WWDC 2025 开始只有 20 天了。在过去的几年中,每当此时我都会写几篇文章畅想 WWDC 上会带来的新功能和我期待的一些变化。然而,或许是因为最近两年 WWDC 上展示的许多新功能并未完全落地,就我个人而言,今年似乎少了往日的热情和渴望。希望这只是我个人的情况。
在 iOS 开发中,截取或分割音视频是常见需求,适用于短视频剪辑、语音消息裁剪、媒体内容编辑等场景。使用 AVFoundation 框架可以高效实现这一功能。下面将详细介绍如何在 iOS 中截取或分割音视频,并提供完整的代码示例和使用方法。
截取或分割音视频的核心步骤如下:
AVURLAsset
)CMTimeRange
)指定要截取的起始时间与持续时间AVAssetExportSession
).mp4
、.m4a
等格式)- (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);
}
}];
- (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.status 和 exportSession.error
|
AVMutableComposition
实现多段裁剪后的内容拼接。通过 AVAssetExportSession
的 timeRange
属性,你可以轻松地从音视频文件中截取任意时间段的内容。这个方法既适用于音频也适用于视频,具有良好的兼容性和性能表现,是 iOS 音视频处理中的基础技能之一。
在 iOS 开发中,音频和视频的格式选择直接影响性能、兼容性和用户体验。以下是常见的音频和视频格式,以及实际开发中常用的场景:
格式 | 特点 | 使用场景 |
---|---|---|
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 集成)。 |
录制音频:
使用 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)")
}
实时音频处理:
使用 AudioUnit
或 Accelerate
框架处理 PCM 数据(如降噪、混响)。
// 通过 AudioUnit 回调处理音频缓冲区
func audioProcessingCallback(
_ inRefCon: UnsafeMutableRawPointer,
_ ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
_ inTimestamp: UnsafePointer<AudioTimeStamp>,
_ inBusNumber: UInt32,
_ inNumberFrames: UInt32,
_ ioData: UnsafeMutablePointer<AudioBufferList>
) -> OSStatus {
// 处理 PCM 数据(如 FFT 变换)
return noErr
}
格式 | 特点 | 使用场景 |
---|---|---|
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) | 容器格式,兼容性极广,适合网络传输。 | 视频上传、跨平台播放。 |
视频录制:
使用 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
)
兼容性:
性能优化:
VideoToolbox
/AudioToolbox
)提升效率,减少 CPU 开销。容器格式选择:
.mov
(QuickTime 容器),网络传输优先使用 .mp4
(兼容性更好)。音视频同步:
场景 | 使用的格式 | 框架 |
---|---|---|
实时视频通话 | 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 在实时通信中表现优异。实际开发中,通过 AVFoundation
和 VideoToolbox
等框架,开发者可以灵活处理这些格式,并结合硬件加速优化性能。
在 Swift 宏体系中,BodyMacro
是一种专门用于替换方法体实现的宏协议。通过 BodyMacro
,开发者可以为已有方法、构造器等提供新的实现代码,减少重复代码的书写,并将功能逻辑更加灵活地注入到已有的声明体中。它与其他宏类型(如 MemberMacro
或 AccessorMacro
)的区别在于,它并不生成新的方法声明或属性,而是专注于方法实现的替换。
本节建议结合《Swift Macros - 宏之全貌》和《Swift Macros - 宏之协议》一并阅读,以便更好地理解宏在声明体中的角色和具体应用。
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 |
提供宏展开时的上下文信息,可用于报错、追踪、生成唯一名称等用途 |
语法结构 | 是否支持 BodyMacro
|
说明 |
---|---|---|
func xxx() {} |
✅ 支持 | 替换函数体 |
init() {} |
✅ 支持 | 替换构造器体 |
deinit {} |
✅ 支持 | 替换析构器体 |
var xxx: Type {} |
✅ 支持 | 替换计算属性的 getter/setter 实现 |
subscript(...) {} |
✅ 支持 | 替换下标访问体 |
@attached(body)
的声明类型:语法结构 | 是否支持 | 原因 |
---|---|---|
struct , class
|
❌ 不支持 | 没有方法体可替换 |
存储属性(var a = 1 ) |
❌ 不支持 | 不是函数体结构,不能被 body 替换 |
enum case , typealias 等 |
❌ 不支持 | 没有可替换的声明体 |
of node: AttributeSyntax
node
表示宏的语法标记本身,它包含了宏调用的信息。例如,@AutoEquatable
中的 @AutoEquatable
会作为 node
传递给宏处理方法。在宏实现中,开发者可以检查这个节点,解析传递给宏的参数,进而控制宏的行为。
attachedTo declaration: some DeclGroupSyntax
declaration
是宏附加到的声明体。它代表了宏应用的上下文。例如,如果宏应用于一个方法或构造器,declaration
就会是该方法或构造器的语法节点。开发者可以从中获取类型名、方法签名等信息。
in context: some MacroExpansionContext
context
提供了宏展开的上下文信息,包括文件路径、源代码位置等。这对于诊断错误、生成唯一名称以及确保代码的正确性非常重要。
BodyMacro
的返回值BodyMacro
的返回值是一个数组,表示宏生成的 新的方法体 或 实现代码。这些方法体会替换原有方法的实现。
返回的代码会按照开发者的需求生成新的方法体,这些方法体将替代原始方法的内容,而不会影响方法签名。
@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]
}
}
BodyMacro
是 Swift 宏体系中非常重要的一类宏,它允许开发者替换现有方法的实现部分。通过 BodyMacro
,可以动态生成方法体,减少冗余代码,并提高代码的灵活性和可重用性。
AccessorMacro
、MemberMacro
等宏类型共同使用,构建更高层次的自动化功能。未来,开发者可以利用 BodyMacro
更加灵活地控制方法实现,为 Swift 项目注入强大的元编程能力。
Swift 宏系统中,MemberAttributeMacro
是一种用于为类型中的成员声明自动附加属性标记的宏。它适用于需要为多个成员统一附加如 @available
、@objc
、@discardableResult
等语义的场景。
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] ,即附加的属性标记 |
场景 | 示例宏 | 功能描述 |
---|---|---|
批量附加可用性标记 | @iOSOnly |
为所有成员添加 @available(iOS 13.0, *)
|
自动添加 @discardableResult
|
@AllowDiscard |
避免函数返回值未使用时警告 |
自动标记为 @objc
|
@ExposeToObjC |
支持 Objective-C 可见性 |
参数 | 用途 |
---|---|
of node: AttributeSyntax |
宏本身语法节点 |
attachedTo declaration |
宏所附加的类型体(struct/class/enum) |
providingAttributesFor member |
被作用的每一个成员(方法、属性等) |
in context: MacroExpansionContext |
用于生成诊断、唯一名等辅助功能 |
你可以在 member
中判断成员类型、名称,并进行有选择性地附加属性。
@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
, 我们可以通过 成员属性绑定宏 给属性都添加上。
@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")]
}
}
例如,我们可以只为方法名以 "old"
开头的函数添加 @available
:
if let funcDecl = member.as(FunctionDeclSyntax.self),
funcDecl.identifier.text.hasPrefix("old") {
return [try AttributeSyntax("@available(iOS 13.0, *)")]
}
return []
限制 | 说明 |
---|---|
只能附加属性 | 不能添加新方法或修改函数体 |
不影响嵌套类型 | 仅作用于第一层成员 |
与手动属性并存 | 可以手动添加属性,宏添加不会冲突 |
MemberAttributeMacro
是一种细粒度的声明增强工具,非常适合用于:
它的设计理念是“轻量级修饰”,通过规则生成统一的标记代码,是一种常见的声明式元编程方式。
在 Swift 宏系统中,ExtensionMacro
是一种用于自动生成扩展(extension
)代码块的宏协议,适用于为类型生成协议实现、工具方法、便捷功能等 “类型之外”的附加内容。它是 Swift 中唯一专门用于生成类型扩展的宏角色。
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]
,即多个扩展声明语法。应用场景 | 示例 | 说明 |
---|---|---|
自动协议实现 | @AutoEquatable |
在扩展中实现 Equatable 协议方法 |
添加工具方法 | @Stringifyable |
为类型扩展一个 stringify() 方法 |
组合属性行为 | @Bindable |
在扩展中添加辅助函数支持绑定逻辑 |
动态特性注入 | @Observable |
在扩展中生成 Publisher 等观察能力 |
of node: AttributeSyntax
代表宏标记语法本身,例如 @AutoEquatable
,可用于分析传入参数、控制行为。
表示宏绑定的原始类型声明体,例如:
@AutoEquatable
struct User {
var name: String
}
此处 declaration
就是整个 struct User { ... }
的结构。
providingExtensionsOf type: some TypeSyntaxProtocol
即绑定的类型名(如 User
),可以用于组装扩展语法,例如:
extension (type.trimmedDescription): Equatable { ... }
in context: some MacroExpansionContext
上下文信息,包括定位宏展开位置、生成唯一 ID、发出诊断信息等。
[ExtensionDeclSyntax]
返回的是多个完整的 extension
语法块:
extension User: Equatable {
static func == (lhs: User, rhs: User) -> Bool {
lhs.name == rhs.name
}
}
宏系统将这些扩展插入到类型作用域之外。
@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]
}
}
ExtensionMacro
;static
方法、协议实现,优先考虑 ExtensionMacro
而非 MemberMacro
;@attached(member)
搞混,两者生成的位置与作用域不同。ExtensionMacro
是一种强大的宏类型,它让你能够安全、清晰地将协议实现或工具逻辑注入到类型之外,而不干扰类型本身的结构声明。
适合用于:
它是宏系统中实现“非侵入式增强”的关键角色。
在 Swift 宏体系中,AccessorMacro
是一种专用于自动生成属性访问器(如 getter、setter、willSet、didSet 等) 的宏协议。它适用于那些希望对属性访问行为进行自定义、跟踪或扩展的场景,在构建声明式属性模型和状态观察系统中极具价值。
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)
类型的宏;[AccessorDeclSyntax]
,即访问器数组;MemberMacro
不同,它不生成新成员,只生成该属性的访问逻辑。应用场景 | 示例 | 说明 |
---|---|---|
自动打印追踪 | @Observe |
自动打印属性变化前后的值 |
自动脏标记更新 | @DirtyTrack |
属性变更时自动设置脏标志 |
数据合法性校验 | @Validate |
在 setter 中自动进行值的合法性校验 |
双向绑定触发器 | @Bindable |
在 set 时触发 UI 更新或事件回调 |
只要你希望控制属性访问行为(特别是赋值过程) ,AccessorMacro
就是首选工具。
of node: AttributeSyntax
代表宏标记语法本身,例如 @Observe
,可用于参数识别与行为控制。
attachedTo declaration: some DeclSyntaxProtocol
表示宏所附着的原始属性声明,一般是 VariableDeclSyntax
。
providingAccessorsOf storedProperty: some DeclSyntaxProtocol
同样表示所操作的属性本身,与 attachedTo
通常相同,但语义更明确:你要为它提供访问器。
in context: some MacroExpansionContext
上下文信息:用于生成唯一标识符、定位源文件位置或报告错误。
[AccessorDeclSyntax]
返回值是访问器声明数组,可以包含任意组合,如:
[ AccessorDeclSyntax("get { _value }"), AccessorDeclSyntax("set { print("New value: \(newValue)"); _value = newValue }") ]
这些访问器将完全替换原始属性的访问行为。
我们定义一个宏 @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]
}
}
PeerMacro
配合使用通常 AccessorMacro
与 PeerMacro
是组合使用的:
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
}
}
var
属性上;willSet
和 didSet
与 get/set
同时存在的混合访问器(Swift 语法限制);PeerMacro
生成);@propertyWrapper
不同,它不会引入额外类型或语义负担。AccessorMacro
是 Swift 宏系统中控制“属性行为”的关键工具。它通过访问器代码生成机制,将属性语义与行为解耦,适用于:
结合 MemberMacro
、PeerMacro
,你可以构建出完整的声明式状态模型系统,实现真正的结构驱动式编程体验。
在 Swift 宏体系中,PeerMacro
是一种非常灵活且强大的宏协议,专用于生成与绑定声明处于同一作用域的“对等”声明,常用于自动扩展同级的变量、函数或类型定义。
本节将深入介绍 PeerMacro
的用途、定义、参数结构以及实际示例,帮助你理解它在元编程场景中的独特价值。
建议结合《Swift Macros - 宏之全貌》和《Swift Macros - 宏之协议》一并阅读,便于全面理解宏系统的角色协作模型。
PeerMacro
的定义标准库中 PeerMacro
的定义如下:
public protocol PeerMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
这意味着:
[DeclSyntax]
,即可以注入多个顶层/局部声明;PeerMacro
的典型用途Peer 宏的应用场景非常广泛,常用于:
场景 | 示例 | 说明 |
---|---|---|
自动生成伴生变量 | @WithWrapper |
为属性生成 _xxx 存储变量 |
自动生成伴生函数 | @BindAction |
为属性自动生成相关行为函数 |
生成衍生声明 | @AutoObservable |
为属性自动生成观察者包装及通知机制 |
声明反射信息 | @Reflectable |
自动生成结构体元信息注册代码 |
特别适合那些需要基于现有声明生成“相关声明”的情境,但不适合直接插入原声明体内的场合。
of node: AttributeSyntax
代表宏的语法标记本身,例如 @WithWrapper
。可用于:
attachedTo declaration: some DeclSyntaxProtocol
DeclSyntaxProtocol
,表示可以是变量、函数、类型等;in context: some MacroExpansionContext
上下文对象,常用于:
Peer 宏生成的声明会插入到与原声明相同的作用域中,而不是类型或函数内部。
例如:
@WithWrapper
var name: String
展开后等同于:
var name: String
private var _name: String = ""
即:_name
是 name
的“对等声明”,它们在同一语法级别上。
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))"
}
"""
]
}
}
PeerMacro
会生成多个完整的顶层声明节点,开发者需手动控制命名与作用域;names:
标注宏声明;PeerMacro
是 Swift 宏系统中“横向扩展”的核心工具,它允许开发者在不修改原始声明的前提下添加紧密关联的辅助声明。适用于:
当你需要构建“围绕声明的附属结构”,PeerMacro
就是你的利器。
在 Swift 中,结构体和类的声明体(即 {}
中的内容)常常会包含许多重复或模式化的成员声明。为了提升开发效率并避免重复劳动,Swift 宏系统提供了一种用于自动生成成员声明的宏协议:MemberMacro
。在 Swift 宏体系中,MemberMacro
是一种具有极高实用价值的宏协议,它专门用于在类型声明内部生成新的成员(如属性、方法、构造器等)。这种宏是典型的附加宏(attached macro) ,能够大幅减少重复成员定义的样板代码,提高类型声明的表达能力。
本节建议结合《Swift Macros - 宏之全貌》和《Swift Macros - 宏之协议》一并阅读,以便更好地理解宏在声明结构中的角色。
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 |
宏所附加的类型声明体,例如 struct 或 class
|
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) }
💡 注意:返回的成员会插入到原始类型声明体中,因此要避免命名冲突。
📌 使用限制
- 只可用于具有声明体(
{}
)的类型定义:struct
、class
、enum
、actor
- 不可用于
func
、var
、extension
等其他声明- 若注入的成员包含具名声明(如
var id
),必须在宏声明中通过names:
显式声明,以避免命名未覆盖错误(Declaration name 'id' is not covered by macro
)
MemberMacro
适用于所有需要自动生成类型成员的场景,特别是:
场景 | 示例 | 说明 |
---|---|---|
自动生成协议实现 | @AutoEquatable |
自动实现 Equatable 的 == 方法 |
自动添加辅助属性 | @Observe |
为属性生成 _xxx 存储与监控 getter |
自动实现构造器 | @AutoInit |
基于属性自动生成初始化函数 |
自动生成默认值 | @WithDefaults |
为成员属性自动附加默认实现 |
@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:
明确标注。
对于继承自某个父类的子类,我们希望自动生成 CodingKeys
与 init(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
}
}
}
MemberMacro
是 Swift 宏体系中连接语法结构与声明注入的关键机制。它让开发者能够根据类型结构自动生成成员,真正实现:
未来你可以将它与 AccessorMacro
、PeerMacro
等组合使用,构建更高层次的声明式元编程能力。
在 Swift 宏体系中,DeclarationMacro
是一种用途广泛的角色,专门用于生成声明级别的代码,如变量、函数、结构体等。它同样属于自由悬挂宏(freestanding macro)的一种,但与 ExpressionMacro
不同,它不会展开为表达式,而是生成一个或多个 完整的声明语法节点(DeclSyntax) 。
本节将深入讲解 DeclarationMacro
的定义、用途、特点,以及其参数、返回值的结构分析,并通过示例帮助你掌握其使用方式。
建议先阅读基础篇《Swift Macros - 宏之全貌》与协议篇《Swift Macros - 宏之协议》,以更好地理解本节内容。
DeclarationMacro
的定义DeclarationMacro
协议由标准库提供,其定义如下:
public protocol DeclarationMacro: FreestandingMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
简而言之,声明式独立宏具备以下特性:
[DeclSyntax]
数组,支持生成多个声明。DeclarationMacro
的作用分析场景 | 示例 | 说明 |
---|---|---|
自动生成函数 | #makeDebugFunction("log") |
生成具名的调试函数 |
统一封装声明 | #injectCommonImports() |
插入一批通用 import 语句 |
构建配置项常量集 | #defineKeys("id", "name") |
根据传入字符串列表定义常量 |
静态信息注入 | #generateBuildInfo() |
生成包含版本、时间、构建号的静态变量 |
DeclarationMacro
的参数解析与 ExpressionMacro
一样,DeclarationMacro
的 expansion
函数也接受以下两个参数:
of node: some FreestandingMacroExpansionSyntax
.argumentList
访问用户传入的参数列表;LabeledExprSyntax
类型,可以进一步分析是否为字面量、表达式等。in context: some MacroExpansionContext
ExpressionMacro
中的 context
功能完全一致。DeclarationMacro
的返回值[DeclSyntax]
VariableDeclSyntax
、FunctionDeclSyntax
、StructDeclSyntax
等); return [
DeclSyntax("let name = "Mccc""),
DeclSyntax("let age = 30")
]
调用:
#defineProfile()
展开:
let name = "Mccc"
let age = 30
DeclarationMacro
示例解析定义一个宏 #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"
宏定义:
@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 宏体系中,ExpressionMacro
是一种非常重要且常用的角色。它专门用于生成表达式级别的代码,并且属于独立宏(freestanding macro) 的一种。
本节将深入讲解 ExpressionMacro
的定义、用途、特点,以及其参数、返回值的详细分析,帮助你全面掌握这一类型宏的设计与使用。
在阅读本节前,建议先了解基础篇《Swift Macros - 宏之全貌》和协议篇《Swift Macros - 宏之协议》,可以更流畅地理解本节内容。
ExpressionMacro
的定义Swift 标准库中,ExpressionMacro
协议的定义如下:
public protocol ExpressionMacro: FreestandingMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax
}
简而言之,表达式独立宏就是:
ExprSyntax
节点;注意:
ExpressionMacro
必须是 freestanding 的,意味着它本身不附加到其他声明上,而是以独立表达式的形式展开。
ExpressionMacro
的作用分析ExprSyntax
)场景 | 示例 | 说明 |
---|---|---|
自动封装日志 | #log("message") |
自动插入打印或记录代码 |
调试辅助工具 | #dump(expr) |
在调试时自动格式化输出 |
表达式改写 | #optimize(expr) |
将通用表达式展开成更高效的版本 |
自动计时 | #measure { work() } |
计算某段代码的执行时间 |
可以看出,凡是需要在编译期生成"一个表达式"的场景,都可以使用 ExpressionMacro
实现。
ExpressionMacro
的参数分析of node: some FreestandingMacroExpansionSyntax
node
包含了宏的名字、参数列表、调用位置等信息。node
,可以获取用户传递给宏的具体内容。小提示:常用
node.argumentList
来解析参数。
例如,对于调用:
#stringify(a + b)
则 node
会表示整个 #stringify(a + b)
,你可以从中取出 a + b
作为参数。
in context: some MacroExpansionContext
提供宏展开时的上下文信息。
可以用于:
context
是你在编写宏时的"万能工具箱",尤其在需要辅助信息(如生成辅助变量名、给出友好错误提示)时特别重要。
ExpressionMacro
的返回值分析ExprSyntax
举个简单例子,假设你写了一个 @ExpressionMacro
宏 #double(x)
,展开后返回的是:
ExprSyntax("((x) * 2)")
那么用户代码:
let value = #double(21)
最终编译器看到的是:
let value = (21 * 2)
注意:表达式宏必须返回单个表达式,不能直接返回语句、声明或其他结构。
ExpressionMacro
示例解析 @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)
定义一个宏 #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 宏的强大源于其背后一套精巧严谨的协议体系。这些协议定义了:
在 Swift 中, “宏 = 协议方法的实现” 。宏不会在运行时参与逻辑,而是在编译期间将协议方法转换为结构化代码。
本篇将深入解析这些协议的共性特征与调用方式,为你在后续实现各种角色宏打下统一的基础。
Swift 宏虽然分工明确(表达式宏、声明宏、成员宏等),但它们的实现方式高度统一,主要体现为以下特征:
编号 | 特征 | 描述 |
---|---|---|
1 | 方法统一命名为 expansion
|
所有宏协议都实现 static func expansion(...) 作为展开主入口。 |
2 | 支持 throws 异常机制 |
展开过程中可中止并抛出诊断错误。 |
3 | 必带 context 参数 |
提供编译期上下文信息,是宏的“工具箱”。 |
4 | 必带 node 参数 |
表示宏的调用现场,如 #宏名(...) 或 @宏名 。 |
5 | 输入输出皆为 Syntax 类型 |
宏只操作语法树,输入输出都是 SwiftSyntax 节点。 |
6 | 仅在编译期执行 | 宏不能访问运行时信息,所有逻辑基于静态源码。 |
7 | 返回类型严格固定 | 每种宏角色返回类型不同,且不可交叉使用。 |
static func expansion(...)
Swift 宏协议统一使用 expansion
方法命名,使得不同类型的宏拥有相似的签名与调用习惯,极大降低学习与维护成本。
// 各协议方法签名示例
protocol ExpressionMacro {
static func expansion(...) throws -> ExprSyntax
}
protocol DeclarationMacro {
static func expansion(...) throws -> [DeclSyntax]
}
static
,因为宏不依赖实例node
+ 编译上下文 context
ExprSyntax
、DeclSyntax
等throws
,可中止并报告错误所有宏的 expansion
方法都支持 throws
,允许在发现语义错误时立即中止,并通过 context.diagnose(...)
抛出诊断信息,提升宏的可维护性与用户友好度。
只需要在适当的地方抛出异常,你可以自行编辑异常的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 也会高亮定位到宏调用位置,提升调试体验。
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
协议四个核心成员的作用详解,按重要性分层说明:
自动生成唯一标识符,避免命名冲突
// 使用场景:临时变量、缓存值、内部标识符等场景。
let uniqueVar = context.makeUniqueName("result")
// 输出结果可能是 `result_7FE3A1` 之类的唯一名称
diagnose(_:)
核心作用:编译时错误报告系统
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(...)
给于警告提醒。
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 // 提示信息,常用于补充说明。 }
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
→ 完整系统路径(调试用)lexicalContext
核心作用:获取词法作用域上下文
以数组形式,记录从当前节点向外的层层包裹结构;
经过脱敏处理(如移除函数体、清空成员列表)。
// 检查是否在类方法中
let isInClassMethod = context.lexicalContext.contains {
$0.is(FunctionDeclSyntax.self) &&
$0.parent?.is(ClassDeclSyntax.self) != nil
}
node
调用现场信息每个宏的 expansion
方法,除了 context
外,还会接收一个 node
参数,类型通常是 some SyntaxProtocol
(如 FreestandingMacroExpansionSyntax
、AttributeSyntax
等)。
它代表了宏的调用现场——也就是源码中触发宏展开的那段语法结构。
简单理解:
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
= 辅助功能工具箱。
两者结合使用,才能让宏既能理解调用现场,又能灵活地生成对应代码。
Syntax
节点Swift 宏以结构化 AST(抽象语法树)为基础,输入输出都基于 SwiftSyntax
类型,例如:
AttributeSyntax
、FreestandingMacroExpansionSyntax
、DeclSyntaxProtocol
;ExprSyntax
、[DeclSyntax]
、[AccessorDeclSyntax]
等。这种设计保证了宏生成的代码具备:
Swift 宏不是简单拼接字符串,而是真正生成 AST。
Swift 宏只能在编译期运行,这意味着它们不能访问运行时信息、全局变量、实例状态或外部服务。所有宏的行为都必须建立在静态源代码、类型系统和语法结构之上。
这为宏提供了如下保证:
开发者在编写宏时,也应遵循“编译时思维”,尽可能将逻辑转化为静态分析与结构转换。
每个宏协议都明确限定了其 expansion
方法的返回类型,这种限制具有强约束力:
宏协议 | 返回类型 |
---|---|
ExpressionMacro |
ExprSyntax |
DeclarationMacro |
[DeclSyntax] |
MemberMacro |
[DeclSyntax] |
AccessorMacro |
[AccessorDeclSyntax] |
BodyMacro |
[CodeBlockItemSyntax] |
ExtensionMacro |
[ExtensionDeclSyntax] |
MemberAttributeMacro |
[AttributeSyntax] |
这种强约束带来:
比如:成员宏只能生成成员声明,不能直接生成表达式或代码块。
Swift 宏协议的结构化设计,使得宏具备了安全、清晰、灵活的特性。无论你编写哪种类型的宏,理解 expansion
的统一调用模式、context
工具箱能力、node
的语法抽象、以及 Syntax
类型的输入输出机制,都是构建可靠宏逻辑的基础。
在接下来的章节中,我们将深入每一种宏协议(如 ExpressionMacro
、DeclarationMacro
等),并结合实际案例,帮助你实现更多有趣且实用的 Swift 宏。
版本:2025.04.27|维护者:Mccc|欢迎共同维护与补充!
在编写 Swift 宏时,你将频繁与 SwiftSyntax
打交道。SwiftSyntax 将源码拆解为结构化的语法节点(Syntax)树,这些节点覆盖了表达式、声明、语句、类型、模式、属性等各个层面。
本篇文章提供一个实用速查表,帮助你快速了解各类常见语法节点的用途与构造方法,便于高效构建宏所需的代码结构。
用于表示各种计算表达式、函数调用、字面量等,是最常见的语法结构之一。
名称 | 描述 | 快速构造示例 |
---|---|---|
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。
表示变量、函数、类型、协议等的定义,是构建宏时生成结构代码的核心组成。
名称 | 描述 | 快速构造示例 |
---|---|---|
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: "+") |
用于构建控制流程语句(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: ...) |
用于表示类型声明,包括简单类型、数组、可选、元组、函数类型等。
名称 | 描述 | 快速构造示例 |
---|---|---|
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: ...) |
用于 let/var
绑定、模式匹配等结构。
名称 | 描述 | 快速构造示例 |
---|---|---|
IdentifierPatternSyntax | 标识符模式 | IdentifierPatternSyntax(identifier: .identifier("name")) |
TuplePatternSyntax | 元组模式 | TuplePatternSyntax(elements: [...]) |
WildcardPatternSyntax | 通配符 _
|
WildcardPatternSyntax() |
ValueBindingPatternSyntax | let/var 模式 | ValueBindingPatternSyntax(bindingSpecifier: "let", pattern: ...) |
ExpressionPatternSyntax | 表达式匹配 | ExpressionPatternSyntax(expression: ...) |
用于修饰声明,包括标准属性和自定义属性包装器。
名称 | 描述 | 快速构造示例 |
---|---|---|
AttributeSyntax | 标准属性 | AttributeSyntax(attributeName: "available") |
CustomAttributeSyntax | 自定义属性 | CustomAttributeSyntax(attributeName: "MyWrapper") |
专门用于表示宏的使用与展开。
名称 | 描述 | 快速构造示例 |
---|---|---|
FreestandingMacroExpansionSyntax | 表达式独立宏 #stringify(x)
|
FreestandingMacroExpansionSyntax(macroName: "stringify", arguments: [...]) |
AttributeMacroExpansionSyntax | 属性宏 @MyMacro
|
AttributeMacroExpansionSyntax(macroName: "MyMacro", arguments: [...]) |
AccessorMacroExpansionSyntax | Accessor 宏(getter/setter) | AccessorMacroExpansionSyntax(macroName: "MyAccessor") |
名称 | 描述 | 快速构造示例 |
---|---|---|
CodeBlockSyntax | 一组语句块 { ... }
|
CodeBlockSyntax(statements: [...]) |
MemberDeclListSyntax | 成员声明列表 | MemberDeclListSyntax(members: [...]) |
ParameterClauseSyntax | 参数签名 (x: Int)
|
ParameterClauseSyntax(parameters: [...]) |
TupleExprElementListSyntax | 元组表达式元素列表 | TupleExprElementListSyntax(elements: [...]) |
TokenSyntax | 基础 Token,如标识符/关键字等 |
.identifier("foo") , .keyword(.func)
|
SourceFileSyntax | 整个 Swift 源文件语法结构 | SourceFileSyntax(statements: [...]) |
为了确保内容的准确性和时效性,欢迎您定期参考官方文档和资源:
如有更新,提交MR,一起维护它。
在正式深入宏的世界之前,我们必须理解一个核心概念:Syntax(语法节点) 。它不仅是 Swift 宏生成和操作代码的“原材料”,更是编译器理解代码结构的基础。
语法树(Syntax Tree) 是代码生成与转换的基础数据结构。理解语法树的结构和操作方式是掌握宏开发的关键第一步。
本篇文章旨在帮助你掌握 SwiftSyntax 提供的语法节点体系、如何从语法树中提取信息、如何构建语法树,以及这些能力在宏中的实战应用,为你后续理解宏协议与宏实现打下扎实的基础。
在 Swift 宏中:
简言之:不了解语法树,就无法理解宏的工作方式。
Syntax
是对 Swift 源代码的结构化表示。Swift 编译器在编译时,将源代码依次转换为:
🟡 源代码 → 🟢 词法分析 → 🔵 语法分析 → 🟣 语法树 → 🔴 宏处理 → 🟤 编译
每一行 Swift 代码,都会被解析为一棵树状结构,树上的每个节点都是一个语法片段,称为 Syntax 节点(Syntax Node) 。
节点类别 | 示例类型 | 对应代码示例 |
---|---|---|
表达式节点 | InfixOperatorExprSyntax |
a + b |
声明节点 | VariableDeclSyntax |
let x = 1 |
语句节点 | ReturnStmtSyntax |
return result |
类型节点 | TypeAnnotationSyntax |
: Int |
每种节点类型都有明确的结构定义,可通过 SwiftSyntax 操作。
在 Swift 宏中,你不是直接操作字符串文本,也不是直接修改源代码,而是:
🟡 读取 Syntax → 🟢 生成新的 Syntax → 🔵 交给编译器继续处理
直接操作结构化的语法树,能带来:
优势 | 说明 |
---|---|
安全性高 | 生成的语法结构不会导致非法代码 |
可读性强 | 结构清晰,易于调试和理解 |
自动格式化 | 编译器可自动对齐风格,无需手动调整 |
易于优化 | 编译器直接理解语法结构,可执行更智能的优化 |
所以,你可以把 Swift 宏想象成是在编辑一棵代码树(Syntax Tree) ,而你的任务,就是在这棵树上插入、修改、替换节点。
可以用一张简单图理解:
源代码
└──> 词法分析(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 的子节点) |
允许递归处理复杂表达式 |
类型安全 | 每个节点类型明确(如区分 DeclReference 和 BinaryOperator ) |
编译时验证生成代码合法性 |
可组合性 | 独立节点通过父子关系组合(如操作符左右操作数) | 支持模块化代码生成 |
精准定位 | 每个节点包含位置信息(leading/trailing trivia) | 实现精确的错误诊断 |
SwiftSyntax 中的节点都遵循一套协议:
协议名 | 描述 |
---|---|
SyntaxProtocol |
所有节点的基类协议 |
DeclSyntaxProtocol |
声明类节点 |
ExprSyntaxProtocol |
表达式类节点 |
TypeSyntaxProtocol |
类型相关节点 |
StmtSyntaxProtocol |
语句节点 |
这些协议能帮助你在代码中进行统一操作与类型匹配。
.as(...)
类型转换 if let call = expr.as(FunctionCallExprSyntax.self) {
let functionName = call.calledExpression.description
}
let structDecl = decl.as(StructDeclSyntax.self)
let name = structDecl?.identifier.text
let members = structDecl?.memberBlock.members
for child in node.children(viewMode: .all) {
print(child.syntaxNodeType)
}
let expr: ExprSyntax = "1 + 2"
这是最常用且便捷的构造方式,适合简单的宏输出场景。
let one = ExprSyntax("1")
let two = ExprSyntax("2")
let plus = TokenSyntax.binaryOperator("+")
let expr = InfixOperatorExprSyntax(
leftOperand: one,
operatorOperand: plus,
rightOperand: two
)
适用于需要控制每个组成部分、生成复杂结构的宏实现。
(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"
这种非预期结果)#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
在 Swift 宏系统中,你要掌握的不是字符串拼接技巧,而是:
(raw:)
保证结构合法)语法树是宏系统的“语言”,也是宏生成代码的唯一通道。
在 Swift 宏系统中,宏类型(Macro Kind) 、宏角色(Macro Role) 与命名说明符(Name Specifier) 共同决定了宏的使用范围和生成内容的可控性。
理解这三者,是编写稳定、可维护、具协作性的宏的前提。
表示宏的附着方式,它分为两类:
@freestanding(...)
标记,独立于任何已有声明,适合生成表达式或新的声明语句。@attached(...)
标记,附着在已有声明(如类型、函数、属性)上,用于扩展或修改它们的结构。类型 | 展开位置 | 可生成内容 | 示例用途 |
---|---|---|---|
独立宏 | 代码中的任意表达式或声明位置 | 表达式、声明语句(变量、类型、函数等) | 生成常量、表达式等 |
绑定宏` | 类型、函数、属性、扩展等的前后或内部位置 | 成员、访问器、扩展、函数体等 | 注入属性、协议实现等 |
📌 独立宏使用
#宏名(...)
语法,绑定宏使用@宏名
修饰已有声明。
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) |
绑定宏生成代码时,常会生成具名实体:函数、变量、类型、扩展等。默认情况下,这些名称不明确,导致:
因此 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)
不支持,因为它不生成命名实体。
使用时机:
使用建议:
_
或命名空间隐藏内部实现,避免命名污染;arbitrary
,仅限工具生成结构; @attached(peer, names: arbitrary)
public macro DebugEqual() = #externalMacro(module: "McccMacros", type: "DebugEqualMacro")
Swift 宏并非简单模板,而是一套基于类型系统和结构规则的元编程能力。
三者合力,使宏在复杂多模块环境中保持结构清晰、命名隔离与语义明确。
理解宏的职责控制与命名控制,是构建健壮、可维护宏功能的基石。
后续文章将详细拆解各宏角色的使用方式、限制与最佳实践,配合命名说明符示例,帮助你构建结构良好、可组合的宏能力体系。
📌 想打造真实可维护的宏框架?必须先理解角色,控制命名。
Swift 宏(Macro) 是一种在编译期执行的代码生成机制。开发者可以通过简洁的语法标记,在源代码中自动插入、替换或补充逻辑,从而实现样板代码的自动化。
Swift 宏建立在语法树与类型系统之上,具备类型安全、语义明确与可预测的元编程特性。
为什么使用宏?
Swift 宏的优势体现在以下几个方面:
Equatable
、Codable
、监听器等重复性逻辑,宏可以自动生成这些代码。Swift 宏的设计秉承“显式、安全、可预测”三大原则,避免“魔法式”的隐式行为:
原则 | 说明 |
---|---|
显式调用 | 宏必须通过明确语法标记使用,开发者清晰可见。 |
类型检查 | 宏生成的代码会经过完整的类型系统验证,不会绕过语言规则。 |
可预测展开 | 宏的展开逻辑必须是稳定的、可预期的,结果不会因外部环境而改变。 |
宏不是魔法,它并不神秘,也不凌驾于语言规则之上。你写下的每一个宏调用,都将以可读、可测、可调试的方式插入源代码中。
Swift 宏基于编译器插件(Compiler Plug-in) 机制运行,整个过程发生在编译期,并受到严格的沙盒限制。
为了确保宏系统的 安全、稳定与可控性,Swift 从两个维度对宏行为做出约束:
所有宏插件运行在独立的沙盒进程中,Swift 对其能力进行了严格限制:
FileManager
)这些限制是编译器层面的强制规定,一旦访问受限资源,会立即报错,例如:
"The file “xxx” couldn’t be opened because you don’t have permission to view it."
因此,即使使用第三方宏插件,也无需担心其在背后执行未授权的操作。
Swift 鼓励将宏视为纯函数 —— 相同输入始终生成相同输出。这有助于:
推荐做法
不建议行为
UUID()
或 Date()
等生成动态值这些行为虽然 技术上允许,但会破坏宏的一致性,导致难以调试、不可复现的构建结果。
Swift 宏并非千篇一律,它具备明确的职责划分,这种职责由编译器通过一套称为 宏角色(Macro Role) 的机制识别和执行。
宏角色决定了一个宏可以做什么。Swift 中的宏主要分为两类:
@freestanding(...)
标记,独立于任何已有声明,适合生成表达式或新的声明语句。@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 宏构建起了清晰的职责体系 —— 每个宏角色都对应一类语法结构的生成或修改行为。
对于会生成 具名实体(如属性、函数、类型) 的宏,Swift 提供了另一套机制来进一步控制“生成出来的东西叫什么”,这就是 命名说明符(Name Specifier) 。
在绑定宏(例如 MemberMacro
、AccessorMacro
)中,我们通常使用 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 等 |
Swift 宏的功能是建立在一套明确分层的协议体系上的。这些协议定义了宏的 基本行为、适用场景,以及 如何响应编译器的宏展开请求。
Macro
所有 Swift 宏都遵循 Macro
协议,它是宏体系的根基,定义了宏的基本能力和默认行为。
public protocol Macro {
/// 控制宏展开后的代码是否格式化,默认为 `.auto`
static var formatMode: FormatMode { get }
}
.auto
(默认):使用格式化后的展开代码,推荐使用,能保持代码一致性。.disabled
:展开后的代码将原样插入,不进行格式化,适用于自定义输出。FreestandingMacro
与 AttachedMacro
在 Macro
协议的基础上,Swift 将宏分为两类:
FreestandingMacro
:用于 独立使用的宏,可以直接插入到表达式、声明等任何地方,适合用来生成简单的表达式。 public protocol FreestandingMacro: Macro { }
AttachedMacro
:用于 附着在已有代码上的宏,必须绑定到已有的类型、属性、函数等声明上,适合对已有代码进行扩展。 public protocol AttachedMacro: Macro { }
这两个协议本身不定义任何具体行为,它们为更细分的角色协议提供了基础。
💡 Swift 使用协议体系来设计宏的目的是:
- 层次清晰:基础协议定义宏的公共行为,高层协议划分宏的使用场景,角色协议定义宏的具体能力。
- 编译器驱动:根据宏的角色和位置,编译器调用特定协议中的
expansion(...)
方法展开宏。- 类型安全:协议方法的定义明确,展开时处理的语法结构与上下文类型都有严格的检查。
每个宏的角色都需要实现一个静态方法 expansion(of:in:)
,这是编译器在宏展开时调用的核心方法。该方法将接收当前语法节点和上下文信息,并返回生成的语法树,最终插入到用户代码中。
💡 一个宏的实现可以遵循多个协议,从而具备多重角色能力。 例如,以下
AutoCodableMacro
同时实现了MemberMacro
和AccessorMacro
,因此它具备生成成员和访问器的能力:
public struct AutoCodableMacro: MemberMacro, AccessorMacro {
public static func expansion(...) -> [DeclSyntax] { ... }
public static func expansion(...) -> [AccessorDeclSyntax] { ... }
}
这正是 Swift 宏系统的强大之处 —— 通过协议组合实现宏的 多重角色。
接下来,我们将一一解析不同的角色协议,详细说明每个协议的职责、调用时机及适用场景。
ExpressionMacro
public protocol ExpressionMacro: FreestandingMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax
}
功能:插入和替换表达式。适用于动态计算、生成常量、包装表达式等。
DeclarationMacro
public protocol DeclarationMacro: FreestandingMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
功能:用于插入新的声明(例如,变量、函数、类型声明等)。
PeerMacro
public protocol PeerMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
功能:在现有声明旁边生成平级结构,通常用于插入同级声明。
AccessorMacro
public protocol AccessorMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax]
}
功能:为属性添加访问器(如 get
、set
、didSet
等)。
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]
}
功能:为成员添加统一的修饰符或属性标签。
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]
}
功能:为类型添加成员(如属性、方法、构造器等)。
BodyMacro
public protocol BodyMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
in context: some MacroExpansionContext
) throws -> [CodeBlockItemSyntax]
}
功能:为现有声明提供具体实现或行为,常用于生成计算属性的实现或补充函数体。
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]
}
功能:为类型生成扩展,通常用于协议一致性等。
CodeItemMacro
:用于插入宽泛的代码片段。 PreambleMacro
:为文件自动注入文件级代码。
Swift 宏系统之所以强大,在于它并不追求“全能型”宏,而是通过“角色划分”将每种宏限制在特定场景中。这不仅让系统具备类型安全与上下文约束,还帮助开发者在设计宏时建立起清晰的思维路径。
本章我们将构建这样一个模型:每一种宏的“角色” → 应该遵循的“协议” → 实现的“行为结构” 。
并通过一些实际例子,帮助你理解 如何基于宏的使用意图,选择正确的协议与输出结构。
一个宏所作用的语法位置被称为它的 角色(Role) 。角色决定了宏能应用在哪类语法结构上(如表达式、属性、类型、函数体等),也决定了宏展开时能生成哪类代码结构。
你想做什么? | 角色名称 | 示例 |
---|---|---|
在表达式中插入代码? | 表达式级宏 | #stringify(value) |
为 struct 自动添加成员? | 成员绑定宏 | @AddID |
生成 computed 属性的 getter/setter? | 属性访问宏 | @UserDefault |
自动生成某个函数体? | 函数体宏 | @AddDescription |
为类型生成协议扩展和默认实现? | 扩展绑定宏 | @CodableSubclass |
额外添加旁路函数或类型? | 对等绑定宏 | @BindEvent |
每个角色背后都对应着一个(或多个)专用协议,用来限制其行为。
Swift 宏协议是以 Macro
结尾的一组协议,定义了你在该角色下应该实现的接口。
角色 | 对应协议 | 你要返回的结构类型 |
---|---|---|
表达式级宏 | ExpressionMacro |
ExprSyntax |
声明级宏 | DeclarationMacro |
[DeclSyntax] |
成员绑定宏 | MemberMacro |
[DeclSyntax] |
对等绑定宏 | PeerMacro |
[DeclSyntax] |
属性访问宏 | AccessorMacro |
[AccessorDeclSyntax] |
扩展绑定宏 | ExtensionMacro |
[ExtensionDeclSyntax] |
成员属性宏 | MemberAttributeMacro |
[AttributeSyntax] |
函数体宏 | BodyMacro |
CodeBlockSyntax |
这些协议都提供了一个 static func expansion(...)
方法,但根据角色不同,返回的语法结构也各不相同。
宏的本质是 “你想让它为你生成什么代码?” ,这套设计过程可以简化为三步:
你想扩展的目标(角色)
↓
确定宏协议
↓
实现 expansion,构造语法树(行为)
我们将这个过程称为**「角色 → 协议 → 行为」**的思维模型。
例子 1:我想为 struct 添加一个成员 ID
MemberMacro
DeclSyntax
形式的变量声明@AddID
struct User { }
→ 展开为:
struct User {
var id: String = UUID().uuidString
}
例子 2:我想为属性自动生成访问器(getter/setter)
AccessorMacro
[AccessorDeclSyntax]
,如 get
和 set
@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 宏(Macro)对许多开发者来说,是一种既熟悉又陌生的工具。在 Objective-C 时代,我们经常使用 #define
、条件编译、日志封装,甚至自动插桩来提升开发效率。这些基于 C 的宏机制虽然灵活强大,却缺乏类型检查,容易引发错误,调试困难且可读性差。因此,Swift 在最初设计时,选择摒弃这种宏体系,专注于类型安全与语法清晰。
但这并不意味着 Swift 不需要“宏”能力。相反,随着语言的发展和应用场景的复杂化,开发者始终渴望一种既安全又可控的自动代码生成机制。正因如此,Swift 的全新宏系统应运而生。
自 Swift 5.9 起,Apple 正式引入了 宏系统(Macros) ,它允许我们通过编译期的语法扩展(Macro Expansion)自动生成 Swift 代码,具备更强的表达力、更高的类型安全性,以及良好的 IDE 支持。相较于传统 C 宏,Swift 宏具备以下显著优势:
宏会在编译阶段对源代码进行“展开”,自动生成结构化的 Swift 代码,帮助我们避免重复劳动。下图展示了宏展开的过程:
官方描述中提到,Swift 宏遵循“加法原则”:宏只添加代码,不移除或篡改现有逻辑,以确保代码结构和意图的清晰可控。这意味着,宏通常在现有代码基础上进行扩展,而不是完全替换原有的实现。
然而,实际上,Swift 宏的行为并非总是如此。特定类型的宏,如
BodyMacro
,确实可以完全替换现有代码。因此,可以理解为在某些场景下,完全替换代码是预期的行为,而非违反“加法原则”的异常情况。💡 个人认为:最初的 Swift 宏定义严格遵循“加法原则”,但随着
BodyMacro
类型的引入,这一原则在某些情况下被打破,以提供更高的灵活性和功能扩展。
更重要的是,Swift 会对宏的输入与输出进行语法与类型检查,确保生成的代码在语义上也是正确的。如果宏实现存在问题,或使用方式不符合规则,编译器会抛出错误,使得问题可以在编译期被及时发现。这种设计大幅提升了宏使用的可靠性与开发信心。
Swift 中的宏主要分为两类:
虽然这两类宏的使用方式略有不同,但它们共享统一的扩展机制,且都通过实现相应的协议来定义行为。接下来的章节将详细介绍这两种宏的使用场景、实现方式与典型示例。
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 代码。
Swift 宏的实现和使用分为多个阶段。以官方模板示例( #stringify
)为例,演示如何用 Xcode 创建一个可运行、可测试的宏工程。
要求:Xcode 15+,Swift 5.9+
通过 File → New → Package 创建一个新的 Swift 包,Xcode 会自动为你生成一个包含宏插件支持的标准结构。
工程模版说明
MyMacro/
├── Package.swift ← Swift 包描述文件
├── Sources/
│ ├── MyMacro/ ← 宏声明(对外暴露)
│ │ └── MyMacro.swift
│ └── MyMacroMacros/ ← 宏实现(逻辑 + 注册)
│ └── StringifyMacro.swift
├── Tests/
│ └── MyMacroTests/ ← 单元测试
│ └── MyMacroTests.swift
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroMacros", type: "StringifyMacro")
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))"
}
}
@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」,即可看到宏生成的真实代码
在开发宏的过程中,调试是一件比较特殊的事。你可能会遇到这些问题:
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 宏系统能怎么帮助我实现代码生成?
如果我有很多重复的代码任务(比如编写 Codable
、Equatable
等),如何通过宏来减少重复工作,提升开发效率,而不需要每次手动编写一遍?
这些问题,都是理解 Swift 宏系统的关键。在你初次接触 Swift 宏时会感到迷茫,但正是这些思考,能够帮助你深入理解 Swift 宏系统的强大能力。
在下一篇《Swift Macros - 2. 宏之全貌》中,我们将从宏系统的设计理念出发,全面剖析 Swift 宏的结构组成与角色划分,建立起宏编程的认知地图。你将逐步掌握宏定义背后的底层原理,为真正驾驭 Swift 的编译期能力打下坚实基础。
本文介绍了 Swift 宏 的背景与引入动机,阐述了宏在编程中的基本用途,帮助开发者理解宏如何简化代码编写、提升开发效率。内容还包括了宏的基本配置方式与使用场景,适合入门读者对宏有一个初步的了解。
这篇文章从宏的类型体系、执行原理到角色机制和协议模型,为读者提供了一个宏系统的全景鸟瞰。文章介绍了宏在 Swift 中的不同角色及其协议关系,阐述了宏的扩展、展开过程以及如何结合协议实现多角色配合,帮助有一定基础的开发者全面理解宏的设计和应用。
本篇文章聚焦 Swift 宏系统中的“职责控制”与“命名控制”机制,系统讲解宏的三大核心组成:宏类型(Macro Kind) 、宏角色(Macro Role) 和 命名说明符(Name Specifier) 。通过区分独立宏与绑定宏、梳理各类宏角色,并结合命名说明符的使用方式,帮助开发者构建更清晰、稳定、可维护的宏能力体系。
本篇文章聚焦 Swift 宏系统中的“语法基础”与“结构操控”能力,深入剖析宏所操作的核心结构——语法节点(Syntax) 。通过讲解 SwiftSyntax 的节点体系、语法树的提取与构造方式、以及 (raw:)
插值的正确用法,帮助开发者掌握宏展开背后的语法逻辑与生成原理,为构建结构化、类型安全的宏能力奠定坚实基础。
本文梳理了 SwiftSyntax 中常用的语法节点,涵盖表达式、声明、语句、类型、模式、属性、宏以及其他常用节点。每个节点均提供简要描述和快速构造示例,旨在为开发者在使用 Swift 宏和 SwiftSyntax 进行代码生成、分析和转换时,提供一份高效、准确的参考资料。
Swift 宏系统通过不同的协议定义了宏的多种角色和能力,从表达式生成、声明插入,到成员绑定、访问控制等。本系列文章将逐个讲解这些协议的定义、特性及适用场景,帮助你系统理解宏的角色体系。
简要介绍 Swift 中所有宏协议的概念、作用及它们之间的关系,作为理解整个宏协议体系的入口。 查看详情
讲解 ExpressionMacro
协议的定义与使用场景,展示如何基于表达式节点生成动态代码。
分析 DeclarationMacro
如何生成声明代码,结合初始化器、变量、函数的实际生成场景讲解。
展示 MemberMacro
如何为结构体或类动态插入属性、方法等成员内容,是构建派生代码的核心手段。
说明 PeerMacro
的用途,即在同一层级下生成兄弟声明,常用于生成辅助类型或伴生属性等。
讲解 AccessorMacro
如何为现有的属性生成 getter/setter 等访问逻辑,结合 UserDefault 等场景展开。
展示 ExtensionMacro
用于生成扩展声明,实现协议适配、默认实现等功能。
讲解 MemberAttributeMacro
的应用,如为属性自动添加注解、权限、内联标识等修饰语。
说明 BodyMacro
的设计方式,以及如何为已有函数添加函数体代码,常用于方法插桩与默认实现生成。
我今天见到美国的审核人员真人了!
5月6日,Apple开发者官方公众号发起了一次《App审核预约沟通》的活动。起初我不以为然,因为苹果以前也搞过类似的活动,请几个苹果审核专家宣讲常见的过审问题,最后有几分钟有提问环节,干货不多。
但这次的活动似乎和以往不太一样,预约前要选语言偏好,而且会议不是在一个统一时间开,而是要自己预约时间点。我预约的时候发现,可选的时段一半以上都是北京时间的凌晨左右,当时我还觉得很奇怪,甚至有些气愤,苹果这么没人性吗,故意把会议安排在凌晨。我运气不错挑了个上午11:00点的。预约成功后,我才反应过来,难道这次是1v1的视频沟通?既激动又紧张。
今天上午到了预定时间,我提前10分钟就进入会议频道。想着美国那边这会都晚上8点多了,审核人员估计也想早点下班吧,说不定会提前进来。结果苹果审核真守时,11:00整准时进来的。是一个中年的戴眼镜的亚洲女士,着休闲装,穿着打扮挺美国人的,看起来很亲和。互相打过招呼后,她就熟练地开始介绍会议规则,蹩脚的中文时不时夹一些英文,我没听懂她具体说了什么,但大概的意思是不允许截图、录屏等。
交流挺顺利的,我想问的核心问题得到了解决,一看时间才过了7分钟。会议最多30分钟,这不得再多聊会,有种挂早了就亏了的感觉。下面是我问的几个问题,可以分享给大家。
1、我们有款应用停运主动下架了,想在另外一个账号上重新制作一款全新的应用,用同样的名字,但名字被旧应用占用了,不想转让应用,怎么解决?
苹果审核:光下架是不行的,在原账号上把应用删除就可以了。
我:(我有点不相信这么简单)删除就可以了吗,但是删除的应用是可以随时从苹果后台恢复的呀。
苹果审核表示,删除应用后,别的账号确实就可以使用这个名字了,并提醒我删除后要尽快操作,避免名字被其他人抢占了。
事后我实操了一下,确实删除了旧应用,新账号就可以保存成功了。
官方文档上《移除 App》中有这么一段,解答我上面的疑问。
如果你移除了某个 App,则会失去该 App 名称的所有权。对于已移除的 App,如果其名称当前未被其他开发者账户使用,则该 App 可以恢复。
真是个冷知识。
2、社交应用想在美区上架,有什么需要注意的地方,比如资质文件、应用内容这些方面?
苹果审核说了一些通用性的内容,没有比较有用的信息:
上美区是否需要资质文件,她说不需要。
3、美区现在支持第三方支付了,如果同一个包发多地区,那包里必然会包含第三方支付的代码以及切换支付的功能,比如 美国商店显示第三方支付方式,其它地区商店隐藏第三方支付,审核会被判定为隐藏功能2.3.1吗?美区第三方支付的技术实现细节,哪里有相关的资料可以查看?
我以为苹果审核会很回避这个话题,毕竟第三方支付是不利于苹果公司的,没想到她听完并没有特别的反应,这反倒显得我很紧张。
....(手动加密)....
后续为付费内容,有兴趣的掘友可以前往公众号付费阅读,感谢您的支持。
mp.weixin.qq.com/s/jGyK1QoNo…
4、苹果审核流程细节,苹果是否会先机审,然后出一份机审报告,人工对照报告再进行人工审核?4.3被拒苹果那边是否可以看到相似应用的截图?
....(手动加密)....
....(手动加密)....