Files
suixinkan_uikit/AGENTS.md

183 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# suixinkan
## 项目概览
这是一个 **Swift + UIKit** 的 iOS 工程,采用 **MVVM** 设计模式。
本工程的主要目标是将原 **SwiftUI** 项目迁移为 **Swift + UIKit** 实现。迁移时应按 UIKit idiomatic 方式重写,而不是把 SwiftUI 的声明式/响应式写法原样搬到 UIKit 上。
## 平台与设备
- **最低系统版本**iOS 16
- **设备**:仅支持 iPhone`TARGETED_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` 等观察型状态机制
- **Combine**`PassthroughSubject``CurrentValueSubject``sink` 等)驱动 UI 更新
- 为模拟 SwiftUI 的 `onChange` 而堆砌大量属性监听回调
### 推荐做法
- **命令式更新 UI**:在明确的时机(网络回调、按钮点击、页面出现等)直接调用 `apply(snapshot)` 或更新对应子视图
- **显式通信**:优先 `delegate`、target-action、或带语义的单一回调`onSubmit`),避免「任意字段变化 → 触发 refresh」的泛化监听
- **按场景刷新**:数据变了就调对应刷新方法,而不是让 View 订阅一堆变更信号
- **迁移时重写交互模型**:对照 SwiftUI 页面的*行为*,用 UIKit 方式实现,而不是逐行翻译声明式语法
```swift
// ✅ 推荐:用户点击后显式加载并刷新
@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](https://github.com/onevcat/Kingfisher)
- **入口**`AppDelegate``SceneDelegate``Main.storyboard`
## 第三方库约定
### SnapKitUI 约束)
- 使用 **SnapKit** 编写 Auto Layout 约束,优先代码布局,不使用 Storyboard 约束
- 约束写在 `setupUI()` / `setupConstraints()` 等独立方法中,保持 `viewDidLoad` 简洁
- 使用 `snp.makeConstraints` 设置约束,`snp.updateConstraints` / `snp.remakeConstraints` 处理动态布局
```swift
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、缓存策略
```swift
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 无法表达的场景(如整体替换且无需动画)才考虑全量刷新
```swift
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`
### 示例
```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` 式回调
- **新增功能必须附带单元测试,且全部通过**
- 保持代码简洁,遵循现有命名与目录风格