swiftui-patterns by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-patterns面向 iOS 26+ 和 Swift 6.2 的现代 SwiftUI 模式。涵盖架构、状态管理、视图组合、环境配置、异步加载、设计优化以及平台/共享集成。导航和布局模式位于独立的兄弟技能中。除非特别说明,这些模式向后兼容至 iOS 17。
范围边界: 本技能涵盖架构、状态所有权、组合、环境配置、异步加载以及相关的 SwiftUI 应用结构模式。详细的导航模式在 swiftui-navigation 技能中介绍,包括 NavigationStack、NavigationSplitView、表单、标签页和深度链接模式。详细的布局、容器和组件模式在 swiftui-layout-components 技能中介绍,包括堆栈、网格、列表、滚动视图模式、表单、控件、使用 .searchable 的搜索界面、覆盖层以及相关的布局组件。
默认采用 MV 模式 —— 视图是轻量级的状态表达;模型和服务拥有业务逻辑。除非现有代码已在使用,否则不要引入视图模型。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
核心原则:
@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。
重要提示: 始终使用 @MainActor 注解 @Observable 视图模型类,以确保在 UI 绑定的状态在主线程上更新。这是 Swift 6 并发安全所必需的。
| 包装器 | 何时使用 |
|---|---|
@State | 视图拥有对象或值。创建并管理其生命周期。 |
let | 视图接收一个 @Observable 对象。只读观察 —— 无需包装器。 |
@Bindable | 视图接收一个 @Observable 对象并需要双向绑定($property)。 |
@Environment(Type.self) | 从环境中访问共享的 @Observable 对象。 |
@State(值类型) | 视图本地的简单状态:开关、计数器、文本字段值。始终为 private。 |
@Binding | 与父视图的 @State 或 @Bindable 属性的双向连接。 |
// @Observable 视图模型 —— 始终使用 @MainActor
@MainActor
@Observable final class ItemStore {
var title = ""
var items: [Item] = []
}
// 拥有模型的视图
struct ParentView: View {
@State 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("Title", text: $store.title)
}
}
// 从环境中读取的视图
struct DeepView: View {
@Environment(ItemStore.self) var store
var body: some View {
@Bindable var s = store
TextField("Title", text: $s.title)
}
}
细粒度跟踪: SwiftUI 仅重新渲染读取了已更改属性的视图。如果一个视图读取 items 但不读取 isLoading,那么更改 isLoading 不会触发重新渲染。这是相较于 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)
}
}
将相关的子视图作为同一文件中的计算属性;当子视图需要复用或自身携带状态时,提取到独立的 View 结构体中。
var body: some View {
List {
header
filters
results
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.title2)
Text(subtitle).font(.subheadline)
}
}
用于不需要单独结构体的条件逻辑:
@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(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 2)
}
}
extension View { func cardStyle() -> some View { modifier(CardStyle()) } }
避免顶层的条件视图切换。倾向于使用一个稳定的基础视图,在内部区域或修饰符中使用条件。当视图文件超过约 300 行时,使用扩展和 // MARK: - 注释进行拆分。
private struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .default
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// 用法
.environment(\.theme, customTheme)
@Environment(\.theme) var theme
@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 {} 是可以接受的。
.scrollEdgeEffectStyle(.soft, for: .top) —— 滚动边缘的淡入淡出边缘效果.backgroundExtensionEffect() —— 安全区域边缘的镜像/模糊效果@Animatable 宏 —— 自动合成 AnimatableData 一致性(参见 swiftui-animation 技能)TextEditor —— 现在接受 AttributedString 用于富文本LazyVStack、LazyHStack、LazyVGrid、LazyHGrid。常规堆栈会立即渲染所有子视图。List/ForEach 中的所有项必须符合 Identifiable 协议并具有稳定的 ID。切勿使用数组索引。body 中内联进行。Equatable 协议。遵循苹果人机界面指南进行布局、排版、颜色和辅助功能设计。关键规则:
Color.primary、.secondary、Color(uiColor: .systemBackground))以支持自动浅色/深色模式.title、.headline、.body、.caption)以支持动态类型ContentUnavailableView 处理空状态和错误状态horizontalSizeClass 支持自适应布局.accessibilityLabel),并通过切换布局方向支持动态类型的辅助功能尺寸关于人机界面指南、主题、触觉反馈、焦点、过渡和加载模式,请参阅 references/design-polish.md。
使用 .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 —— 仅用于视图本地状态;共享状态应属于 @ObservableNavigationView —— 已弃用;使用 NavigationStack.sheet(isPresented:) —— 应使用 .sheet(item:)AnyView 进行类型擦除 —— 导致身份重置并禁用差异比较。应使用 @ViewBuilder、Group 或泛型。参见 references/deprecated-migration.md@Observable 处理共享状态模型(在 iOS 17+ 上不使用 ObservableObject)@State 拥有对象;let/@Bindable 接收对象NavigationStack(而非 NavigationView).task 修饰符进行异步数据加载LazyVStack/LazyHStackIdentifiable ID(而非数组索引)body 中没有繁重计算ViewModifier 处理重复样式.sheet(item:) 而非 .sheet(isPresented:)dismiss()@Observable 视图模型类被 @MainActor 隔离Sendable 的references/architecture-patterns.mdreferences/design-polish.mdreferences/deprecated-migration.mdreferences/platform-and-sharing.md每周安装量
415
仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex409
github-copilot408
opencode407
kimi-cli406
gemini-cli406
amp406
Modern SwiftUI patterns targeting iOS 26+ with Swift 6.2. Covers architecture, state management, view composition, environment wiring, async loading, design polish, and platform/share integration. Navigation and layout patterns live in dedicated sibling skills. Patterns are backward-compatible to iOS 17 unless noted.
Scope boundary: This skill covers architecture, state ownership, composition, environment wiring, async loading, and related SwiftUI app structure patterns. Detailed navigation patterns are covered in the swiftui-navigation skill, including NavigationStack, NavigationSplitView, sheets, tabs, and deep-linking patterns. Detailed layout, container, and component patterns are covered in the swiftui-layout-components skill, including stacks, grids, lists, scroll view patterns, forms, controls, search UI with .searchable, overlays, and related layout components.
Default to MV -- views are lightweight state expressions; models and services own business logic. Do not introduce view models unless the existing code already uses them.
Core principles:
Favor @State, @Environment, @Query, .task, and .onChange for orchestration
Inject services and shared models via @Environment; keep views small and composable
Split large views into smaller subviews rather than introducing a view model
Test models, services, and business logic; keep views simple and declarative
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)
}
}
For MV pattern rationale, app wiring, and lightweight client examples, see references/architecture-patterns.md.
Important: Always annotate @Observable view model classes with @MainActor to ensure UI-bound state is updated on the main thread. Required for Swift 6 concurrency safety.
| Wrapper | When to Use |
|---|---|
@State | View owns the object or value. Creates and manages lifecycle. |
let | View receives an @Observable object. Read-only observation -- no wrapper needed. |
@Bindable | View receives an @Observable object and needs two-way bindings ($property). |
@Environment(Type.self) | Access shared object from environment. |
// @Observable view model -- always @MainActor
@MainActor
@Observable final class ItemStore {
var title = ""
var items: [Item] = []
}
// View that OWNS the model
struct ParentView: View {
@State 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) var store
var body: some View {
@Bindable var s = store
TextField("Title", text: $s.title)
}
}
Granular tracking: SwiftUI only re-renders views that read properties that changed. If a view reads items but not isLoading, changing isLoading does not trigger a re-render. This is a major performance advantage over ObservableObject.
Only use if supporting iOS 16 or earlier. @StateObject → @State, @ObservedObject → let, @EnvironmentObject → @Environment(Type.self).
Order members top to bottom: 1) @Environment 2) let properties 3) @State / stored properties 4) computed var 5) init 6) body 7) view builders / helpers 8) async functions
Break views into focused subviews. Each should have a single responsibility.
var body: some View {
VStack {
HeaderSection(title: title, isPinned: isPinned)
DetailsSection(details: details)
ActionsSection(onSave: onSave, onCancel: onCancel)
}
}
Keep related subviews as computed properties in the same file; extract to a standalone View struct when reuse is intended or the subview carries its own state.
var body: some View {
List {
header
filters
results
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.title2)
Text(subtitle).font(.subheadline)
}
}
For conditional logic that does not warrant a separate struct:
@ViewBuilder
private func statusBadge(for status: Status) -> some View {
switch status {
case .active: Text("Active").foregroundStyle(.green)
case .inactive: Text("Inactive").foregroundStyle(.secondary)
}
}
Extract repeated styling into ViewModifier:
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 2)
}
}
extension View { func cardStyle() -> some View { modifier(CardStyle()) } }
Avoid top-level conditional view swapping. Prefer a single stable base view with conditions inside sections or modifiers. When a view file exceeds ~300 lines, split with extensions and // MARK: - comments.
private struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .default
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// Usage
.environment(\.theme, customTheme)
@Environment(\.theme) var theme
@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
Always use .task -- it cancels automatically on view disappear:
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() }
}
}
Use .task(id:) to re-run when a dependency changes:
.task(id: searchText) {
guard !searchText.isEmpty else { return }
await search(query: searchText)
}
Never create manual Task in onAppear unless you need to store a reference for cancellation. Exception: Task {} is acceptable in synchronous action closures (e.g., Button actions) for immediate state updates before async work.
.scrollEdgeEffectStyle(.soft, for: .top) -- fading edge effect on scroll edges.backgroundExtensionEffect() -- mirror/blur at safe area edges@Animatable macro -- synthesizes AnimatableData conformance automatically (see swiftui-animation skill)TextEditor -- now accepts AttributedString for rich textLazyVStack, LazyHStack, LazyVGrid, LazyHGrid for large collections. Regular stacks render all children immediately.List/ForEach must conform to Identifiable with stable IDs. Never use array indices.body.Equatable.Follow Apple Human Interface Guidelines for layout, typography, color, and accessibility. Key rules:
Color.primary, .secondary, Color(uiColor: .systemBackground)) for automatic light/dark mode.title, .headline, .body, .caption) for Dynamic Type supportContentUnavailableView for empty and error stateshorizontalSizeClass.accessibilityLabel) and support Dynamic Type accessibility sizes by switching layout orientationSee references/design-polish.md for HIG, theming, haptics, focus, transitions, and loading patterns.
Control the Apple Intelligence Writing Tools experience on text views with .writingToolsBehavior(_:).
| Level | Effect | When to use |
|---|---|---|
.complete | Full inline rewriting (proofread, rewrite, transform) | Notes, email, documents |
.limited | Overlay panel only — original text untouched | Code editors, validated forms |
.disabled | Writing Tools hidden entirely | Passwords, search bars |
.automatic | System chooses based on context (default) | Most views |
TextEditor(text: $body)
.writingToolsBehavior(.complete)
TextField("Search…", text: $query)
.writingToolsBehavior(.disabled)
Detecting active sessions: Read isWritingToolsActive on UITextView (UIKit) to defer validation or suspend undo grouping until a rewrite finishes.
@ObservedObject to create objects -- use @StateObject (legacy) or @State (modern)body -- move to model or computed property.task for async work -- manual Task in onAppear leaks if not cancelledForEach IDs -- causes incorrect diffing and UI bugs@Bindable -- $property syntax on requires @Observable used for shared state models (not ObservableObject on iOS 17+)@State owns objects; let/@Bindable receives themNavigationStack used (not NavigationView).task modifier for async data loadingLazyVStack/LazyHStack for large collectionsIdentifiable IDs (not array indices)references/architecture-patterns.mdreferences/design-polish.mdreferences/deprecated-migration.mdreferences/platform-and-sharing.mdWeekly Installs
415
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex409
github-copilot408
opencode407
kimi-cli406
gemini-cli406
amp406
Apple Reminders CLI (remindctl) - 终端管理苹果提醒事项,同步iPhone/iPad
777 周安装
}
@Observable@State (value types) | View-local simple state: toggles, counters, text field values. Always private. |
@Binding | Two-way connection to parent's @State or @Bindable property. |
@Observable@Bindable@State -- only for view-local state; shared state belongs in @ObservableNavigationView -- deprecated; use NavigationStack.sheet(isPresented:) when state represents a model -- use .sheet(item:) insteadAnyView for type erasure -- causes identity resets and disables diffing. Use @ViewBuilder, Group, or generics instead. See references/deprecated-migration.mdbodyViewModifier for repeated styling.sheet(item:) preferred over .sheet(isPresented:)dismiss() internally@Observable view model classes are @MainActor-isolatedSendable