添加 iOS 基础架构与 SPM 依赖
This commit is contained in:
184
AGENTS.md
184
AGENTS.md
@ -2,181 +2,67 @@
|
||||
|
||||
## 项目概览
|
||||
|
||||
这是一个 **Swift + UIKit** 的 iOS 工程,采用 **MVVM** 设计模式。
|
||||
**Swift + UIKit** 工程,**MVVM** 架构。iOS 版 **随心瞰商家版**,与 Android **ZhiFlyFollow** 功能对齐——以 Android 工程为参考同步实现业务能力,UI 按 UIKit 习惯编写,不逐行翻译 Compose。
|
||||
|
||||
本工程的主要目标是将原 **SwiftUI** 项目迁移为 **Swift + UIKit** 实现。迁移时应按 UIKit idiomatic 方式重写,而不是把 SwiftUI 的声明式/响应式写法原样搬到 UIKit 上。
|
||||
**Android 参考:** `/Users/hanqiu/Desktop/android_project/zhiflyfollow`(`com.zhifly.follow`)
|
||||
**文档:** `CLAUDE.md`、`docs/home/`、`docs/排队叫号业务流程梳理.md`
|
||||
**后端:** 生产 `api.zhifly.cn`,测试 `api-test.zhifly.cn`
|
||||
|
||||
**主要功能域:** 首页/订单/数据/我的 Tab;登录注册与实名认证;订单/任务/核销;相册与云存储;直播与钱包;景区排队叫号、位置上报;消息、设置、扫码、语音通话等。
|
||||
|
||||
**同步约定:** 接口与业务流程以 Android `NetworkApi` / DTO / ViewModel 为准;UI 用 SnapKit 独立设计;OTG 相机、DJI 飞控、厂商推送等 Android 专属能力按需评估。
|
||||
|
||||
## 平台与设备
|
||||
|
||||
- **最低系统版本**:iOS 16
|
||||
- **设备**:仅支持 iPhone(`TARGETED_DEVICE_FAMILY = 1`)
|
||||
- **屏幕方向**:仅竖屏(Portrait)
|
||||
|
||||
开发时请遵守以上约束,不要引入 iPad 专属布局、横屏适配或低于 iOS 16 的 API。
|
||||
iOS 16+ · 仅 iPhone · 仅竖屏。不做 iPad / 横屏 / 低版本 API 适配。
|
||||
|
||||
## 架构(MVVM)
|
||||
|
||||
按 MVVM 分层组织代码,职责清晰:
|
||||
|
||||
| 层级 | 职责 |
|
||||
|------|------|
|
||||
| **View** | `UIViewController` / 自定义 `UIView`,负责 UI 展示与用户交互,不包含业务逻辑 |
|
||||
| **ViewModel** | 处理业务逻辑与状态;通过方法、delegate 或明确的回调与 View 通信,不直接引用 UIKit 视图 |
|
||||
| **View** | `UIViewController` / 自定义 `UIView`,UI 展示与交互,不含业务逻辑 |
|
||||
| **ViewModel** | 业务逻辑与状态;通过方法、delegate 或明确回调与 View 通信 |
|
||||
| **Model** | 数据模型、网络/本地数据源 |
|
||||
|
||||
### 约定
|
||||
- 新增功能:对应 Model + ViewModel + ViewController,同步单元测试
|
||||
- ViewModel 不依赖 UIKit 视图类型
|
||||
|
||||
- View 通过 ViewModel 获取数据并响应用户操作,避免在 ViewController 中写复杂业务逻辑
|
||||
- ViewModel 不依赖 `UIViewController` 或具体 View 类型
|
||||
- 新增功能时,优先创建对应的 Model、ViewModel、View(Controller)文件,保持结构一致
|
||||
- 新增功能时,同步添加单元测试(详见「单元测试」章节)
|
||||
## 开发风格
|
||||
|
||||
## SwiftUI 迁移与开发风格
|
||||
**命令式 UIKit**,不用 `@Observable` / `ObservableObject` / `@Published`、不用 Combine 驱动 UI、不堆泛化 `onChanged` 回调。
|
||||
|
||||
迁移过程中采用 **传统命令式** 开发,按 UIKit 原生模式组织代码,避免引入 SwiftUI 时代的响应式习惯。
|
||||
- 在明确时机(点击、网络回调、页面出现)主动刷新 UI(`apply(snapshot)` 或更新子视图)
|
||||
- 通信用 delegate、target-action 或语义明确的单一回调
|
||||
- 对照 Android **行为** 用 UIKit 重写,不翻译 Compose 语法
|
||||
- ViewModel 属性供读取,UI 更新由 View 在回调中主动触发
|
||||
|
||||
### 不使用
|
||||
## 代码注释
|
||||
|
||||
- `@Observable` / `ObservableObject` / `@Published` 等观察型状态机制
|
||||
- **Combine**(`PassthroughSubject`、`CurrentValueSubject`、`sink` 等)驱动 UI 更新
|
||||
- 为模拟 SwiftUI 的 `onChange` 而堆砌大量属性监听回调
|
||||
`class` / `struct` / `enum`、协议及公开 API 必须写 `///` 文档注释,说明职责与用途;复杂内部逻辑按需补充。不写废话注释、不写过时注释。
|
||||
|
||||
### 推荐做法
|
||||
## 技术栈与库
|
||||
|
||||
- **命令式更新 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`
|
||||
|
||||
## 第三方库约定
|
||||
|
||||
### SnapKit(UI 约束)
|
||||
|
||||
- 使用 **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 会自动处理
|
||||
Swift · UIKit · SnapKit(代码布局,约束写在 `setupUI()` / `setupConstraints()`)· Kingfisher(`kf.setImage(with:)` 加载网络图,列表利用缓存)
|
||||
|
||||
## 列表展示
|
||||
|
||||
根据列表复杂度选择合适的容器,数据更新优先使用 **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,保持职责分离
|
||||
- 简单列表 → `UITableView`;复杂列表(网格、卡片、不等高)→ `UICollectionView` + Compositional Layout
|
||||
- 数据更新用 Diffable Data Source + `apply(_:animatingDifferences: true)`,Item 遵循 `Hashable`,避免 `reloadData()`
|
||||
- ViewModel 提供数据,View 构建 snapshot 并 apply
|
||||
|
||||
## 单元测试
|
||||
|
||||
每添加一个功能,**必须**编写对应的单元测试,并确保全部通过后再视为完成。
|
||||
每增一功能必须附带单元测试且全部通过(`Cmd + U` / `xcodebuild test`),与功能同次提交。
|
||||
|
||||
### 要求
|
||||
| 层级 | 优先级 |
|
||||
|------|--------|
|
||||
| ViewModel / Model | ✅ 优先 |
|
||||
| View | 按需 |
|
||||
|
||||
- 新功能与单元测试同一次改动中提交,不单独留「后续补测」
|
||||
- 测试需在本地运行通过(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)
|
||||
}
|
||||
}
|
||||
```
|
||||
测试放 `suixinkanTests/`,独立可重复,mock 网络与外部依赖。
|
||||
|
||||
## 开发注意
|
||||
|
||||
- 使用 iOS 16+ 可用 API;如需兼容更低版本,需先与项目维护者确认
|
||||
- UI 按竖屏 iPhone 设计,无需考虑横屏与 iPad 分屏
|
||||
- 布局统一用 SnapKit,网络图片统一用 Kingfisher
|
||||
- 简单列表用 `UITableView`,复杂列表优先 `UICollectionView`;数据更新用 Diffable Data Source 并带动画
|
||||
- **SwiftUI → UIKit 迁移**:命令式开发,不用 Observable / Combine,不堆 `onChanged` 式回调
|
||||
- **新增功能必须附带单元测试,且全部通过**
|
||||
- 保持代码简洁,遵循现有命名与目录风格
|
||||
- SnapKit 布局、Kingfisher 图片、Diffable 列表带动画
|
||||
- 以 Android 工程为功能参考,命令式 UIKit,不用 Observable / Combine
|
||||
- 新增功能必测、类型与公开 API 必注释、保持简洁
|
||||
|
||||
Reference in New Issue
Block a user