mcpField
S
swiftui-view-refactor

swiftui-view-refactor

github.com/openai开发7.8k118 次安装2 天前更新
代码仓库

关于 swiftui-view-refactor

一项将 SwiftUI 视图重构为更小、更明确类型的技能,并指导将业务逻辑留在视图之外。内容涵盖状态管理、子视图、稳定视图结构和 Observation 用法。

主要功能

  1. 01视图属性排序
  2. 02MV 优先的状态管理
  3. 03提取独立子视图
  4. 04将操作移出 body
  5. 05保持视图树稳定
  6. 06Observation 包装器建议

适用场景

  1. 01将较长的 SwiftUI 页面拆分为子视图
  2. 02从视图 body 中移出业务逻辑
  3. 03重构使用可选视图模型的视图

安装

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

$npx skills add https://github.com/openai/plugins --skill swiftui-view-refactor

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

附带文件agents/openai.yamlreferences/mv-patterns.md

详细介绍

SwiftUI 视图重构

概述

重构 SwiftUI 视图,使其成为小巧、明确且稳定的视图类型。默认采用原生 SwiftUI 方案:在视图中管理本地状态,通过环境共享依赖项,将业务逻辑放在服务/模型中,并且仅在请求或现有代码明确需要时使用视图模型。

核心指南

1) 视图中的排列顺序(从上到下)
  • 除非现有文件有必须保留的、更强的局部约定,否则请遵循此顺序。
  • 环境
  • private/public let
  • @State/其他存储属性
  • 计算属性 var(非视图)
  • init
  • body
  • 计算视图构建器/其他视图辅助项
  • 辅助函数/异步函数
2) 默认采用 MV,而非 MVVM
  • 视图应是轻量的状态表达和协调点,而不是业务逻辑的容器。
  • 优先使用 @State、@Environment、@Query、.task、.task(id:) 和 onChange,之后再考虑视图模型。
  • 通过 @Environment 注入服务和共享模型;将领域逻辑放在服务/模型中,而不是视图的 body 中。
  • 不要仅仅为了映射本地视图状态或封装环境依赖项而引入视图模型。
  • 如果屏幕变得庞大,应先将 UI 拆分为子视图,再考虑创建新的视图模型层。
3) 强烈建议使用专用子视图类型,而非计算属性 some View 辅助项
  • 标记长度超过大约一屏或包含多个逻辑部分的 body 属性。
  • 对于非简单部分,优先提取专用的 View 类型,尤其是包含状态、异步工作、分支逻辑或值得拥有单独预览的部分。
  • 尽量少用计算属性 some View 辅助项,并保持其简短。不要用 private var header: some View 这类片段构建整个屏幕。
  • 将少量、明确的输入(数据、绑定、回调)传递给提取出的子视图,而不是传递整个父视图状态。
  • 如果提取出的子视图变得可复用或具有独立意义,应将其移至单独的文件。

优先采用:

var body: some View {
    List {
        HeaderSection(title: title, subtitle: subtitle)
        FilterSection(
            filterOptions: filterOptions,
            selectedFilter: $selectedFilter
        )
        ResultsSection(items: filteredItems)
        FooterSection()
    }
}

private struct HeaderSection: View {
    let title: String
    let subtitle: String

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(title).font(.title2)
            Text(subtitle).font(.subheadline)
        }
    }
}

private struct FilterSection: View {
    let filterOptions: [FilterOption]
    @Binding var selectedFilter: FilterOption

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(filterOptions, id: \.self) { option in
                    FilterChip(option: option, isSelected: option == selectedFilter)
                        .onTapGesture { selectedFilter = option }
                }
            }
        }
    }
}

避免:

var body: some View {
    List {
        header
        filters
        results
        footer
    }
}

private var header: some View {
    VStack(alignment: .leading, spacing: 6) {
        Text(title).font(.title2)
        Text(subtitle).font(.subheadline)
    }
}
3b) 将操作和副作用从 body 中提取出来
  • 不要将复杂的按钮操作内联在视图的 body 中。
  • 不要将业务逻辑藏在 .task、.onAppear、.onChange 或 .refreshable 中。
  • 优先从视图调用简短的私有方法,并将真正的业务逻辑移至服务/模型中。
  • body 应呈现 UI,而不是视图控制器。
