001project_wildgrowth/ios/WildGrowth/COMPLETION_NAVIGATION_审查报告.md

7.6 KiB
Raw Blame History

完成页导航方案 — 审查报告

审查对象:基于「零副作用 / SSOT / 响应式导航 / 原生手感」的完成页与播放器导航改造方案
审查结论:只做审查,不修改代码;本报告供决策与后续实现参考。


一、方案目标与标准(引用)

标准 含义
零副作用 不引入「假页面」污染数据源,无 Hack
单一事实来源 (SSOT) 数据源 allCourseNodes 仅含真实课程章节
响应式导航 NavigationPath 堆栈精确控制流向,消除白屏回退
原生手感 用手势识别(非页面索引)触发跳转

二、方案要点摘要

  1. 删除 CompletionPlaceholderPage 的导航职责(或整页删除,视实现而定)

    • 若保留:仅作纯视觉占位,无 onAppear 等导航逻辑。
  2. VerticalScreenPlayerView

    • 最后一节挂载「边缘左滑手势」修饰符(如 LastPageSwipeModifier)。
    • 左滑触发:navigationPath.append(CourseNavigation.completion(...)),或先设 currentNodeId = "wg://completion" 再由现有 onChange 统一 push。
    • TabView 仅渲染真实 allCourseNodes,不把「完成」当作一节数据。
  3. CompletionView

    • 新增 @Binding var navigationPath: NavigationPath
    • 「回到我的内容」/「继续学习」调用 handleReturnToMap()navigationPath.removeLast(2),一次 pop 完成页 + 播放器,直接回地图。
  4. 调用方

    • GrowthView / ProfileView / DiscoveryView 的 .completion 分支向 CompletionView 传入对应 path 的 Binding(如 $navStore.growthPath 等)。

三、与当前实现的对照

维度 当前实现 方案
数据源 allCourseNodes 纯净TabView 多一页 CompletionPlaceholderPage 用 tag "wg://completion"写入 allCourseNodes 与当前一致或更进一步:完全移除占位页,仅手势触发 push
进入完成页 左滑到占位页 → onChange(of: currentNodeId) → 0.1s 后 append .completion 最后一节左滑手势 → 直接 append 或先切 tag 再 append不依赖「多一页」
完成页返回 navStore.switchToGrowthTab() + dismiss(),先回播放器再靠系统/逻辑 removeLast(2) 穿透回地图,无中间层
占位页 存在,纯视觉 + 父视图 onChange 驱动 push无占位内 onAppear 方案 A保留为纯视觉方案 B删除仅手势

结论:方案在「单一事实来源」和「响应式导航」上比当前更彻底;当前已避免在数据源里掺假节点,但仍依赖 TabView 多一页占位。


四、按标准的符合度

4.1 零副作用

  • 方案:若不保留占位页,则无「假页」;若保留占位页且仅视觉、无逻辑,则也算零逻辑副作用。
  • 当前:占位页无 onAppear 导航,副作用已收敛,但 TabView 仍多一个「虚拟页」。
  • 符合度:方案完全符合;当前基本符合,方案更干净。

4.2 单一事实来源 (SSOT)

  • 方案allCourseNodes 仅课程章节;完成页由导航栈 + 手势驱动,不进入数据模型。
  • 当前allCourseNodes 已是纯净的 flatMap 章节节点,未追加 placeholder node。
  • 符合度:方案与当前都符合;方案在视图层也不再依赖「多一页」的 tagSSOT 更纯粹。

4.3 响应式导航

  • 方案CompletionView 通过 Binding<NavigationPath> 执行 removeLast(2),栈由 path 唯一决定,无 switchToGrowthTab() + dismiss() 的二次操作。
  • 当前:依赖 dismiss() 回播放器,再从播放器回地图,存在中间层与潜在白屏/闪烁。
  • 符合度:方案明显更符合;当前有改进空间。

4.4 原生手感

  • 方案:最后一节用 DragGesture(或类似)识别左滑,不依赖「滑到下一 tab 索引」才触发。
  • 当前:依赖用户滑到「占位页」才触发,仍与 TabView 索引/选页绑定。
  • 符合度:方案更贴近「手势驱动」;当前是「页面索引 + 手势」混合。

五、与既有文档的一致性

5.1 COMPLETION_VIEW_UI_LAYER_SPEC.md

  • 说明要求:返回用 dismiss()接收或操作 NavigationPath
  • 方案:改为接收 Binding<NavigationPath>removeLast(2),与当前 spec 冲突。
  • 建议:若采用方案,需同步更新 COMPLETION_VIEW_UI_LAYER_SPEC
    • 写明「穿透式返回」为可选/推荐实现;
    • 接口增加 @Binding var navigationPath: NavigationPath
    • 底部按钮行为改为「可调用 removeLast(2) 直接回地图」,并注明调用方需传入对应 path 的 Binding。

5.2 COMPLETION_PAGE_MEANING.md

  • 当前描述为:最后一节后再左滑一页进入占位页,占位页触发 push。
  • 方案:最后一节左滑即触发(或先切 tag 再触发),可完全去掉「多一页」概念。
  • 建议:若采用方案,更新 COMPLETION_PAGE_MEANING
    • 「最后一页」仍指课程的最后一节;
    • 进入完成页的方式改为「在最后一节左滑(手势)触发」,并注明是否保留占位页。

六、风险与注意点

  1. 多入口 path
    CompletionView 在 Growth / Profile / Discovery 三处被 present需分别传入 growthPath / profilePath / homePath 的 Binding漏传或传错会导致 removeLast(2) 作用在错误栈上。建议:

    • 在审查/实现时逐处确认传参;
    • 或为 CompletionView 封装「当前栈」来源(例如 Environment 注入当前 path避免调用方误绑。
  2. NavigationPath.count
    removeLast(2) 前需保证 path.count >= 2(完成页 + 播放器)。若从深链或异常入口进入完成页,栈深度可能不足,需保留兜底(如 dismiss())。

  3. 手势与 TabView 滑动冲突
    最后一节左滑既会触发自定义手势,也是 TabView 的翻页手势。需通过 minimumDistancecoordinateSpace 或「边缘优先」等策略区分,避免误触或重复触发。建议在真机多测:最后一节左滑、快速连续左滑、斜滑。

  4. 从完成页返回后的 Tab 状态
    当前实现:从完成页 dismiss 回播放器后,onAppear 里若 currentNodeId == "wg://completion" 会切回 allCourseNodes.last?.id。采用方案后,若使用 removeLast(2),不会回到播放器,故无需再依赖该逻辑;若仍保留「先 dismiss 再回地图」的入口,需保留或等价处理该状态恢复。


七、审查结论与建议

项目 结论
架构与标准 方案满足「零副作用、SSOT、响应式导航、原生手感」四项标准且比当前实现更彻底。
与现有 spec 与 COMPLETION_VIEW_UI_LAYER_SPEC 的「不操作 NavigationPath」冲突需更新 spec。
实现成本 中等CompletionView 接口与三处调用方改动VerticalScreenPlayerView 增加手势与可选移除占位页;需回归测试返回与多入口。
建议 若采纳方案:
  1. 先更新 COMPLETION_VIEW_UI_LAYER_SPEC 与 COMPLETION_PAGE_MEANING再改代码
  2. CompletionView 保留 path.count >= 2 判断与 dismiss() 兜底;
  3. 手势与 TabView 的冲突在真机验证,必要时加防抖或边缘区域限制;
  4. 三处 navigationDestination 的 Binding 传参做清单检查,避免漏传/错传。 |

报告日期:基于当前代码与所提供方案整理,未对仓库做任何代码修改。