
序:"案发"
毛利侦探事务所又乱了:同一张「下载卡片」案发现场,有时在等、有时下到 42%、有时 98%、有时成功、有时断网。
柯南盯着 Canvas 叹气------总不能每条线索开一间房、再一间间跑吧?
好消息来了:Xcode 27 的 #Preview(arguments:),像把证据一次性摊在桌上,格子点开就能单独细看。

多数 SwiftUI 视图都不止一种样子:空态、有数据、加载中、失败、权限、边角情况......这些状态本该放一起对照。可过去要么 VStack 全堆一个 Preview,要么写一串几乎雷同的 #Preview------像服部喊「一起看」,实际却在走廊里来回跑。
Xcode 27 引入了 #Preview(arguments:):丢给它一组值,每个参数都会变成同一预览组里可独立选择的变体:
swift
@available(iOS 26.0, *)
#Preview(arguments: DownloadItem.previewItems) { item in
DownloadCard(item: item)
}
Canvas 会把参数排成网格;需要放大镜时,再点开某一个变体单独看。
#Preview(arguments:)需要 Xcode 27,可用平台:iOS 26、macOS 27、tvOS 26、watchOS 26、visionOS 26。

先给状态建档
例子是一张小下载卡。

状态放在 DownloadItem 里,Preview 只负责塞一个参数------像柯南只递「这一份证物」。
swift
enum DownloadState {
case waiting
case downloading(progress: Double)
case completed
case failed(message: String)
}
struct DownloadItem {
let fileName: String
let fileSize: String
let state: DownloadState
}
struct DownloadCard: View {
let item: DownloadItem
// 具体 UI 省略,完整实现见文末示例工程
}

建一份 Preview 证物清单
在视图旁或 Preview 专用文件里扩展即可:
swift
extension DownloadItem {
static let previewItems: [Self] = [
.init(fileName: "WWDC sessions.zip", fileSize: "2.4 GB", state: .waiting),
.init(fileName: "WWDC sessions.zip", fileSize: "2.4 GB", state: .downloading(progress: 0.42)),
.init(fileName: "WWDC sessions.zip", fileSize: "2.4 GB", state: .downloading(progress: 0.98)),
.init(fileName: "WWDC sessions.zip", fileSize: "2.4 GB", state: .completed),
.init(fileName: "WWDC sessions.zip", fileSize: "2.4 GB", state: .failed(message: "The connection was lost.")),
]
}

老办法一:一状态一 #Preview
最直接------每个状态单独声明:
swift
#Preview("Waiting", traits: .sizeThatFitsLayout) {
DownloadCard(item: DownloadItem.previewItems[0])
}
// Downloading 42% / 98% / Completed / Failed ... 同理
各自独立,但样板代码重复。
Canvas 当成多个 Preview,你只能来回切换,很难「一眼对照」。加状态还得加声明,名字和数据还得自己对齐------小兰都懒得收拾这种案卷。


老办法二:全塞进一个 VStack
想一次看完,可以:
swift
#Preview("All download states", traits: .sizeThatFitsLayout) {
VStack(spacing: 12) {
ForEach(DownloadItem.previewItems.indices, id: \.self) { index in
DownloadCard(item: DownloadItem.previewItems[index])
}
}
.padding()
.background(Color(uiColor: .systemGroupedBackground))
}
对比很快,但 Canvas 仍只认一个 Preview。
卡片只是大布局里的子视图,不是独立变体------想单独打开某一态、或把网格当状态清单,都不顺手。像把所有证据叠成一摞:看得见,却难抽。


新招:#Preview(arguments:) 生成变体
新宏吃一个数组,闭包接收单个元素:
swift
@available(iOS 26.0, *)
#Preview(
"Download states",
traits: .sizeThatFitsLayout,
arguments: DownloadItem.previewItems
) { item in
DownloadCard(item: item)
.padding()
.background(Color(uiColor: .systemGroupedBackground))
}
该方法接受泛型类型 [T] :枚举、模型、专用场景类型都行;不必 Identifiable / Hashable / Equatable / CaseIterable。
第一个字符串是预览组名。Canvas 渲染含五个参数的网格 ,顺序跟数组一致。若不自定义标签,Xcode 会用默认 description,往往又长又难扫------
灰原大概会说:「标签不清晰,推理效率为零。」


给每个 Canvas 变体起名
让参数类型遵循 CustomStringConvertible,用 description 返回短标签:
swift
extension DownloadItem: CustomStringConvertible {
var description: String {
switch state {
case .waiting: "Waiting"
case let .downloading(progress):
"Downloading \(progress.formatted(.percent.precision(.fractionLength(0))))"
case .completed: "Completed"
case .failed: "Failed"
}
}
}
两个都叫 Downloading 会分不清;标签要短、稳、组内唯一。
点网格里某一格,Xcode 会单独打开该变体,方便细看;有控件时还可进 Interactive 模式------真正的「单独审讯室」。


参数留给状态,环境留给 traits
arguments 适合有限且有意义的输入状态轴:主状态 + 少量会暴露布局问题的边角(超长文件名、大数值、本地化错误文案)。
配色、动态字体、语言、设备、方向,请用 preview traits 或另开预览组。否则小清单会膨胀成难扫的大矩阵------比黑衣组织成员表还可怕。
参数只描述变体的初始输入,不替代本地可变状态;交互要改 binding / 可观察模型时,用 @Previewable 或小包装视图。

部署目标仍可留在 iOS 17
App 不必升到 iOS 26:保持原部署版本,只给新 Preview 标 @available(iOS 26.0, *)。
swift
@available(iOS 17.0, macOS 14.0, tvOS 17.0, visionOS 1.0, watchOS 10.0, *)
nonisolated struct $s23PreviewArgumentsExample0023DownloadCardswift_elFCffMX145_0_33_ABA51F1A69BAC7119FC7A1DC29AA9D8ELl0A0fMf_15PreviewRegistryfMu_: DeveloperToolsSupport.PreviewRegistry {
static var fileID: String {
"PreviewArgumentsExample/DownloadCard.swift"
}
static var line: Int {
146
}
static var column: Int {
1
}
@MainActor static func makePreview() throws -> DeveloperToolsSupport.Preview {
if #available(iOS 26.0, *) {
DeveloperToolsSupport.Preview(
"Download states",
traits: .sizeThatFitsLayout,
arguments: DownloadItem.previewItems
) { item in
DownloadCard(item: item)
.padding()
.background(Color(uiColor: .systemGroupedBackground))
}
} else {
throw DeveloperToolsSupport.PreviewUnavailable()
}
}
}
在声明上 Control-click → Expand Macro 可见:注册表基于更旧的 Preview 基础设施(自 iOS 17),新重载在运行时用 if #available(iOS 26.0, *) 守护;不够新就抛 PreviewUnavailable()。外层的 @available(iOS 17.0, ...) 不会 让 arguments: 在 iOS 17--25 真可用------它只是让低部署目标能干净编译。

"结案"
同一张下载卡、五种案情,不必再像小五郎一样一间间踢门。
把状态收成清单,交给 #Preview(arguments:)------
柯南式结案:证据摊开,一眼看清;要点哪条,点哪条。
那么,宝子们学会了吗?
感谢大家的观赏,我们下次不见不散!