11 KiB
书签能力方案讨论
需求背景
Doc/EPUB_MAINTENANCE.md 已将“书签能力”列为 EPUB 阅读器下一阶段的高优先级能力之一。
当前阅读器已经具备:
- EPUB 打开与解析
- WebView / DTCoreText / fixed-layout 三条正文渲染路径
- 目录跳转、阅读位置恢复、主题与字号调整
- 高亮、划线、批注、搜索等阅读交互能力
但现阶段“位置记录”仍主要依赖:
- 自动保存的上次阅读位置
- 需要选中文本的高亮 / 批注能力
这会遗漏一种非常常见、也非常轻量的阅读诉求:
- 只想标记当前位置
- 稍后回来继续读
- 不需要摘录正文
- 不需要输入批注
因此,“书签能力”这项需求的目标,不是扩展现有标注功能,而是补上一条面向“位置收藏”的轻量链路,用来覆盖“只记录位置、不做摘录”的常见阅读需求。
代码事实
已有基础
当前仓库其实已经具备不少可直接复用的基础设施:
RDEPUBLocation已能稳定表达href + progression + fragmentRDEPUBReaderController已具备“当前位置读取”和“跳转到位置”的主链路RDEPUBReaderPersistence已形成按bookIdentifier分书持久化的模式- 高亮管理页已经具备“列表展示 -> 点击跳转 -> 删除”的成熟交互范式
结论:
- 书签不需要重新设计位置模型
- 书签可以复用现有定位和跳转能力
- 更需要新增的是独立模型、持久化存储位和 UI 入口
当前实现存在的问题
问题 1:当前只有单一阅读位置,没有“多书签”能力
RDEPUBReaderPersistence 当前只提供:
loadLocation / saveLocationloadHighlights / saveHighlightsloadReaderSettings / saveReaderSettings
这意味着:
- 系统只能保存“上次读到哪”
- 不能保存“用户主动收藏的多个位置”
而这两者的语义并不相同:
- 阅读位置是自动覆盖的
- 书签是主动创建、可长期保留多个的
问题 2:当前标注模型不适合直接承载书签
虽然看起来可以用 RDEPUBHighlight 勉强模拟书签,但这会带来明显问题:
- 书签不一定有选中文本
- 书签不应依赖
rangeInfo - 书签列表应以“位置”为中心,而不是以“摘录内容”为中心
- 书签与高亮、划线、批注的展示和管理语义不同
因此,不建议把书签硬塞进现有高亮模型。
问题 3:当前 UI 没有书签入口与状态反馈
从 RDEPUBReaderBottomToolView 当前结构来看,只有:
- 目录
- 批注列表
- 添加标注
- 设置
当前并没有:
- 当前页添加 / 取消书签入口
- 书签列表入口
- 当前阅读位置是否已加书签的状态反馈
这意味着即便补了数据层,用户仍然无法自然感知和使用这项能力。
需求拆解
建议把“书签能力”拆成四部分推进。
1. 独立书签模型
需要新增一个面向“位置记录”的独立模型,而不是复用高亮模型。
2. 独立书签持久化
需要新增书签读写接口,支持每本书存储多个书签。
3. 书签入口与状态反馈
需要让用户在当前页能轻量创建 / 取消书签,并能看到当前位置的书签状态。
4. 书签管理与跳转
需要补齐书签列表、点击跳转、删除等基本管理闭环。
技术路线对比
路线 A:把书签并入 RDEPUBHighlight
思路:
- 给
RDEPUBHighlight增加一个新的 style 或特殊标识 - 将书签存入 highlights
- 复用现有标注列表页
优点:
- 表面上改动范围较小
- 可以快速借用现有列表管理代码
风险:
- 模型语义不清晰
- 书签没有文本选区时会很别扭
- 后续展示、筛选、持久化逻辑会越来越绕
- 容易让“书签”和“标注”边界变得混乱
路线 B:完全独立一条书签链路
思路:
- 新建
RDEPUBBookmark - 新增
loadBookmarks / saveBookmarks - 新增书签列表页
- 独立接入添加、删除、跳转、状态更新
优点:
- 模型语义清晰
- 更符合产品能力边界
- 后续扩展空间更好
风险:
- 会新增一部分 UI 与管理代码
- 与高亮列表在形态上会有一定重复
路线 C:模型独立,交互模式局部复用
思路:
- 数据模型与持久化独立
- 定位、跳转和列表交互模式尽量复用现有高亮链路
- 第一阶段先做轻量闭环,再考虑更复杂管理能力
优点:
- 语义清晰,同时控制改动范围
- 能复用现有阅读位置链路和管理交互经验
- 更适合当前仓库的演进方式
风险:
- 仍需补一组新的数据与列表代码
- 需要明确书签状态判定规则,避免重排后误判
推荐结论
建议采用:
主路线:路线 C,模型独立,位置链路复用,UI 先做轻量闭环
原因:
- 书签和标注的语义不同,模型应保持独立
- 当前仓库已有成熟的位置和跳转主链路,可以直接复用
- 第一阶段先满足“记录位置”的核心需求,不需要把书签做得过重
推荐方案
方案总览
书签能力第一阶段建议形成如下结构:
- 新增
RDEPUBBookmark独立模型 - 扩展
RDEPUBReaderPersistence,新增 bookmarks 读写 - 在
RDEPUBReaderController中接入书签状态、添加、删除、跳转能力 - 增加轻量级书签入口和书签列表页
- 支持退出重进后的书签持久化恢复
推荐数据模型
建议书签模型至少包含以下字段:
idbookIdentifierlocationchapterTitledisplayText或摘要信息(可选)note(可选)createdAt
其中第一阶段真正必要的最小集合是:
idbookIdentifierlocationcreatedAt
建议同时补上 chapterTitle,这样书签列表在展示时会更友好,也更接近真实阅读器使用习惯。
推荐持久化结构
建议在 RDEPUBReaderPersistence 中新增:
loadBookmarks(for:)saveBookmarks(_:for:)
RDEPUBUserDefaultsPersistence 中新增:
bookmarksPrefix
第一阶段继续沿用当前:
UserDefaultsJSONEncoder / JSONDecoderbookIdentifier作用域隔离
这样能在最小改动下形成可用闭环。
推荐 UI 入口
第一阶段建议至少补两类入口:
- 当前页添加 / 取消书签入口
- 书签列表入口
比较自然的放置方式是:
- 顶部工具栏增加“当前页书签”切换按钮
- 底部工具栏增加“书签列表”入口
这样可以把两种动作区分开:
- “加书签”是当前上下文动作
- “看书签列表”是全局管理动作
推荐交互闭环
第一阶段建议用户路径收敛成下面这条主链路:
- 用户在当前阅读位置点击书签按钮
- 如果当前语义位置未加书签,则创建书签
- 如果当前语义位置已加书签,则取消该书签
- 用户可从书签列表查看当前书籍的全部书签
- 点击任意书签后跳转到对应位置
- 支持从列表中删除书签
关键实现建议
Task K1:新增书签模型
文件:
Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift
改动:
- 新增
RDEPUBBookmark: Codable, Equatable - 字段以
location为核心,不依赖选区文本
完成定义:
- 可以表达一本书中的一个可持久化书签位置
Task K2:扩展书签持久化协议
文件:
Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift
改动:
- 协议增加
loadBookmarks / saveBookmarks RDEPUBUserDefaultsPersistence增加bookmarksPrefix
完成定义:
- 每本书可以稳定读取和保存多个书签
Task K3:控制器接入书签状态与操作
文件:
Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift
改动:
- 增加
activeBookmarks - 打开图书时加载 bookmarks
- 增加
addBookmark - 增加
removeBookmark - 增加
toggleBookmark - 增加
go(to: bookmark) - 增加当前阅读位置是否已书签的状态计算
完成定义:
- 书签与阅读主链路、跳转链路、持久化链路打通
Task K4:补书签 UI 与列表管理
文件:
Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift- 新增
Sources/RDReaderView/EPUBUI/RDEPUBReaderBookmarksViewController.swift - 视情况补充顶部工具栏按钮
改动:
- 添加书签入口
- 添加书签列表入口
- 点击书签跳转
- 支持删除
- 提供当前位置书签状态反馈
完成定义:
- 用户能看到、添加、取消、查看和管理书签
当前页书签判定建议
“当前位置是否已加书签”不建议简单按“完全相同的 progression”判断,因为:
- 字号变化会触发重分页
- 视口尺寸变化会导致 progression 细微漂移
- WebView 和文本分页的进度精度并不完全一致
更合理的判定策略是:
- 优先比较标准化后的
href - 如果存在
fragment,优先使用fragment作为更强锚点 - reflowable 路径允许一定 progression 容差
- fixed-layout 路径优先按页级位置判断
这样可以减少用户在重排、重进或轻微位置变化时的书签状态抖动。
验收矩阵建议
第一阶段至少要覆盖以下验证维度:
- 当前页可添加书签
- 同一位置可取消书签
- 书签列表展示正常
- 点击书签可以跳转
- 删除书签后列表更新正确
- 退出重进后书签仍存在
- 字号变化后书签仍能回到同章节或邻近语义位置
webInteractive路径正常textReflowable路径正常webFixedLayout路径至少能按页恢复
风险与注意点
风险 1:把书签与高亮强行合并
这样短期看似省事,长期会让模型、持久化和 UI 语义都变重。
风险 2:过度依赖精确 progression 匹配
如果书签命中规则太严格,重分页后会显得“不稳定”,用户体验会很差。
风险 3:第一阶段把书签做得过重
书签能力的首要目标是“轻量记录位置”,不是一开始就做成复杂的收藏管理系统。
推荐实施顺序
如果目标是“最小投入换最大需求闭环”,建议按下面顺序推进:
- 先补独立
RDEPUBBookmark模型 - 先扩展
RDEPUBReaderPersistence的书签读写能力 - 再在
RDEPUBReaderController接入书签主链路 - 最后补 UI 入口和书签列表页
- 待第一阶段稳定后,再考虑备注、排序、筛选等增强能力
推荐结论
“书签能力”这项需求,建议最终收敛为下面这句话:
为 EPUB 阅读器补齐一条独立的轻量书签链路,以 RDEPUBLocation 为核心记录位置,支持添加、取消、列表管理、跳转与持久化恢复,用来覆盖“只记录位置、不做摘录”的常见阅读需求。