Button("Save", action: save)
    .disabled(isSaving)

.task(id: searchText) {
    await reload(for: searchText)
}

private func save() {
    Task { await saveAsync() }
}

private func reload(for searchText: String) async {
    guard !searchText.isEmpty else {
        results = []
        return
    }
    await searchService.search(searchText)
}
4) 保持视图树稳定(避免在顶层切换条件视图)
  • 避免让 body 或计算视图通过 if/else 返回完全不同的根分支。
  • 优先采用单一且稳定的基础视图,并在各个部分/修饰符(overlay、opacity、disabled、toolbar 等)中处理条件。
  • 在根级别切换分支会导致标识变化、扩大失效范围并增加重新计算。

优先采用:

var body: some View {
    List {
        documentsListContent
    }
    .toolbar {
        if canEdit {
            editToolbar
        }
    }
}

避免:

var documentsListView: some View {
    if canEdit {
        editableDocumentsList
    } else {
        readOnlyDocumentsList
    }
}
5) 视图模型处理(仅限已存在或明确要求的情况)
  • 将视图模型视为旧有模式或有明确需求时才采用的模式,而非默认做法。
  • 除非请求或现有代码明确需要,否则不要引入视图模型。
  • 如果已有视图模型,尽可能将其设为非可选。
  • 通过视图的 init 传入依赖项,然后在视图的 init 中创建视图模型。
  • 避免使用 bootstrapIfNeeded 模式和其他延迟设置的变通方案。

示例(基于 Observation):

@State private var viewModel: SomeViewModel

init(dependency: Dependency) {
    _viewModel = State(initialValue: SomeViewModel(dependency: dependency))
}
6) Observation 的用法
  • 对于 iOS 17 及更高版本上的 @Observable 引用类型,在所属视图中将其存储为 @State。
  • 显式向下传递可观察对象;除非 UI 确实需要,否则避免使用可选状态。
  • 如果部署目标包含 iOS 16 或更早版本,则在所属视图中使用 @StateObject,并在注入旧版可观察模型时使用 @ObservedObject。

工作流

  1. 按照排序规则重新排列视图。
  2. 从 body 中移除内联操作和副作用;将业务逻辑移入服务/模型,视图中只保留精简的协调逻辑。
  3. 将较长的 body 拆分为专用子视图类型;避免通过许多计算属性 some View 辅助方法重建屏幕。
  4. 确保视图结构稳定:避免在顶层通过 if 切换分支;将条件移至局部区域或修饰符中。
  5. 如果存在视图模型或明确要求使用视图模型,请将可选视图模型替换为非可选的 @State 视图模型,并在 init 中初始化。
  6. 确认 Observation 的用法:在 iOS 17 及更高版本中,根 @Observable 模型使用 @State;只有在部署目标要求时才使用旧式包装器。
  7. 保持行为不变:除非收到要求,否则不要更改布局或业务逻辑。

说明

  • 优先使用小巧、明确的视图类型,避免使用大型条件代码块和大型计算属性 some View。
  • 将计算视图构建器放在 body 之后,将非视图计算变量放在 init 之前。
  • 优秀的 SwiftUI 重构应让视图从上到下呈现数据流和布局,而不是混杂布局与命令式逻辑。
  • 关于以 MV 为先的指导和原因,请参阅 references/mv-patterns.md。
  • 除上述参考资料外,当 SwiftUI API、Observation 行为或平台指导可能发生变化时,请使用网络搜索查阅最新的 Apple Developer 文档。

大型视图的处理

当 SwiftUI 视图文件超过约 300 行时,应积极拆分。将有意义的区域提取为专用 View 类型,而不是将复杂逻辑藏在多个计算属性中。可以使用带有 // MARK: - 注释的 private 扩展来组织操作和辅助方法,但不要用扩展代替将大型屏幕拆分为较小的视图类型。如果提取的子视图会被复用或本身具有独立意义,请将其移至单独的文件中。

评论

支持 Markdown