wake-ios/specs/refactor_spec.md

169 lines
12 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.

# Wake iOS 重构与性能优化规格说明
版本: v0.1
创建时间: 2025-09-08 15:41 +08
## 背景与目标
- 现状问题:代码组织结构一般、页面间切换卡顿(特别是在重动画/媒体加载/网络日志时)。
- 目标:
- 提升导航一致性与可维护性(仅保留顶层 NavigationStack + Router
- 降低页面切换卡顿(减少主线程压力、控制刷新频率、优化媒体与动画负载、收敛网络日志)。
- 推动 Feature-Oriented 结构与 MVVM降低视图体量与重绘范围。
## 架构调整总览
- 统一导航:顶层 `NavigationStack(path: $router.path)`(见 `wake/WakeApp.swift`),子页面不再嵌套 `NavigationView`;使用 `Router.shared.navigate/pop/popToRoot`
- MVVM优先对 `BlindBoxView` 引入 `BlindBoxViewModel`,将轮询、计时器、媒体预处理、会员信息等迁至 VM。
- 并发与取消:轮询改 `AsyncSequence`/`Task` 可取消;倒计时改 Combine/AsyncTimer统一在 `onDisappear`/路由变化处取消。
- 媒体与动画GIF 优先替换为 Lottie 或仅在可见态播放;模糊与缩放动画范围与时机控制;媒体元数据后台计算。
- 网络日志Debug 可控、限流Release 关闭大段打印;使用 `os_log/Logger` 分类。
- 工程结构Feature-Oriented`Core/``Features/*``SharedUI/`);延续 Theme/Typography/Spacing 设计系统。
## 导航设计规范
- 顶层:`WakeApp` 中唯一 `NavigationStack`;其它页面不使用 `NavigationView`
- 路由:统一通过 `Router.shared.navigate(to:)``Router.shared.pop()``Router.shared.popToRoot()`
- 返回按钮:子页面通过 `Router.shared.pop()` 而非 `presentationMode.dismiss()`
## BlindBox 模块重构要点
- `BlindBoxViewModel`@MainActor
- 状态:盲盒列表/单盒数据、会员信息、计时与轮询状态、媒体 URL/尺寸/播放器句柄。
- 行为:`loadBlindBox()``start/stopPolling()``startCountdown()``prepareVideo/Image()`、资源清理。
- 视图拆分:
- `BlindBoxHeader``BlindBoxAnimationArea`Loading/Ready/Opening/None`BlindBoxActionButton``BlindBoxScalingOverlay`
- 视图仅订阅少量 `@Published`,降低 body 重绘。
## 并发与轮询规范
- 轮询:使用 `Task { for await ... in pollSequence }` + `task.cancel()`,严禁无 cancel 的 `while + Task.sleep`
- 倒计时:优先 0.25s0.5s 频率;必要时“毫秒展示”不落地 state严格在主线程更新 UI 状态。
## 媒体与动画规范
- GIF -> Lottie 优先;若保留 GIF仅在可见态播放避免与大范围模糊+缩放并发。
- 媒体预热与尺寸探测走后台,回主线程赋值。
- 播放器生命周期集中管理,页面切换前暂停并释放。
## 网络日志策略
- Debug按需与限长打印错误优先可通过开关关闭详细日志。
- Release关闭大段请求/响应体打印。
## 工程结构规划(建议)
- `Core/``Utils/``Network/``Auth/``Router/``Theme/``Typography/`
- `Features/BlindBox/``View/``ViewModel/``Models/``API/``Components/`
- `Features/Subscribe/`:含 `CreditsInfoCard``PlanCompare`
- `Features/Memories/`
- `SharedUI/`Buttons、LottieView、SVGImage、SheetModal 等
## 实施计划(分阶段)
- 第一阶段12 天,先解卡顿):
1) 统一导航:移除子页面 `NavigationView`,使用顶层 `NavigationStack + Router`
2) 计时器降频0.250.5s;如非必要移除毫秒级显示。
3) GIF 限制播放或替换为 Lottie关/收敛网络大日志。
- 第二阶段24 天):
4) 为 `BlindBox` 引入 ViewModel迁移副作用与状态。
5) 轮询改为可取消的异步序列;媒体预热与尺寸探测后台化。
6) 视图拆分与体量控制。
- 第三阶段(持续):
7) 目录重组ViewModel 标注 `@MainActor`;保留 `os_signpost` 监测关键路径。
## 验收标准DoD
- 导航:仅顶层 `NavigationStack`;子页面无 `NavigationView`
- 性能:转场掉帧率明显下降;主界面进入/退出动画流畅。
- 结构:`BlindBoxView` < 300 主要状态/副作用位于 ViewModel
- 资源GIF 仅在可见时播放或替换为 Lottie网络日志按需输出
## 任务清单(同步 todo
- [x] nav-1 统一导航移除子页面 NavigationViewRouter 返回
- [x] mvvm-1 BlindBox 引入 ViewModel迁移逻辑
- [x] timer-1 计时器降频与取消
- [x] polling-1 轮询可取消化
- [x] media-1 媒体与动画优化GIF->Lottie/可见播放)
- [x] concurrency-1 @MainActor 与线程安全
- [x] netlog-1 网络日志开关与限流
- [ ] structure-1 目录重组(进行中)
- [ ] perf-1 性能埋点与基线(进行中)
## 进度记录(每次执行后更新)
- 2025-09-08 15:41 +08: 创建 specs 目录与本说明v0.1)。
- 2025-09-08 15:41 +08: 完成 nav-1第一步移除 `wake/View/Blind/BlindOutCome.swift``wake/View/Memories/MemoriesView.swift` 中的 `NavigationView`,改为使用 `Router.shared.pop()` 返回;依赖顶层 `NavigationStack`
- 2025-09-08 15:51 +08: 继续完成 nav-1
- 移除 `wake/View/Credits/CreditsDetailView.swift``wake/View/Welcome/SplashView.swift``wake/View/Owner/SettingsView.swift``wake/View/Components/Upload/MediaUpload.swift`(示例 `MediaUploadExample`)、`wake/View/Examples/MediaDemo.swift` 中的 `NavigationView`
-`wake/View/Feedback.swift``FeedbackView``FeedbackDetailView` 的返回行为从 `dismiss()` 统一为 `Router.shared.pop()`
- 保留预览Preview中的 `NavigationView`,运行时代码已全部依赖顶层 `NavigationStack + Router`
- 2025-09-08 16:10 +08: 完成 timer-1 与 netlog-1
- 倒计时更新频率由 0.1s 改为 1s移除毫秒级显示初始值设为 `(36, 50, 0)`,减少 UI 重绘(`wake/View/Blind/ContentView.swift`)。
- 网络日志:将详细请求/成功响应日志置于 `#if DEBUG`,错误响应体截断至约 300 字符401 刷新 Token 相关提示仅在 Debug 下打印(`wake/Utils/NetworkService.swift`)。
- 2025-09-08 16:28 +08: 完成 polling-1
- 新增 `wake/View/Blind/BlindBoxPolling.swift`,提供 `AsyncThrowingStream` 序列:`singleBox(boxId:)``firstUnopened()`,内部可取消(`Task.isCancelled`)并使用统一的间隔控制。
- `wake/View/Blind/ContentView.swift` 轮询改为 `for try await ... in` 形式;通过 `pollingTask` 管理任务,在 `stopPolling()``onDisappear` 中取消,避免视图中出现 `while + Task.sleep`
- 2025-09-08 18:05 +08: 推进 mvvm-1 与 media-1
- ViewModel`BlindBoxViewModel`)新增 `applyStatusSideEffects()`,将状态联动的副作用集中处理:`Preparing` 开始 1s 倒计时(`countdownText`),其它状态停止倒计时;在 `bootstrapInitialState()``startPolling()` 的数据落地后调用。
- 倒计时迁移至 VM新增 `startCountdown/stopCountdown``countdownText`,视图使用 `viewModel.countdownText` 展示;`ContentView` 移除本地倒计时与定时器。
- 首帧无闪烁:`ContentView` 初始 `animationPhase = .none`,监听 `viewModel.didBootstrap` 后按初始状态(大小写不敏感)切换 `loading/ready`;操作按钮在 `didBootstrap` 前隐藏。
- 媒体优化起步:将 `loading` 阶段的 GIF 替换为 Lottie`LottieView(name: "loading")`,资源位于 `wake/Assets/Lottie/loading.json`)。
- 2025-09-08 19:19 +08: 完成 media-1
- Loading/Ready/Opening 全部替换为 Lottie`loading.json`/`data.json`/`open.json`),并使用 `isPlaying` 仅在可见时播放Opening 使用 `.playOnce`
- 引入 `Perf` 工具类并在关键路径打点Appear、状态切换、开盒点击、开启动画以支持 perf-1。
- 2025-09-08 19:19 +08: 启动 structure-1进行中
- 创建目录骨架:`Core/``SharedUI/``Features/BlindBox/``Features/Subscribe/` 等,仅添加 README不改变构建后续在 Xcode 内移动文件以保持引用正确。
- 2025-09-09 11:26 +08: 推进 structure-1
- 完成盲盒批次文件迁移View/ViewModel/API/Models/Components
- 完成 SharedUI 与 Core 的第 13 步迁移:
- SharedUI`LottieView.swift``GIFView.swift``SVGImage.swift``SheetModal.swift` 已迁移至 `wake/SharedUI/...`(当前均放在 `Animation/` 分组,后续可按需细分 `Media/``Modals/`)。
- Core`Router.swift` 已迁至 `Core/Navigation/``NetworkService.swift` 已迁至 `Core/Network/``Theme.swift``Typography.swift` 已迁至 `Core/DesignSystem/`
- 待迁移:`Performance.swift``Core/Diagnostics/``APIConfig.swift``Core/Network/`
## structure-1 目录重构计划与迁移步骤
### 目标结构
- `wake/Core/`
- `DesignSystem/``Theme.swift``Typography.swift`
- `Navigation/``Router.swift`
- `Diagnostics/``Performance.swift`
- `Network/``NetworkService.swift``APIConfig.swift`、通用 ApiClient/*
- `wake/SharedUI/`
- `Animation/``LottieView.swift`
- `Media/``GIFView.swift``SVGImage.swift`/`SVGImageHtml.swift`
- `Modals/``SheetModal.swift` 等)
- `Controls/`(通用按钮、输入框等)
- `Graphics/`(共享图形资源包装)
- `wake/Features/BlindBox/`
- `View/``ContentView.swift` -> 建议更名 `BlindBoxView.swift`
- `ViewModel/``BlindBoxViewModel.swift`
- `API/``BlindBoxApi.swift``BlindBoxPolling.swift`
- `Models/``BlindModels.swift`
- `Components/`(与盲盒强相关的子视图)
- `wake/Features/Subscribe/`
- `Components/``CreditsInfoCard.swift``PlanCompare.swift` 等)
- `View/``SubscribeView.swift` 及其子视图)
### 迁移建议
1. 在 Xcode 的 Project Navigator 中先创建“Group without folder”形式的虚拟分组稳定编译再选择是否同步到磁盘。
2. 若需要调整磁盘目录:
- 建议在 Xcode 中使用拖拽将文件移动到对应 Group同时勾选“Move files”与“Add to targets”避免红色引用。
- 一次迁移一个 Feature迁移后立即编译验证。
3. 资源文件Lottie JSON/SVG 等)保持在 `wake/Assets/` 下;仅调整其使用方的源文件位置。
4. 网络与工具类尽量放入 `Core/`Feature 专用 API 可放在 `Features/<Feature>/API/`
### 第一批建议迁移(盲盒)
- `wake/View/Blind/ContentView.swift``wake/Features/BlindBox/View/BlindBoxView.swift`(建议改名)
- `wake/View/Blind/BlindBoxViewModel.swift``wake/Features/BlindBox/ViewModel/BlindBoxViewModel.swift`
- `wake/View/Blind/BlindBoxPolling.swift``wake/Features/BlindBox/API/BlindBoxPolling.swift`
- `wake/Utils/ApiClient/BlindBoxApi.swift``wake/Features/BlindBox/API/BlindBoxApi.swift`
- `wake/Models/BlindModels.swift``wake/Features/BlindBox/Models/BlindModels.swift`
### 公共组件迁移
- `wake/Components/Lottie/LottieView.swift``wake/SharedUI/Animation/LottieView.swift`
- `wake/Utils/GIFView.swift``wake/SharedUI/Media/GIFView.swift`
- `wake/Utils/SVGImage.swift``wake/Utils/SVGImageHtml.swift``wake/SharedUI/Media/`
- `wake/View/Components/SheetModal.swift``wake/SharedUI/Modals/SheetModal.swift`
- `wake/View/Components/Button.swift`/`Buttons/``wake/SharedUI/Controls/`
### 核心模块迁移
- `wake/Utils/Router.swift``wake/Core/Navigation/Router.swift`
- `wake/Utils/Performance.swift``wake/Core/Diagnostics/Performance.swift`
- `wake/Utils/NetworkService.swift``wake/Utils/APIConfig.swift``wake/Core/Network/`
- `wake/Theme.swift``wake/Typography.swift``wake/Core/DesignSystem/`
## 决策记录
- 采用顶层 `NavigationStack + Router`,子页面取消 `NavigationView`
- `BlindBox` 优先落地 MVVM 重构,其它模块随后跟进。