swiftui-patterns

swiftui-patterns

热门

构建和审查采用现代MV架构、状态管理、组合、隔离预览和迁移指导的SwiftUI视图。涵盖@Observable所有权、@State/@Bindable/@Environment连接、视图分解、ViewModifier、环境值、.task加载、iOS 26+交接、Writing Tools、剪贴板可用性和性能。适用于组织SwiftUI状态、管理@Observable、组合视图、预览有意义的UI状态或纠正SwiftUI模式。

922Star
46Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
swiftui-patterns
description

构建和审查采用现代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。

目录

范围边界: 本技能涵盖架构、状态所有权、组合、环境连接、异步加载以及相关的SwiftUI应用结构模式。详细的导航模式在swiftui-navigation技能中涵盖,包括NavigationStackNavigationSplitView、sheet、标签页和深度链接模式。详细的布局、容器和组件模式在swiftui-layout-components技能中涵盖,包括栈、网格、列表、滚动视图模式、表单、控件、使用.searchable的搜索UI、覆盖层及相关布局组件。详细的动画编排在swiftui-animation中涵盖。Liquid Glass采用、自定义玻璃控件、滚动边缘效果、.scrollEdgeEffectStyle.backgroundExtensionEffectswiftui-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("错误", 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@ObservedObjectlet@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-animationTextEditor(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

性能指南

  • 惰性栈/网格: 对于大型集合,使用LazyVStackLazyHStackLazyVGridLazyHGrid。常规栈会立即渲染所有子视图。
  • 稳定ID: List/ForEach中的所有项目必须符合Identifiable并具有稳定ID。切勿使用数组索引。
  • 避免body重新计算: 将过滤和排序移动到计算属性或模型中,而不是内联在body中。
  • Equatable视图: 对于不必要重新渲染的复杂视图,使其符合Equatable

HIG对齐

遵循Apple人机界面指南进行布局、排版、颜色和可访问性。关键规则:

  • 使用语义颜色(Color.primary.secondaryColor(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以推迟验证或暂停撤消分组,直到重写完成。

文档: 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确实需要异构视图存储,否则使用@ViewBuilderGroup或泛型。请参见references/deprecated-migration.md

  13. @AppStorage放在@Observable类内部。 @AppStorage是一个视图DynamicProperty;将其保留在View中,或在模型中暴露一个由UserDefaults支持的常规观察属性。

  14. 在每个栈上硬编码spacing:——省略它以获得自适应平台间距;仅在值是有意指定时指定

  15. .copyable.cuttable或基于命令的.pasteDestination(for:action:validator:)视为iOS 16/iOS 26 API——它们在当前的Apple文档中是macOS 13+和iOS/iPadOS/Mac Catalyst 27 beta。对于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中没有大量计算
  • [ ] 深度共享状态使用环境
  • [ ] 当语义样式优于固定颜色时使用foregroundStyle(_:)
  • [ ] 重复样式使用自定义ViewModifier
  • [ ] 优先使用.sheet(item:)而不是.sheet(isPresented:)
  • [ ] Sheet拥有自己的操作并在内部调用dismiss()
  • [ ] 遵循MV模式——没有不必要的视图模型
  • [ ] UI绑定的@Observable存储和视图模型是@MainActor隔离的
  • [ ] 跨并发边界传递的模型类型是Sendable
  • [ ] 栈的spacing:省略,除非需要特定值(优先使用自适应默认值)

参考资料