关于 swiftui-patterns
面向 SwiftUI 开发者的模式指南,涵盖架构、状态管理、视图组合、环境注入与异步加载。重点是采用 MV 结构和可验证的构建、预览及交互检查。
主要功能
- 01MV 架构与状态所有权
- 02视图拆分与组合
- 03环境值与依赖注入
- 04异步数据加载模式
- 05性能与 HIG 指引
适用场景
- 01规划 SwiftUI 应用结构
- 02重构大型视图
- 03管理视图状态和依赖
- 04检查加载、空态与错误态预览
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftui-patterns安装后在 Claude Code 中输入/swiftui-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/architecture-patterns.mddeprecated-migration.mddesign-polish.mdplatform-and-sharing.mdpreview-isolation.mdview-refactoring.md详细介绍
SwiftUI 模式
面向 iOS 26+ 的现代 SwiftUI 模式,使用 Swift 6.3。涵盖架构、状态管理、视图组合、环境注入、异步加载、设计打磨以及平台和共享集成。导航、布局、动画和 Liquid Glass 模式见专门的同级技能。除非另有说明,这些模式向后兼容 iOS 17。
目录
- 架构:Model-View(MV)模式
- 工作流
- 状态管理
- 视图顺序约定
- 视图组合
- 环境
- 异步数据加载
- iOS 26+ 新 API
- 性能指南
- 符合 HIG
- 写作工具(iOS 18+)
- 常见错误
- 审查清单
- 参考资料
**范围界定:**此技能涵盖架构、状态所有权、组合、环境注入、异步加载及相关的 SwiftUI 应用结构模式。详细的导航模式见 swiftui-navigation 技能,包括 NavigationStack、NavigationSplitView、工作表、标签页和深度链接模式。详细的布局、容器和组件模式见 swiftui-layout-components 技能,包括堆栈、网格、列表、滚动视图模式、表单、控件、使用 .searchable 的搜索界面、叠加层及相关布局组件。详细的动画编排见 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("Error", 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。Observation 会跟踪更改,但不会让共享可变状态变得线程安全。不涉及 UI 状态的领域模型可以采用自己的隔离策略。
| 包装器 | 使用场景 |
|---|---|
@State | 视图拥有该对象或值。负责创建并管理其生命周期。 |
let | 视图接收一个 @Observable 对象。只读观察——无需包装器。 |
@Bindable | 视图接收一个 @Observable 对象,且需要双向绑定($property)。 |
@Environment(Type.self) | 从环境中访问共享的 @Observable 对象。 |
@State(值类型) | 视图本地的简单状态:开关、计数器、文本字段值。始终使用 private。 |
@Binding | 与父视图的 @State 或 @Bindable 属性建立双向连接。 |
所有权模式
// UI-bound @Observable store -- main-actor isolated
@MainActor
@Observable final class ItemStore {
var title = ""
var items: [Item] = []
}
// View that OWNS the model
struct ParentView: View {
@State private var viewModel = ItemStore()
var body: some View {
ChildView(store: viewModel)
.environment(viewModel)
}
}
// View that READS (no wrapper needed for @Observable)
struct ChildView: View {
let store: ItemStore
var body: some View { Text(store.title) }
}
// View that BINDS (needs two-way access)
struct EditView: View {
@Bindable var store: ItemStore
var body: some View {
TextField("Title", text: $store.title)
}
}
// View that reads from ENVIRONMENT
struct DeepView: View {
@Environment(ItemStore.self) private var store
var body: some View {
@Bindable var s = store
TextField("Title", 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("Active").foregroundStyle(.green)
case .inactive: Text("Inactive").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 = {}
}
// Usage
.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。例外:在同步操作闭包(例如 Button 操作)中,Task {} 可用于在异步工作开始前立即更新状态。
在需要取消处理器、防抖和时钟、AsyncSequence 或 Actor 隔离时,使用 swift-concurrency。
iOS 26+ 新 API
将 .scrollEdgeEffectStyle、.backgroundExtensionEffect 和玻璃控件交由 swiftui-liquid-glass 处理;将 @Animatable 交由 swiftui-animation 处理。TextEditor(text: Binding<AttributedString>) 是 iOS 26 的富文本编辑方案。在采用这些 API 的代码旁保留可用性检查。
剪贴板命令修饰符并非 iOS 26 的默认功能:根据 Apple 当前文档,.copyable、.cuttable 和基于命令的 .pasteDestination(for:action:validator:) 支持 macOS 13+ 和 iOS/iPadOS/Mac Catalyst 27 beta。对于 iOS 26 目标,自定义剪贴板命令请使用 UIPasteboard;对于 Transferable 流程,请使用拖放和 ShareLink。请参阅 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),并通过切换布局方向支持动态字体的辅助功能尺寸
有关 HIG、主题、触觉反馈、焦点、转场和加载模式,请参阅 references/design-polish.md。
写作工具(iOS 18+)
使用 .writingToolsBehavior(_:) 控制文本视图中的 Apple Intelligence 写作工具体验。
| 级别 | 效果 | 使用场景 |
|---|---|---|
.complete | 完整的行内改写(校对、重写、转换) | 备忘录、电子邮件、文档 |
.limited | 精简的浮层面板体验 | 代码编辑器、经过验证的表单 |
.disabled | 完全隐藏写作工具 | 密码、搜索栏 |
.automatic | 系统根据上下文选择(默认) | 大多数视图 |
TextEditor(text: $body)
.writingToolsBehavior(.complete)
TextField("Search…", 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 -
在
@Observable类中放置@AppStorage。@AppStorage是视图的DynamicProperty;请将它保留在View中,或在模型中使用由UserDefaults支持的普通可观察属性。 -
在每个堆栈上都硬编码
spacing:——省略它即可采用自适应平台间距;仅在有意指定该值时才设置 -
将
.copyable、.cuttable或基于命令的.pasteDestination(for:action:validator:)误认为是 iOS 16/iOS 26 API——根据当前 Apple 文档,它们适用于 macOS 13+ 和处于 beta 阶段的 iOS/iPadOS/Mac Catalyst 27。对于 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中不进行高开销计算 - 深层共享状态使用 Environment
- 语义化样式优于固定颜色时,使用
foregroundStyle(_:) - 重复样式使用自定义
ViewModifier - 优先使用
.sheet(item:),而不是.sheet(isPresented:) - Sheet 自行管理操作,并在内部调用
dismiss() - 遵循 MV 模式——不使用不必要的视图模型
- 绑定 UI 的
@Observable存储和视图模型隔离到@MainActor - 跨并发边界传递的模型类型符合
Sendable - 除非需要特定值,否则省略 Stack 的
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