构建和审查采用现代MV架构、状态管理、组合、隔离预览和迁移指导的SwiftUI视图。涵盖@Observable所有权、@State/@Bindable/@Environment连接、视图分解、ViewModifier、环境值、.task加载、iOS 26+交接、Writing Tools、剪贴板可用性和性能。适用于组织SwiftUI状态、管理@Observable、组合视图、预览有意义的UI状态或纠正SwiftUI模式。
SwiftUI Patterns
面向iOS 26+和Swift 6.3的现代SwiftUI模式。涵盖架构、状态管理、视图组合、环境连接、异步加载、设计打磨和平台/分享集成。导航、布局、动画和Liquid Glass模式位于专门的兄弟技能中。除非另有说明,模式向后兼容至iOS 17。
目录
- 架构:Model-View (MV) 模式
- 工作流程
- 状态管理
- 视图排序约定
- 视图组合
- 环境
- 异步数据加载
- iOS 26+ 新API
- 性能指南
- HIG对齐
- Writing Tools (iOS 18+)
- 常见错误
- 审查清单
- 参考资料
范围边界: 本技能涵盖架构、状态所有权、组合、环境连接、异步加载以及相关的SwiftUI应用结构模式。详细的导航模式在swiftui-navigation技能中涵盖,包括NavigationStack、NavigationSplitView、sheet、标签页和深度链接模式。详细的布局、容器和组件模式在swiftui-layout-components技能中涵盖,包括栈、网格、列表、滚动视图模式、表单、控件、使用.searchable的搜索UI、覆盖层及相关布局组件。详细的动画编排在swiftui-animation中涵盖。Liquid Glass采用、自定义玻璃控件、滚动边缘效果、.scrollEdgeEffectStyle和.backgroundExtensionEffect在swiftui-liquid-glass中涵盖。
工作流程
- 记录当前的状态所有权、操作、副作用、导航和生命周期行为。
- 选择保持该契约的最小MV/状态/组合更改。
- 在每个结构步骤后构建;在继续之前修复编译器和隔离错误。
- 为已加载、加载中、空和错误状态(如适用)渲染确定性预览,包括所需的环境依赖。
- 执行重要的交互和副作用。如果行为发生变化,恢复测试夹具,修复最小边界,并重新运行相同的构建、预览和交互检查。
加载行为保持的视图重构以重构现有视图,加载隔离预览构建以获取测试夹具和依赖模式。
架构:Model-View (MV) 模式
默认使用MV——视图是轻量级状态表达式;模型和服务拥有业务逻辑。除非现有代码已经使用视图模型,否则不要引入视图模型。
核心原则:
- 优先使用
@State、@Environment、@Query、.task和.onChange进行编排 - 通过
@Environment注入服务和共享模型;保持视图小而可组合 - 将大型视图拆分为较小的子视图,而不是引入视图模型
- 测试模型、服务和业务逻辑;保持视图简单和声明式
struct FeedView: View {
@Environment(FeedClient.self) private var client
enum ViewState {
case loading, error(String), loaded([Post])
}
@State private var viewState: ViewState = .loading
var body: some View {
List {
switch viewState {
case .loading:
ProgressView()
case .error(let message):
ContentUnavailableView("错误", systemImage: "exclamationmark.triangle",
description: Text(message))
case .loaded(let posts):
ForEach(posts) { post in
PostRow(post: post)
}
}
}
.task { await loadFeed() }
.refreshable { await loadFeed() }
}
private func loadFeed() async {
do {
let posts = try await client.getFeed()
viewState = .loaded(posts)
} catch {
viewState = .error(error.localizedDescription)
}
}
}
有关MV模式的原理、应用连接和轻量级客户端示例,请参见references/architecture-patterns.md。
状态管理
@Observable 所有权规则
重要: 当SwiftUI视图拥有、修改或绑定到UI绑定的@Observable存储和视图模型时,将其隔离在@MainActor上。观察跟踪变化,但不会使共享可变状态线程安全。不涉及UI状态的领域模型可以使用自己的隔离策略。
| 包装器 | 使用时机 |
|---|---|
@State |
视图拥有对象或值。创建并管理生命周期。 |
let |
视图接收一个@Observable对象。只读观察——无需包装器。 |
@Bindable |
视图接收一个@Observable对象并需要双向绑定($property)。 |
@Environment(Type.self) |
从环境中访问共享的@Observable对象。 |
@State(值类型) |
视图本地简单状态:开关、计数器、文本字段值。始终为private。 |
@Binding |
与父视图的@State或@Bindable属性的双向连接。 |
所有权模式
// UI绑定的@Observable存储——主线程隔离
@MainActor
@Observable final class ItemStore {
var title = ""
var items: [Item] = []
}
// 拥有模型的视图
struct ParentView: View {
@State private var viewModel = ItemStore()
var body: some View {
ChildView(store: viewModel)
.environment(viewModel)
}
}
// 读取的视图(@Observable无需包装器)
struct ChildView: View {
let store: ItemStore
var body: some View { Text(store.title) }
}
// 需要绑定的视图(需要双向访问)
struct EditView: View {
@Bindable var store: ItemStore
var body: some View {
TextField("标题", text: $store.title)
}
}
// 从环境读取的视图
struct DeepView: View {
@Environment(ItemStore.self) private var store
var body: some View {
@Bindable var s = store
TextField("标题", text: $s.title)
}
}
细粒度跟踪: SwiftUI仅重新渲染读取了已更改属性的视图。如果视图读取了items但没有读取isLoading,更改isLoading不会触发重新渲染。这是相对于ObservableObject的主要性能优势。
遗留的ObservableObject
仅在支持iOS 16或更早版本时使用。@StateObject → @State,@ObservedObject → let,@EnvironmentObject → @Environment(Type.self)。
视图排序约定
从上到下排序成员:1) @Environment 2) let属性 3) @State / 存储属性 4) 计算属性var 5) init 6) body 7) 视图构建器/辅助函数 8) 异步函数
视图组合
提取子视图
将视图拆分为专注的子视图。每个子视图应具有单一职责。
重构现有视图时,加载行为保持的视图重构
以了解操作/副作用边界和构建/预览验证。
var body: some View {
VStack {
HeaderSection(title: title, isPinned: isPinned)
DetailsSection(details: details)
ActionsSection(onSave: onSave, onCancel: onCancel)
}
}
计算视图属性
对于小的、无状态片段,保持计算some View属性。当出现以下任何信号时,将部分提取为专用的View类型:
- 有意义的分支或实质性布局
- 自己的状态或异步生命周期
- 比父视图更窄的Observation依赖
- 有用的独立预览
- 足够复杂以至于掩盖父视图的数据流
在缩小依赖时,只传递子视图所需的值、绑定和操作。如果它们形成一个庞大但内聚的接口,则传递一个功能范围的@Observable模型。Observation将失效限制在子视图读取的属性上,但应用范围的存储仍然创建了一个广泛的接口;将其保留给真正需要该内聚状态的子视图。
复用是有用的结果,而不是分解的先决条件。
扩展和// MARK: -用于组织大型文件;它们不会创建视图边界或替代提取。
ViewBuilder函数
对于不需要单独结构体的条件逻辑:
@ViewBuilder
private func statusBadge(for status: Status) -> some View {
switch status {
case .active: Text("活跃").foregroundStyle(.green)
case .inactive: Text("不活跃").foregroundStyle(.secondary)
}
}
自定义视图修饰符
将重复的样式提取到ViewModifier中:
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background)
.clipShape(.rect(cornerRadius: 12))
.shadow(radius: 2)
}
}
extension View { func cardStyle() -> some View { modifier(CardStyle()) } }
稳定的视图树
避免顶层的条件视图切换。优先使用单个稳定的基础视图,并在部分或修饰符内部使用条件。
当提取的视图需要独立的状态覆盖、确定性测试夹具或环境设置时,加载隔离预览构建。
环境
自定义环境值
使用@Entry创建自定义环境值和操作。它会为EnvironmentValues生成入口样板代码。
extension EnvironmentValues {
@Entry var theme: Theme = .default
@Entry var refreshFeed: @Sendable () async -> Void = {}
}
// 使用
.environment(\.theme, customTheme)
.environment(\.refreshFeed) { await feedStore.refresh() }
@Environment(\.theme) private var theme
@Environment(\.refreshFeed) private var refreshFeed
对于iOS 17兼容代码或更旧的兼容性垫片,请改用手动EnvironmentKey类型。
常见内置环境值
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@Environment(\.horizontalSizeClass) var sizeClass
@Environment(\.isSearching) var isSearching
@Environment(\.openURL) var openURL
@Environment(\.modelContext) var modelContext
异步数据加载
始终使用.task——它会在视图消失时自动取消:
struct ItemListView: View {
@State var store = ItemStore()
var body: some View {
List(store.items) { item in
ItemRow(item: item)
}
.task { await store.load() }
.refreshable { await store.refresh() }
}
}
使用.task(id:)在依赖项更改时重新运行:
.task(id: searchText) {
guard !searchText.isEmpty else { return }
await search(query: searchText)
}
除非需要存储引用以进行取消,否则永远不要在onAppear中创建手动Task。例外:在同步操作闭包(例如按钮操作)中,Task {}可用于在异步工作之前进行即时状态更新。
使用swift-concurrency处理取消处理器、防抖和时钟、AsyncSequence或参与者隔离。
iOS 26+ 新API
将.scrollEdgeEffectStyle、.backgroundExtensionEffect和玻璃控件路由到swiftui-liquid-glass;将@Animatable路由到swiftui-animation。TextEditor(text: Binding<AttributedString>)是iOS 26富文本编辑路径。在采用这些API的代码旁边保留可用性检查。
剪贴板命令修饰符不是iOS 26的默认值:.copyable、.cuttable和基于命令的.pasteDestination(for:action:validator:)在当前的Apple文档中是macOS 13+和iOS/iPadOS/Mac Catalyst 27 beta。对于iOS 26目标,使用UIPasteboard进行自定义剪贴板命令,或使用拖放和ShareLink进行Transferable流程。请参见references/platform-and-sharing.md。
性能指南
- 惰性栈/网格: 对于大型集合,使用
LazyVStack、LazyHStack、LazyVGrid、LazyHGrid。常规栈会立即渲染所有子视图。 - 稳定ID:
List/ForEach中的所有项目必须符合Identifiable并具有稳定ID。切勿使用数组索引。 - 避免body重新计算: 将过滤和排序移动到计算属性或模型中,而不是内联在
body中。 - Equatable视图: 对于不必要重新渲染的复杂视图,使其符合
Equatable。
HIG对齐
遵循Apple人机界面指南进行布局、排版、颜色和可访问性。关键规则:
- 使用语义颜色(
Color.primary、.secondary、Color(uiColor: .systemBackground))以自动支持浅色/深色模式 - 使用系统字体样式(
.title、.headline、.body、.caption)以支持动态类型 - 使用
ContentUnavailableView处理空和错误状态 - 除非需要特定值,否则省略栈上的
spacing:——nil(默认值)使用平台适应的自适应间距 - 通过
horizontalSizeClass支持自适应布局 - 提供VoiceOver标签(
.accessibilityLabel)并通过切换布局方向支持动态类型可访问性大小
请参见references/design-polish.md了解HIG、主题、触觉、焦点、过渡和加载模式。
Writing Tools (iOS 18+)
使用.writingToolsBehavior(_:)控制文本视图上的Apple Intelligence Writing Tools体验。
| 级别 | 效果 | 使用时机 |
|---|---|---|
.complete |
完整的内联重写(校对、重写、转换) | 笔记、邮件、文档 |
.limited |
简化的覆盖面板体验 | 代码编辑器、验证表单 |
.disabled |
完全隐藏Writing Tools | 密码、搜索栏 |
.automatic |
系统根据上下文选择(默认) | 大多数视图 |
TextEditor(text: $body)
.writingToolsBehavior(.complete)
TextField("搜索…", text: $query)
.writingToolsBehavior(.disabled)
检测活动会话: 读取UITextView(UIKit)上的isWritingToolsActive以推迟验证或暂停撤消分组,直到重写完成。
常见错误
-
使用
@ObservedObject创建对象——应使用@StateObject(遗留)或@State(现代) -
在视图
body中进行大量计算——应移至模型或计算属性 -
未使用
.task进行异步工作——在onAppear中手动创建Task如果不取消会导致泄漏 -
使用数组索引作为
ForEach的ID——会导致错误的差异和UI错误 -
忘记
@Bindable——在@Observable上使用$property语法需要@Bindable -
过度使用
@State——仅用于视图本地状态;共享状态应属于@Observable -
将复杂或可独立预览的部分保留为计算属性——应提取为
View类型;扩展和// MARK:仅用于组织 -
使用
NavigationView——已弃用;应使用NavigationStack -
在
foregroundStyle(_:)更符合语义样式时使用foregroundColor(_:) -
在body中使用内联闭包——将复杂闭包提取为方法
-
当状态表示模型时使用
.sheet(isPresented:)——应改用.sheet(item:) -
在常规分支中使用
AnyView——类型擦除隐藏结构,可能损害性能或身份敏感的过渡。除非API确实需要异构视图存储,否则使用@ViewBuilder、Group或泛型。请参见references/deprecated-migration.md -
将
@AppStorage放在@Observable类内部。@AppStorage是一个视图DynamicProperty;将其保留在View中,或在模型中暴露一个由UserDefaults支持的常规观察属性。 -
在每个栈上硬编码
spacing:——省略它以获得自适应平台间距;仅在值是有意指定时指定 -
将
.copyable、.cuttable或基于命令的.pasteDestination(for:action:validator:)视为iOS 16/iOS 26 API——它们在当前的Apple文档中是macOS 13+和iOS/iPadOS/Mac Catalyst 27 beta。对于iOS 26目标,使用UIPasteboard、拖放或ShareLink。 -
将现代默认值视为正式弃用——
#Preview是现代预览默认值,但PreviewProvider是遗留的而非编译器弃用。EditButton、.onDelete和.onMove在编辑模式列表工作流中仍然有效;使用.swipeActions进行上下文行操作。 -
使必需的依赖项变为可选以阻止预览崩溃——改为安装确定性预览依赖项,无需实时网络、身份验证、生产数据库或全局单例
审查清单
- [ ] 共享状态模型使用
@Observable(iOS 17+上不使用ObservableObject) - [ ]
@State拥有对象;let/@Bindable接收它们 - [ ] 检查迁移和可用性声明以确认当前平台支持,特别是剪贴板和分享API
- [ ] 使用
NavigationStack(不是NavigationView) - [ ] 使用
.task修饰符进行异步数据加载 - [ ] 大型集合使用
LazyVStack/LazyHStack - [ ] 使用稳定的
IdentifiableID(不是数组索引) - [ ] 提取使用分支/布局、生命周期、依赖、预览或父流程信号;小的无状态片段保持计算属性
- [ ] 扩展和
// MARK:仅用于组织文件 - [ ] 仅结构重构保持行为;使用精简的操作/生命周期方法,将可重用逻辑保留在服务/模型中,然后构建并渲染有用的预览
- [ ] 预览涵盖有意义的已加载/加载中/空/错误状态,使用确定性测试夹具和每个必需的环境依赖
- [ ] 视图
body中没有大量计算 - [ ] 深度共享状态使用环境
- [ ] 当语义样式优于固定颜色时使用
foregroundStyle(_:) - [ ] 重复样式使用自定义
ViewModifier - [ ] 优先使用
.sheet(item:)而不是.sheet(isPresented:) - [ ] Sheet拥有自己的操作并在内部调用
dismiss() - [ ] 遵循MV模式——没有不必要的视图模型
- [ ] UI绑定的
@Observable存储和视图模型是@MainActor隔离的 - [ ] 跨并发边界传递的模型类型是
Sendable - [ ] 栈的
spacing:省略,除非需要特定值(优先使用自适应默认值)
参考资料
- 架构、应用连接和轻量级客户端:references/architecture-patterns.md
- 设计打磨(HIG、主题、触觉、过渡、加载、焦点):references/design-polish.md
- 弃用API迁移:references/deprecated-migration.md
- 平台和分享模式(Transferable、剪贴板可用性、媒体、菜单、macOS设置):references/platform-and-sharing.md
- 隔离预览构建(状态覆盖、测试夹具和环境依赖):references/preview-isolation.md
- 现有视图重构(行为契约、操作/副作用边界和验证):references/view-refactoring.md






