要构建交互式 SwiftUI 预览,请为每个预览提供自己的本地状态,将可观察的视图模型传入编辑器,并通过绑定公开可编辑属性。本指南使用合成示例文本构建一个小型 macOS 标签编辑器。
选择预览状态的存放位置
任务是预览一个桌面编辑器:用户可以更改标签的标题和详细文本,然后切换是否显示详细信息。编辑器应立即响应,每个预览都应以明确的示例配置启动。
使用能够支持交互的最少输入。Apple 建议只向视图传入其所需的数据,因为复杂的依赖关系会让构建预览更困难。静态标签可以接收字符串;编辑器则需要与其状态建立可写连接。为界面文件添加预览。
| 视图所需内容 | 合适的输入 | 示例状态的存放位置 |
|---|---|---|
| 显示固定文本 | 不可变字符串 | 直接放在预览中 |
| 编辑由父级拥有的单个值 | @Binding 属性 |
预览宿主中的本地 @State |
| 编辑多个相关的可观察属性 | 通过 @Bindable 暴露的可观察模型 |
存放在预览宿主 @State 中的模型实例 |
| 读取通过环境提供的模型 | 环境依赖项 | 在预览根视图中显式注入 |
在此示例中,视图模型包含两个字符串、一个可见性设置,以及一个计算所得的显示标题。每个预览都有独立的实例。这样,初始条件会清楚地显示在预览声明旁边,示例也不会依赖共享的可变单例。
准备 macOS SwiftUI 目标
此处展示的内联 @Previewable 语法要求使用 Xcode 16 或更高版本。Apple 在 Xcode 16 中引入了该宏。将此示例的最低部署目标设为 macOS 14 或更高版本,因为 SwiftUI 从该版本开始支持 Observation;Apple 当前的 Previewable() 参考资料也列出了 macOS 14 的可用性。Xcode 16 发行说明、在 App 中管理模型数据、Previewable()。
在现有 macOS SwiftUI App 目标中添加名为 LabelEditor.swift 的 Swift 文件。接下来的两个代码块共同构成完整的预览示例。保留 App 现有的入口点;此文件定义模型、编辑器和预览。
代码在受支持的工具链中采用 @State 写法。Apple 当前文档对其实现方式做了区分:Xcode 26 及更早版本使用 State 属性包装器,而 Xcode 27 及更高版本使用 State() 宏。检查生成的代码或分析初始化开销时,请留意这一区别。State、State()。
构建可观察模型和可编辑视图
为模型应用 @Observable,让 SwiftUI 跟踪界面读取的属性。在编辑器中使用 @Bindable,为文本字段和切换控件创建所需的属性绑定。Apple 在编辑可观察模型的说明中记录了这种组合。在 App 中管理模型数据、Bindable。
import Foundation
import Observation
import SwiftUI
@MainActor
@Observable
final class LabelViewModel {
var title: String
var detail: String
var showsDetail: Bool
init(
title: String,
detail: String,
showsDetail: Bool = true
) {
self.title = title
self.detail = detail
self.showsDetail = showsDetail
}
var displayedTitle: String {
let trimmed = title.trimmingCharacters(
in: .whitespacesAndNewlines
)
return trimmed.isEmpty ? "Untitled label" : trimmed
}
}
@MainActor
struct LabelEditor: View {
@Bindable var model: LabelViewModel
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
Text("Title")
TextField("Label title", text: $model.title)
}
VStack(alignment: .leading, spacing: 6) {
Text("Detail")
TextField("Label detail", text: $model.detail)
}
Toggle("Show detail", isOn: $model.showsDetail)
Divider()
Text("Rendered label")
.font(.caption)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 8) {
Text(model.displayedTitle)
.font(.title2)
.fixedSize(horizontal: false, vertical: true)
if model.showsDetail {
Text(model.detail)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(
.quaternary,
in: RoundedRectangle(cornerRadius: 10)
)
}
.textFieldStyle(.roundedBorder)
.padding(20)
}
}
模型有意通过初始化器接收所有示例值。初始化器只会赋值给本地属性。回退计算会改变渲染出的标签内容,但不会覆盖用户输入的标题:只有空白字符的标题仍可编辑,而渲染结果会显示 Untitled label。
编辑器从调用方接收模型。它的 @Bindable 声明提供 $model.title 之类的绑定;状态由预览宿主持有。如果此视图只显示模型的值,那么普通的模型引用就足以支持观察。此处使用 Bindable 存储,是因为控件需要可写的属性绑定。Bindable。
为每个预览提供独立的示例状态
将以下声明追加到同一文件中。每个声明都会使用明确的合成值创建自己的模型。这里的宽度仅用于示意性地检查布局,并非推荐的最小窗口尺寸。
#Preview("Standard label") {
@Previewable @State var model = LabelViewModel(
title: "Sample tile",
detail: "Blue square, medium size"
)
LabelEditor(model: model)
.frame(width: 420)
.preferredColorScheme(.light)
}
#Preview("Long label in a narrow editor") {
@Previewable @State var model = LabelViewModel(
title: "Sample collection with a longer descriptive label",
detail: "Round tiles arranged in three columns"
)
LabelEditor(model: model)
.frame(width: 300)
.preferredColorScheme(.dark)
}
#Preview("Blank title and hidden detail") {
@Previewable @State var model = LabelViewModel(
title: " ",
detail: "An optional detail line",
showsDetail: false
)
LabelEditor(model: model)
.frame(width: 420)
}
@Previewable 会将本地状态声明纳入 #Preview 生成的内嵌 SwiftUI 视图。Apple 文档说明,带有该标记的声明会成为生成视图的属性。请将声明放在预览闭包的根部;在预览主体之外使用 @Previewable 会导致错误。Previewable()。
由此形成了明确的所有权链:生成的预览宿主在 @State 中存储模型,LabelEditor 接收模型引用,而 @Bindable 则公开其可编辑属性。SwiftUI 支持在 state 中存储 Observation 模型,并将该引用传递给子视图。State。
将初始化器中的值视为起始测试数据。交互式预览运行时可以更改这些值。重现特定布局时,请切换到全新的预览实例,再比较其初始外观。
用小型场景矩阵检查行为
打开 LabelEditor.swift,选择 Show Canvas,然后在画布标签页中选择一个已命名的预览。如果画布已暂停,请使用 Restart。启用 Live 模式,以测试文本输入和控件逻辑。Apple 当前的画布指南介绍了这些控件。在画布中与预览交互。
使用以下检查来区分状态问题和布局问题。预期结果来自示例代码,并非测试结果报告。
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 标准标签 | 将标题替换为 Amber circle |
渲染的标题立即更改 |
| 详细信息可见性 | 先关闭 Show detail,再打开 |
详细信息消失后重新显示,且文本得到保留 |
| 空白标题 | 删除标题中的所有字符 | 渲染的标题变为 Untitled label |
| 空格标题 | 只输入空格 | 字段保留这些空格;渲染的标题使用回退值 |
| 窄编辑器 | 查看长标题预览 | 渲染的标题在卡片内换行 |
| 独立测试数据 | 编辑一个预览,然后查看另一个全新的预览 | 另一个预览以其声明的模型值启动 |
如果文本输入有效,但渲染结果没有变化,请检查字段绑定与输出所读取的属性之间的连接。此处两条路径都指向 model.title,直接或通过 displayedTitle 连接。如果切换可见性时详细信息被清空,请查找将值赋为空字符串的代码;此示例只会更改 showsDetail。
研究环境中没有 Swift 编译器或 macOS 预览运行时,因此这些示例是根据文档中的 API 检查的,并未编译或实际运行。改造编辑器之前,请先在 Xcode 中构建目标并完成交互检查。
将共享设置与可编辑测试数据分开
对于这样的小型模型,在本地构建可让每个预览都易于理解。如果之后多个预览需要复用开销较大的设置,Apple 提供了 PreviewModifier:其共享上下文会缓存,并在参与的预览之间复用。该 API 要求 macOS 15 或更高版本。PreviewModifier。
这种有文档依据的共享行为带来了一个实际的设计选择:独立可编辑的测试模型应保留在本地;只有当你确实希望参与的预览复用某项设置时,才使用共享上下文。共享的可变模型也会共享其中的编辑内容,这会让不同场景更难比较。
如果现有视图按类型从环境中获取模型,请在预览根视图中注入所需实例。Apple 文档指出,如果请求类型对应的非可选模型缺失,就会引发异常。因此,预览必须提供其选定根视图实际读取的依赖项。在 App 中管理模型数据。
保留 AppKit 和 TextKit 的边界
上面的编辑器使用 SwiftUI 控件。如果现有 macOS 编辑器包含 AppKit 文本视图,请使用 NSViewRepresentable 将其集成到 SwiftUI 层级中。通过 makeNSView 创建并配置该视图,然后通过 updateNSView 应用传入的 SwiftUI 状态。如果编辑必须回传给 SwiftUI,请通过协调器转发 AppKit 委托的更改。让 SwiftUI 控制所表示视图的框架和边界。NSViewRepresentable。
Apple 还支持直接预览 AppKit 视图和视图控制器。不过,@Previewable 仅适用于 SwiftUI,不能用于直接的 AppKit 预览。若要将本文介绍的本地状态模式用于 AppKit 内容,请预览一个包含 representable 的 SwiftUI 宿主。为界面文件添加预览。
构造 NSTextView 时,请保留所选的文本引擎。Apple 文档说明,可通过 NSTextView(usingTextLayoutManager: true) 在初始化时选择 TextKit 2。TextKit 指南还解释说,访问 TextKit 1 的 layoutManager 可能会触发单向回退,并影响该文本视图实例。如果预期使用 TextKit 2,请采用相应的 TextKit 2 API;预览不会消除这些边界。TextKit 和文本视图的新功能。
在所有权边界处诊断预览故障
从第一个构建诊断开始检查。Swift 宏会在编译时生成代码,因此在宏调用处标出的错误可能涉及生成的声明。Xcode 的 Editor > Expand Macro 命令可用于检查这些代码。应用宏。
然后检查状态机制。现有的基于 Combine 的 ObservableObject 需要相应的所有权模式:Apple 警告,将此类对象存储在 State 中不会跟踪其已发布属性的更改,并建议对象所有者使用 StateObject。此处的示例使用 @Observable,因此其状态所有权遵循 Observation 模式。State。
完成后的预览文件应围绕明确的初始值和可见交互展开。标准、长标题和空白标题场景都按预期运行后,就可以在相应父视图拥有的模型上复用同一个编辑器。预览测试数据仍是重现这些界面状态的简洁方式。
来源
- 为界面文件添加预览 — Apple
- Xcode 16 发行说明 — Apple
- 在 App 中管理模型数据 — Apple
- Previewable() — Apple
- Bindable — Apple
- State — Apple
- State() — Apple
- 在画布中与预览交互 — Apple
- PreviewModifier — Apple
- NSViewRepresentable — Apple
- TextKit 和文本视图的新功能 — Apple
- 应用宏 — Apple