mcpField
S
swiftui-view-refactor

swiftui-view-refactor

github.com/dimillian开发4.0k2.3k 次安装30 周前更新
代码仓库

关于 swiftui-view-refactor

swiftui-view-refactor 为 SwiftUI 视图重构提供规则和工作流,适用于整理复杂界面并保持既有行为。它倡导小型明确的子视图、轻量状态表达,以及将业务逻辑移出视图主体。

主要功能

  1. 01视图属性顺序规范
  2. 02优先使用原生 SwiftUI 状态
  3. 03拆分专用子视图
  4. 04将副作用移出 body
  5. 05保持稳定的视图结构

适用场景

  1. 01拆分包含多个区块的大型视图
  2. 02整理视图中的业务逻辑和操作
  3. 03调整状态模型与依赖注入方式

安装

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

$npx skills add https://github.com/dimillian/skills --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 视图文件超过约 300 行时,应积极拆分。将有意义的区域提取为专用的 View 类型,而不是用许多计算属性隐藏复杂性。对操作和辅助方法使用带有 // MARK: - 注释的 private 扩展,但不要把扩展当作将大型屏幕拆分为较小视图类型的替代方案。如果提取出的子视图会被复用或本身具有独立意义,请将其移到单独的文件中。

评论

支持 Markdown