169 lines
12 KiB
Markdown
169 lines
12 KiB
Markdown
# 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.25s–0.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 等
|
||
|
||
## 实施计划(分阶段)
|
||
- 第一阶段(1–2 天,先解卡顿):
|
||
1) 统一导航:移除子页面 `NavigationView`,使用顶层 `NavigationStack + Router`。
|
||
2) 计时器降频:0.25–0.5s;如非必要移除毫秒级显示。
|
||
3) GIF 限制播放或替换为 Lottie;关/收敛网络大日志。
|
||
- 第二阶段(2–4 天):
|
||
4) 为 `BlindBox` 引入 ViewModel,迁移副作用与状态。
|
||
5) 轮询改为可取消的异步序列;媒体预热与尺寸探测后台化。
|
||
6) 视图拆分与体量控制。
|
||
- 第三阶段(持续):
|
||
7) 目录重组;ViewModel 标注 `@MainActor`;保留 `os_signpost` 监测关键路径。
|
||
|
||
## 验收标准(DoD)
|
||
- 导航:仅顶层 `NavigationStack`;子页面无 `NavigationView`。
|
||
- 性能:转场掉帧率明显下降;主界面进入/退出动画流畅。
|
||
- 结构:`BlindBoxView` < 300 行,主要状态/副作用位于 ViewModel。
|
||
- 资源:GIF 仅在可见时播放或替换为 Lottie;网络日志按需输出。
|
||
|
||
## 任务清单(同步 todo)
|
||
- [x] nav-1 统一导航(移除子页面 NavigationView,Router 返回)
|
||
- [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 的第 1–3 步迁移:
|
||
- 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 重构,其它模块随后跟进。
|