12 KiB
12 KiB
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 天,先解卡顿):
- 统一导航:移除子页面
NavigationView,使用顶层NavigationStack + Router。 - 计时器降频:0.25–0.5s;如非必要移除毫秒级显示。
- GIF 限制播放或替换为 Lottie;关/收敛网络大日志。
- 统一导航:移除子页面
- 第二阶段(2–4 天):
4) 为
BlindBox引入 ViewModel,迁移副作用与状态。 5) 轮询改为可取消的异步序列;媒体预热与尺寸探测后台化。 6) 视图拆分与体量控制。 - 第三阶段(持续):
7) 目录重组;ViewModel 标注
@MainActor;保留os_signpost监测关键路径。
验收标准(DoD)
- 导航:仅顶层
NavigationStack;子页面无NavigationView。 - 性能:转场掉帧率明显下降;主界面进入/退出动画流畅。
- 结构:
BlindBoxView< 300 行,主要状态/副作用位于 ViewModel。 - 资源:GIF 仅在可见时播放或替换为 Lottie;网络日志按需输出。
任务清单(同步 todo)
- nav-1 统一导航(移除子页面 NavigationView,Router 返回)
- mvvm-1 BlindBox 引入 ViewModel,迁移逻辑
- timer-1 计时器降频与取消
- polling-1 轮询可取消化
- media-1 媒体与动画优化(GIF->Lottie/可见播放)
- concurrency-1 @MainActor 与线程安全
- 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)。
- 倒计时更新频率由 0.1s 改为 1s,移除毫秒级显示;初始值设为
-
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)。
- ViewModel(
-
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。
- Loading/Ready/Opening 全部替换为 Lottie(
-
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/。
- SharedUI:
-
2025-09-09 11:30 +08: 更新 structure-1:
- 已完成
Performance.swift→Core/Diagnostics/,APIConfig.swift→Core/Network/。 - 已完成
SVGImageHtml.swift→SharedUI/Media/。 - 待优化(可选):将
GIFView.swift、SVGImage.swift从SharedUI/Animation/细分到SharedUI/Media/;将SheetModal.swift从SharedUI/Animation/移至SharedUI/Modals/。
- 已完成
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及其子视图)
迁移建议
- 在 Xcode 的 Project Navigator 中,先创建“Group without folder”形式的虚拟分组,稳定编译;再选择是否同步到磁盘。
- 若需要调整磁盘目录:
- 建议在 Xcode 中使用拖拽将文件移动到对应 Group,同时勾选“Move files”与“Add to targets”,避免红色引用。
- 一次迁移一个 Feature,迁移后立即编译验证。
- 资源文件(Lottie JSON/SVG 等)保持在
wake/Assets/下;仅调整其使用方的源文件位置。 - 网络与工具类尽量放入
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.swiftwake/View/Blind/BlindBoxPolling.swift→wake/Features/BlindBox/API/BlindBoxPolling.swiftwake/Utils/ApiClient/BlindBoxApi.swift→wake/Features/BlindBox/API/BlindBoxApi.swiftwake/Models/BlindModels.swift→wake/Features/BlindBox/Models/BlindModels.swift
公共组件迁移
wake/Components/Lottie/LottieView.swift→wake/SharedUI/Animation/LottieView.swiftwake/Utils/GIFView.swift→wake/SharedUI/Media/GIFView.swiftwake/Utils/SVGImage.swift与wake/Utils/SVGImageHtml.swift→wake/SharedUI/Media/wake/View/Components/SheetModal.swift→wake/SharedUI/Modals/SheetModal.swiftwake/View/Components/Button.swift/Buttons/→wake/SharedUI/Controls/
核心模块迁移
wake/Utils/Router.swift→wake/Core/Navigation/Router.swiftwake/Utils/Performance.swift→wake/Core/Diagnostics/Performance.swiftwake/Utils/NetworkService.swift、wake/Utils/APIConfig.swift→wake/Core/Network/wake/Theme.swift、wake/Typography.swift→wake/Core/DesignSystem/
决策记录
- 采用顶层
NavigationStack + Router,子页面取消NavigationView。 BlindBox优先落地 MVVM 重构,其它模块随后跟进。