Files
suixinkan_uikit/AGENTS.md
汉秋 6cc2336e7b Expand AGENTS.md with migration and development conventions.
Document SwiftUI-to-UIKit migration approach, third-party libraries, list patterns, unit testing requirements, and imperative UIKit style.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 14:50:14 +08:00

7.2 KiB
Raw Blame History

suixinkan

项目概览

这是一个 Swift + UIKit 的 iOS 工程,采用 MVVM 设计模式。

本工程的主要目标是将原 SwiftUI 项目迁移为 Swift + UIKit 实现。迁移时应按 UIKit idiomatic 方式重写,而不是把 SwiftUI 的声明式/响应式写法原样搬到 UIKit 上。

平台与设备

  • 最低系统版本iOS 16
  • 设备:仅支持 iPhoneTARGETED_DEVICE_FAMILY = 1
  • 屏幕方向仅竖屏Portrait

开发时请遵守以上约束,不要引入 iPad 专属布局、横屏适配或低于 iOS 16 的 API。

架构MVVM

按 MVVM 分层组织代码,职责清晰:

层级 职责
View UIViewController / 自定义 UIView,负责 UI 展示与用户交互,不包含业务逻辑
ViewModel 处理业务逻辑与状态通过方法、delegate 或明确的回调与 View 通信,不直接引用 UIKit 视图
Model 数据模型、网络/本地数据源

约定

  • View 通过 ViewModel 获取数据并响应用户操作,避免在 ViewController 中写复杂业务逻辑
  • ViewModel 不依赖 UIViewController 或具体 View 类型
  • 新增功能时,优先创建对应的 Model、ViewModel、ViewController文件保持结构一致
  • 新增功能时,同步添加单元测试(详见「单元测试」章节)

SwiftUI 迁移与开发风格

迁移过程中采用 传统命令式 开发,按 UIKit 原生模式组织代码,避免引入 SwiftUI 时代的响应式习惯。

不使用

  • @Observable / ObservableObject / @Published 等观察型状态机制
  • CombinePassthroughSubjectCurrentValueSubjectsink 等)驱动 UI 更新
  • 为模拟 SwiftUI 的 onChange 而堆砌大量属性监听回调

推荐做法

  • 命令式更新 UI:在明确的时机(网络回调、按钮点击、页面出现等)直接调用 apply(snapshot) 或更新对应子视图
  • 显式通信:优先 delegate、target-action、或带语义的单一回调onSubmit),避免「任意字段变化 → 触发 refresh」的泛化监听
  • 按场景刷新:数据变了就调对应刷新方法,而不是让 View 订阅一堆变更信号
  • 迁移时重写交互模型:对照 SwiftUI 页面的行为,用 UIKit 方式实现,而不是逐行翻译声明式语法
// ✅ 推荐:用户点击后显式加载并刷新
@objc private func didTapRefresh() {
    viewModel.loadItems { [weak self] items in
        self?.applyItems(items)
    }
}

// ❌ 避免:为迁移方便堆叠泛化 onChanged
viewModel.onItemsChanged = { [weak self] in self?.reload() }
viewModel.onLoadingChanged = { [weak self] in self?.reload() }
viewModel.onErrorChanged = { [weak self] in self?.reload() }
  • ViewModel 可暴露普通属性供读取,但 UI 更新由 View 在操作完成后的回调里主动触发,而非自动绑定

技术栈

  • 语言Swift
  • UI 框架UIKit
  • 布局SnapKit代码约束
  • 图片加载Kingfisher
  • 入口AppDelegateSceneDelegateMain.storyboard

第三方库约定

SnapKitUI 约束)

  • 使用 SnapKit 编写 Auto Layout 约束,优先代码布局,不使用 Storyboard 约束
  • 约束写在 setupUI() / setupConstraints() 等独立方法中,保持 viewDidLoad 简洁
  • 使用 snp.makeConstraints 设置约束,snp.updateConstraints / snp.remakeConstraints 处理动态布局
imageView.snp.makeConstraints { make in
    make.top.equalTo(view.safeAreaLayoutGuide).offset(16)
    make.leading.trailing.equalToSuperview().inset(16)
    make.height.equalTo(200)
}

Kingfisher网络图片

  • 使用 Kingfisher 加载与缓存网络图片,不要手写 URLSession 下载图片
  • 通过 UIImageView.kf.setImage(with:) 加载,按需配置 placeholder、缓存策略
imageView.kf.setImage(
    with: URL(string: imageURL),
    placeholder: UIImage(named: "placeholder")
)
  • 列表/滚动场景中利用 Kingfisher 内置缓存,避免重复请求
  • Cell 复用时无需手动取消任务Kingfisher 会自动处理

列表展示

根据列表复杂度选择合适的容器,数据更新优先使用 Diffable Data Source,让增删改带有过渡动画。

选型

场景 推荐 说明
简单列表 UITableView 单行、结构统一的列表(设置项、纯文本列表等)
复杂列表 UICollectionView 多列、网格、卡片、不等高、混合 Section 布局等
  • 简单场景不必强行上 UICollectionView;复杂布局不要硬用 UITableView 变通
  • UICollectionView 配合 UICollectionViewCompositionalLayout 实现灵活布局

Diff 更新与动画

  • 使用 UITableViewDiffableDataSource / UICollectionViewDiffableDataSource 管理数据源
  • 数据变化时通过 NSDiffableDataSourceSnapshot 描述差异,调用 apply(_:animatingDifferences:) 刷新
  • 默认开启动画animatingDifferences: true),插入、删除、移动应有平滑过渡
  • 避免 reloadData() 全量刷新;仅在 diff 无法表达的场景(如整体替换且无需动画)才考虑全量刷新
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)
  • Item 需遵循 Hashable,便于系统计算 diff
  • ViewModel 输出新数据后,由 View 层构建 snapshot 并 apply保持职责分离

单元测试

每添加一个功能,必须编写对应的单元测试,并确保全部通过后再视为完成。

要求

  • 新功能与单元测试同一次改动中提交,不单独留「后续补测」
  • 测试需在本地运行通过Xcode Cmd + U,或 xcodebuild test
  • 功能有 bug 修复时,优先补充或更新相关测试用例

测试范围

层级 是否测试 说明
ViewModel 优先 业务逻辑、状态变更、数据转换
Model 优先 解析、校验、纯函数逻辑
View 按需 复杂交互或自定义 View 行为;简单 UI 可不测
  • 测试应独立、可重复,不依赖真实网络或外部服务(使用 mock / stub
  • 测试文件放在 suixinkanTests/,命名与被测类型对应,如 FooViewModelTests.swift

示例

import XCTest
@testable import suixinkan

final class FooViewModelTests: XCTestCase {

    func testLoadDataSuccess() {
        let viewModel = FooViewModel(service: MockFooService())
        viewModel.load()
        XCTAssertEqual(viewModel.items.count, 3)
    }
}

开发注意

  • 使用 iOS 16+ 可用 API如需兼容更低版本需先与项目维护者确认
  • UI 按竖屏 iPhone 设计,无需考虑横屏与 iPad 分屏
  • 布局统一用 SnapKit网络图片统一用 Kingfisher
  • 简单列表用 UITableView,复杂列表优先 UICollectionView;数据更新用 Diffable Data Source 并带动画
  • SwiftUI → UIKit 迁移:命令式开发,不用 Observable / Combine不堆 onChanged 式回调
  • 新增功能必须附带单元测试,且全部通过
  • 保持代码简洁,遵循现有命名与目录风格