diff --git a/CLAUDE.md b/CLAUDE.md index 6fab860..068160a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,9 @@ ## 项目概览 -SwiftUI + MVVM 单向数据流 + SPM 多 target 模块化的 iOS「打卡/习惯追踪」客户端,架构对齐 Android NIA(`core/*` + `feature/*`)。 +SwiftUI + MVVM 单向数据流 + SPM 多 target 模块化的 iOS「打卡/计划管理」客户端,功能对齐 `wechat-clock-mini`(小程序,生产参照)与 `android-client`(Android 端,原生简化参照)。架构对齐 Android NIA(`core/*` + `feature/*`)。 + +四个主入口 Tab(顺序):**首页 / 打卡 / 计划 / 我的**;另有独立**登录页**(手机号 + 短信验证码)与**提交计划页**(计划 Tab 内推入)。 ## 目录结构 @@ -17,7 +19,11 @@ SwiftUI + MVVM 单向数据流 + SPM 多 target 模块化的 iOS「打卡/习惯 - `Sources/CoreDatabase`:SwiftData(`@Model` + `ModelContainer`) - `Sources/DesignSystem`:设计 tokens - `Sources/CoreUI`:可复用 SwiftUI 组件 - - `Sources/FeatureHome` / `Sources/FeatureAuth`:Feature 模块 + - `Sources/FeatureHome`:首页(落地页:Hero + 模块卡 + 产品介绍) + - `Sources/FeatureCheckin`:打卡(状态卡 + 状态 pill + 小组/个人记录) + - `Sources/FeaturePlan`:计划(周/月/年 + 小组统计 + 提交计划页) + - `Sources/FeatureProfile`:我的(资料 + 小组分组 + 退出登录) + - `Sources/FeatureAuth`:登录(手机号 + 短信验证码 + 60s 倒计时) ## 依赖方向(硬约束,违反即编译不过) @@ -44,7 +50,14 @@ SwiftUI + MVVM 单向数据流 + SPM 多 target 模块化的 iOS「打卡/习惯 | Hilt `@Inject` / AppContainer | 手写 `AppContainer` | | Retrofit + kotlinx.serialization | URLSession + Codable | | Room | SwiftData | -| DataStore | `@AppStorage` / UserDefaults | +| DataStore(会话持久化) | Combine `CurrentValueSubject`(进程内 Fake 会话)| + +## 会话 / 登录态 + +- `SessionRepository` 协议(CoreDomain)+ `FakeSessionRepository`(CoreRepository,`final class` + Combine `CurrentValueSubject`,默认未登录,**AppContainer 持单例共享**)。 +- 登录态驱动页面:`HomeViewModel` 双源切换(已登录 `observeHome()` / 游客 `observeGuestHome()`);checkin/plan/profile 四态含 `loggedOut`。 +- flatMapLatest 的 iOS 等价:外层 `Task` 观察 `observeIsLoggedIn()`,登录态变化时取消内层数据 `Task` 并重新订阅。 +- 登录 mock 凭据:手机号 `13812341234` + 验证码 `123456`。 ## 代码风格 @@ -79,3 +92,11 @@ swiftlint lint --strict 2. 新建 `Sources/FeatureXxx/`:Screen + ViewModel + UiState(enum)。 3. UseCase 放 `CoreDomain`,Repository 实现放 `CoreRepository`。 4. 在 `App` 的 `AppContainer` 装配依赖、`AppRootView` 接入导航。 + +## 文档地图 + +| 主题 | 文档 | +|---|---| +| 架构与数据流 | [docs/architecture.md](docs/architecture.md) | +| 工作状态与遗留项 | [docs/progress.md](docs/progress.md) | +| 贡献指南 | [CONTRIBUTING.md](CONTRIBUTING.md) | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21367bf..ff3077e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,11 +12,12 @@ - 跨模块符号 `public` + `///` 文档注释。 - 领域模型标记 `Sendable`。 - ViewModel:`@MainActor @Observable`,UiState 用 `enum`。 +- 登录态经 `SessionRepository` 驱动,不要在各页面各自维护登录态副本。 - 引入任何第三方依赖前,需在 PR 说明必要性(项目默认零第三方框架)。 ## 提交流程 -1. 从 `main` 新建分支:`feat/xxx`、`fix/xxx`。 +1. 从 `dev` 新建分支:`feat/xxx`、`fix/xxx`(简化 GitFlow:feature 从 `dev` 拉、merge 回 `dev`,`dev` 稳定后再 merge 到 `main`)。 2. 本地跑通: ```bash swiftlint lint diff --git a/README.md b/README.md index 0d80cef..5f8833c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # ios-client -SwiftUI + MVVM 单向数据流的 iOS「打卡/习惯追踪」客户端,采用 SPM 多 target 模块化,架构对齐 Android NIA(`core/*` + `feature/*`)。 +SwiftUI + MVVM 单向数据流的 iOS「打卡/计划管理」客户端,采用 SPM 多 target 模块化,架构对齐 Android NIA(`core/*` + `feature/*`),功能对齐 `wechat-clock-mini`(小程序)与 `android-client`(Android 端)。 + +## 功能 + +四个主入口 Tab:**首页 / 打卡 / 计划 / 我的**,另有独立**登录页**(手机号 + 短信验证码)与**提交计划页**。当前数据层为 Fake 实现(进程内 mock),尚未接真实后端。 ## 架构 @@ -10,7 +14,7 @@ App(组合根)──► feature/* ──► core/* └──► CoreRepository ──► CoreNetwork / CoreDatabase / CoreDomain ``` -依赖单向无环,由 SPM target 依赖天然强制(import 不到即编不过)。 +依赖单向无环,由 SPM target 依赖天然强制(import 不到即编不过)。详见 [docs/architecture.md](docs/architecture.md)。 ## 模块 @@ -22,10 +26,13 @@ App(组合根)──► feature/* ──► core/* | CoreRepository | Repository 实现(Fake / OfflineFirst)| | CoreNetwork | DTO + URLSession APIClient | | CoreDatabase | SwiftData(`@Model` + `ModelContainer`)| -| DesignSystem | 设计 tokens(颜色/间距/圆角)| +| DesignSystem | 设计 tokens(颜色/渐变/间距/圆角)| | CoreUI | 可复用 SwiftUI 组件 | -| FeatureHome | 首页(打卡列表)| -| FeatureAuth | 登录 / 我的(占位)| +| FeatureHome | 首页(落地页:Hero + 模块卡 + 产品介绍)| +| FeatureCheckin | 打卡(状态卡 + 状态 pill + 小组/个人记录)| +| FeaturePlan | 计划(周/月/年 + 小组统计 + 提交计划页)| +| FeatureProfile | 我的(资料 + 小组分组 + 退出登录)| +| FeatureAuth | 登录(手机号 + 短信验证码)| | App | 组合根(DI + 导航)| ## 环境要求 @@ -48,9 +55,9 @@ brew install swiftlint # 首次 swiftlint lint ``` -## 目录与协作规范 +## 文档与协作规范 -见 [CLAUDE.md](CLAUDE.md) 与 [CONTRIBUTING.md](CONTRIBUTING.md)。 +见 [CLAUDE.md](CLAUDE.md)、[docs/architecture.md](docs/architecture.md)、[docs/progress.md](docs/progress.md) 与 [CONTRIBUTING.md](CONTRIBUTING.md)。 ## License diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d77b871 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,51 @@ +# 架构 + +## 分层与依赖 + +依赖单向无环:`App → feature/* → core/*`,`App` 额外依赖 `CoreRepository` 装配具体实现。 + +``` +App ──► FeatureHome / FeatureCheckin / FeaturePlan / FeatureProfile / FeatureAuth + │ + └──► CoreDomain(协议 + UseCase)──► CoreModel + ▲ + └── CoreRepository(Fake* 实现) +``` + +- **CoreModel**:领域模型,`Sendable` 值类型,叶子不依赖任何模块。 +- **CoreDomain**:Repository 协议 + UseCase,纯 Swift(不 import SwiftUI / 数据框架)。 +- **CoreRepository**:Fake 实现(当前),未来替换为 OfflineFirst(编排 CoreNetwork / CoreDatabase)。 + +## 数据流(MVVM 单向) + +``` +Screen(.task) ──► ViewModel.load() ──► UseCase ──► Repository(AsyncThrowingStream) + ▲ │ + └────────── uiState(@Observable)◄──── yield +``` + +- ViewModel:`@MainActor @Observable final class`,`private(set) var uiState`。 +- UiState:`enum` 关联值(对应 Kotlin sealed interface)。 +- 每次 `load()` 先 `loadTask?.cancel()` 再开新 `Task`,实现 `flatMapLatest` 语义。 + +## 会话 / 登录态 + +`SessionRepository` 持进程内登录态(Fake 用 Combine `CurrentValueSubject`),`AppContainer` 持单例注入所有 ViewModel。 + +- 外层 `Task` 观察 `observeIsLoggedIn()`;登录态变化 → 取消内层数据 `Task` → 重新订阅。 +- **Home**:已登录 `observeHome()`(isGuest=false)/ 游客 `observeGuestHome()`(isGuest=true),无 `loggedOut` 态(游客落地页即未登录首页)。 +- **checkin / plan / profile**:登录态 false → `.loggedOut`;true → `.loading` → 数据。 + +## Feature 一览 + +| Feature | UiState | 数据源(Repository)| +|---|---|---| +| FeatureHome | loading / success(HomeData, isGuest) / error | observeHome / observeGuestHome | +| FeatureCheckin | loading / success(CheckinRecord) / loggedOut / error | observeCheckin + submitCheckin | +| FeaturePlan | loading / success(PlanRecord) / loggedOut / error | observePlan(+ observePlanSubmit / savePlan)| +| FeatureProfile | loading / success(UserProfile) / loggedOut / error | observeProfile | +| FeatureAuth | 表单态(isSendingCode / countdownSeconds / isSubmitting / errorMessage)| requestSmsCode / login | + +## 设计 tokens + +品牌色 / 状态色 / 渐变定义在 `DesignSystem/DesignTokens.swift`(`DS.Color` / `DS.Gradient`),对齐 wechat-clock-mini / android-client 的 designsystem。 diff --git a/docs/progress.md b/docs/progress.md new file mode 100644 index 0000000..8946ae4 --- /dev/null +++ b/docs/progress.md @@ -0,0 +1,31 @@ +# 工作状态 + +> 跨会话状态快照,变更后请更新。 + +## 当前状态 + +- ✅ 四主入口 Tab(首页 / 打卡 / 计划 / 我的)+ 登录页 + 提交计划页,全部 SwiftUI 实现。 +- ✅ 数据层为 Fake 实现(进程内 mock),对齐 android-client 的简化形态。 +- ✅ 登录态驱动:`SessionRepository`(Combine `CurrentValueSubject`)+ 各页 `loggedOut` 态。 +- ✅ `swift build` 与模拟器 `xcodebuild` 均通过。 + +## 已完成 + +- [x] CoreModel 领域模型(Home / Checkin / Plan / Profile / Auth-Session) +- [x] CoreDomain Repository 协议 + UseCase(Home / Checkin / Plan / Profile / Session) +- [x] CoreRepository Fake 实现(含可提交的 checkin / plan、会话) +- [x] FeatureHome(落地页)/ FeatureCheckin / FeaturePlan(+提交页)/ FeatureProfile / FeatureAuth +- [x] DesignSystem 品牌 tokens;AppContainer DI + AppRootView 导航(Tab + sheet 登录 + 推入提交页) + +## 下一步 + +1. 接入真实后端:CoreNetwork 定义 API + DTO(对应 wechat-backend `/api/v1/*`),CoreRepository 替换为 OfflineFirst。 +2. CoreDatabase 落地 SwiftData schema(打卡 / 计划 / 用户)。 +3. 真实图片上传(计划附件、头像)与隐私协议跳转页。 +4. 单元测试(ViewModel / UseCase / Fake 仓库)。 + +## 遗留项(暂缓) + +- 计划附件上传、头像上传仍是占位(与 android 一致)。 +- `webview`(H5)能力未引入,首页为纯原生落地页。 +- 未跑 `swiftlint`(本机未安装)。