mcpField
S
swiftui-patterns

swiftui-patterns

github.com/dpearson2699开发1.2k4.6k 次安装13 周前更新
代码仓库

关于 swiftui-patterns

面向 SwiftUI 开发者的模式指南,涵盖架构、状态管理、视图组合、环境注入与异步加载。重点是采用 MV 结构和可验证的构建、预览及交互检查。

主要功能

  1. 01MV 架构与状态所有权
  2. 02视图拆分与组合
  3. 03环境值与依赖注入
  4. 04异步数据加载模式
  5. 05性能与 HIG 指引

适用场景

  1. 01规划 SwiftUI 应用结构
  2. 02重构大型视图
  3. 03管理视图状态和依赖
  4. 04检查加载、空态与错误态预览

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-patterns

安装后在 Claude Code 中输入/swiftui-patterns确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
architecture-patterns.md
deprecated-migration.md
design-polish.md
platform-and-sharing.md
preview-isolation.md
view-refactoring.md

详细介绍

SwiftUI 模式

面向 iOS 26+ 的现代 SwiftUI 模式,使用 Swift 6.3。涵盖架构、状态管理、视图组合、环境注入、异步加载、设计打磨以及平台和共享集成。导航、布局、动画和 Liquid Glass 模式见专门的同级技能。除非另有说明,这些模式向后兼容 iOS 17。

目录

**范围界定:**此技能涵盖架构、状态所有权、组合、环境注入、异步加载及相关的 SwiftUI 应用结构模式。详细的导航模式见 swiftui-navigation 技能,包括 NavigationStack、NavigationSplitView、工作表、标签页和深度链接模式。详细的布局、容器和组件模式见 swiftui-layout-components 技能,包括堆栈、网格、列表、滚动视图模式、表单、控件、使用 .searchable 的搜索界面、叠加层及相关布局组件。详细的动画编排见 swiftui-animation。Liquid Glass 的采用、自定义玻璃控件、滚动边缘效果、.scrollEdgeEffectStyle 和 .backgroundExtensionEffect 见 swiftui-liquid-glass。

工作流

  1. 记录当前的状态所有权、操作、副作用、导航和生命周期行为。
  2. 选择最小的 MV/状态/组合改动,以保留现有约定。
  3. 每完成一个结构性步骤后都进行构建;继续之前先修复编译器错误和隔离错误。
  4. 视情况为已加载、加载中、空和错误状态生成确定性预览,并包含必需的环境依赖项。
  5. 检查重要交互和副作用。如果行为发生变化,恢复测试夹具,修复最小边界,然后重新运行相同的构建、预览和交互检查。

重构现有视图时,请加载保持行为的视图重构;了解测试夹具和依赖项模式,请加载隔离式预览构建。

架构: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,以便延后验证,或在改写完成前暂停撤销分组。

文档: WritingToolsBehavior · writingToolsBehavior(_:)

常见错误

  1. 使用 @ObservedObject 创建对象——改用 @StateObject(旧版)或 @State(新版)

  2. 在视图的 body 中进行大量计算——移至模型或计算属性中

  3. 未使用 .task 执行异步工作——在 onAppear 中手动创建 Task,如果不取消就会泄漏

  4. 将数组索引作为 ForEach 的 ID——会导致差异计算错误和 UI 问题

  5. 忘记使用 @Bindable——在 @Observable 上使用 $property 语法需要 @Bindable

  6. 过度使用 @State——它仅用于视图本地状态;共享状态应放在 @Observable 中

  7. 将复杂或可独立预览的部分保留为计算属性——提取为 View 类型;扩展和 // MARK: 只能用于整理代码

  8. 使用 NavigationView——它已弃用;请改用 NavigationStack

  9. 当 foregroundStyle(_:) 更符合语义化样式时,仍使用 foregroundColor(_:)

  10. 在 body 中内联闭包——将复杂闭包提取为方法

  11. 当状态表示一个模型时使用 .sheet(isPresented:)——改用 .sheet(item:)

  12. 将 AnyView 用于常规分支处理——类型擦除会隐藏结构,并可能损害性能或影响依赖视图标识的过渡效果。除非 API 确实需要存储异构视图,否则请使用 @ViewBuilder、Group 或泛型。请参阅 references/deprecated-migration.md

  13. 在 @Observable 类中放置 @AppStorage。 @AppStorage 是视图的 DynamicProperty;请将它保留在 View 中,或在模型中使用由 UserDefaults 支持的普通可观察属性。

  14. 在每个堆栈上都硬编码 spacing:——省略它即可采用自适应平台间距;仅在有意指定该值时才设置

  15. 将 .copyable、.cuttable 或基于命令的 .pasteDestination(for:action:validator:) 误认为是 iOS 16/iOS 26 API——根据当前 Apple 文档,它们适用于 macOS 13+ 和处于 beta 阶段的 iOS/iPadOS/Mac Catalyst 27。对于 iOS 26 目标,请使用 UIPasteboard、拖放或 ShareLink。

  16. 将现代默认方式误认为正式弃用——#Preview 是现代预览的默认方式,但 PreviewProvider 属于旧版,并未被编译器弃用。EditButton、.onDelete 和 .onMove 仍适用于编辑模式下的列表工作流;上下文行操作请使用 .swipeActions。

  17. 为了避免预览崩溃而将必需依赖改为可选——应安装确定性的预览依赖,并避免使用实时网络、身份验证、生产数据库或全局单例

审查清单

  • 共享状态模型使用 @Observable(iOS 17 及以上版本不使用 ObservableObject)
  • 对象由 @State 持有;通过 let/@Bindable 接收
  • 核实迁移与可用性声明是否符合当前平台支持情况,尤其是剪贴板和共享 API
  • 使用 NavigationStack(不使用 NavigationView)
  • 使用 .task 修饰符加载异步数据
  • 大型集合使用 LazyVStack/LazyHStack
  • 使用稳定的 Identifiable ID(不使用数组索引)
  • 提取视图时依据分支/布局、生命周期、依赖、预览或父级流程信号;小型无状态片段保留为计算属性
  • 扩展和 // MARK: 仅用于整理文件
  • 仅调整结构的重构须保留行为;使用精简的操作/生命周期方法,将可复用逻辑保留在服务/模型中,然后构建并渲染有用的预览
  • 预览覆盖有意义的已加载/加载中/空/错误状态,使用确定性测试数据,并包含所有必需的环境依赖
  • body 中不进行高开销计算
  • 深层共享状态使用 Environment
  • 语义化样式优于固定颜色时,使用 foregroundStyle(_:)
  • 重复样式使用自定义 ViewModifier
  • 优先使用 .sheet(item:),而不是 .sheet(isPresented:)
  • Sheet 自行管理操作,并在内部调用 dismiss()
  • 遵循 MV 模式——不使用不必要的视图模型
  • 绑定 UI 的 @Observable 存储和视图模型隔离到 @MainActor
  • 跨并发边界传递的模型类型符合 Sendable
  • 除非需要特定值,否则省略 Stack 的 spacing:(优先使用自适应默认值)

参考资料

评论

支持 Markdown