ReadViewSDK/Doc/EPUBUI_功能实现逻辑.md
2026-05-21 19:40:51 +08:00

308 lines
16 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.

# EPUBUI 功能实现逻辑
## 1. 范围与目标
- 代码范围:`Sources/RDReaderView/EPUBUI/`16 个 Swift 文件)
- 目标:说明 EPUBUI 如何作为开箱即用的阅读器 UI 层,协调 EPUBCore 解析、EPUBTextRendering 文本渲染、RDReaderView 分页容器,提供完整的阅读体验(工具栏、目录、高亮批注、设置面板、阅读位置持久化、搜索)。
- 主链路关键词:`RDEPUBReaderController.init -> 解析 EPUB -> 分页 -> 渲染 -> 用户交互(翻页/工具栏/设置/高亮/搜索)-> 持久化`。
## 2. 关键对象职责
### 2.1 主控制器 `RDEPUBReaderController`
- 文件:`EPUBUI/RDEPUBReaderController.swift`~1255 行)
- 入口方法:`init(epubURL:configuration:persistence:)`
- 职责:
- 加载 EPUB后台解析 → 应用分页 → 恢复阅读位置
- 实现 `RDReaderDataSource` / `RDReaderDelegate` 为容器提供数据
- 实现 `RDEPUBWebContentViewDelegate` 接收 WebView 事件
- 管理工具栏显隐、目录面板、高亮管理、设置面板、搜索
- 持久化阅读位置、高亮、用户设置
- 对外暴露 `RDEPUBReaderDelegate` 回调宿主 App
### 2.2 配置 `RDEPUBReaderConfiguration`
- 文件:`EPUBUI/RDEPUBReaderConfiguration.swift`
- 13 个配置项:
- `fontSize`(默认 15、`lineHeightMultiple`(默认 1.6
- `displayType`(默认 .pageCurl、`landscapeDualPageEnabled`(默认 true
- `showsTableOfContents`(默认 true、`allowsHighlights`(默认 true、`showsSettingsPanel`(默认 true
- `reflowableContentInsets`(默认 40/16/40/16、`fixedContentInset`(默认 .zero
- `theme`(默认 .light
- `fixedLayoutFit`(默认 .page、`fixedLayoutSpreadMode`(默认 .automatic
- `textRenderingEngine`(默认 .dtCoreText
- 变更检测:
- `requiresRepagination`fontSize / lineHeightMultiple / contentInsets / fixedLayoutFit / fixedLayoutSpreadMode / textRenderingEngine 变化 → 完整重新分页
- `requiresVisibleRefresh`theme 变化 → 刷新可见内容(不重新分页)
### 2.3 委托协议 `RDEPUBReaderDelegate`
- 文件:`EPUBUI/RDEPUBReaderDelegate.swift`
- 10 个可选方法(全部有默认空实现):
- `didOpen`:成功打开出版物
- `didUpdateLocation`:翻页或滚动时位置更新
- `didReachEnd`:到达最后一页
- `didChangeSelection`:文本选择变化或清除
- `didUpdateHighlights`:高亮增删改
- `didUpdateSearchResult`:搜索状态变化
- `didChangeCurrentSearchMatch`:当前搜索匹配项变化
- `didUpdateCurrentTableOfContentsItem`:翻页时匹配的目录项
- `didActivateExternalLink`:外部链接点击
- `didFailWithError`:解析或分页错误
### 2.4 持久化 `RDEPUBReaderPersistence`
- 文件:`EPUBUI/RDEPUBReaderPersistence.swift`
- 协议定义 6 个方法loadLocation / saveLocation / loadHighlights / saveHighlights / loadReaderSettings / saveReaderSettings
- 默认实现 `RDEPUBUserDefaultsPersistence`
- 位置:`ssreader.epub.location.<bookIdentifier>`JSON 编码)
- 高亮:`ssreader.epub.highlights.<bookIdentifier>`JSON 编码)
- 设置:`ssreader.epub.settings`(全局,非按书)
### 2.5 主题 `RDEPUBReaderTheme`
- 文件:`EPUBUI/RDEPUBReaderTheme.swift`
- 6 个颜色属性contentBackgroundColor、contentTextColor、toolBackgroundColor、toolControlTextColor、toolControlBorderUnselectColor、toolLineColor
- 6 个内置预设:`.light`、`.dark`、`.yellow`、`.green`、`.pink`、`.blue`
- 计算属性 `themeBackgroundColorCSS` / `themeTextColorCSS` 用于 Web 渲染路径
### 2.6 设置面板 `RDEPUBReaderSettingsViewController`
- 文件:`EPUBUI/RDEPUBReaderSettingsViewController.swift`
- 5 个控制项:
- 亮度滑块0-1
- 字号 A-/A+(范围 12-36步长 1
- 行高分段(紧凑 1.3 / 标准 1.6 / 宽松 1.9
- 显示模式分段(仿真 / 横滑 / 竖滑 / 覆盖)
- 主题选择6 个圆形色块按钮)
- 所有变更通过闭包实时回调
### 2.7 内容视图
- `RDEPUBTextContentView``EPUBUI/RDEPUBTextContentView.swift`textReflowable 路径UITextView 展示富文本,支持系统选区、标注菜单、用户标注叠加和搜索高亮叠加
- `RDEPUBWebContentView``EPUBUI/RDEPUBWebContentView.swift`web 路径,包装 `RDEPUBWebView`,转发位置、选区、标注菜单、链接和 JS 错误事件
### 2.8 其他 UI 组件
- `RDEPUBReaderTopToolView`:顶部导航栏(返回按钮 + 标题)
- `RDEPUBReaderBottomToolView`:底部工具栏(目录 / 批注 / 标注 / 设置 4 个按钮)
- `RDEPUBReaderChapterListController`目录列表modal UITableViewController当前项高亮 systemBlue
- `RDEPUBReaderHighlightsViewController`:标注管理(全部 / 批注 / 划线过滤,列表 + 编辑备注 + 删除 + 跳转,空态按过滤条件提示)
## 3. 主流程(代码级)
### 3.1 初始化与加载
1. `init(epubURL:configuration:persistence:)`
- 加载持久化的阅读设置,叠加到 configuration
- 恢复屏幕亮度
2. `viewDidLoad`
- 设置背景色
- `setupReaderView()`:添加 RDReaderView 全屏约束,注册 `RDEPUBTextContentView``RDEPUBWebContentView`
- `setupLoadingIndicator()`、`setupErrorLabel()`
3. `viewDidAppear`:调用 `startInitialLoadIfNeeded()`
4. 加载序列:
- `loadPublication()` → 读取持久化位置
- 后台 `RDEPUBParser.parse(epubURL:)` 解析
- 主线程 `applyParsedPublication()``paginatePublication()`
### 3.2 分页流程
`paginatePublication(restoreLocation:)` 根据 readingProfile 分三条路径:
**textReflowable 路径**
1. 后台队列:`RDEPUBTextBookBuilder.build(parser:publication:pageSize:style:)`
2. 主线程 `applyTextBook(textBook:)`:保存 textBookreloadData
**fixed layout 路径**
1. 创建快照:`pageCounts: Array(repeating: 1, count: publication.spine.count)`
2. `applyPaginationSnapshot(snapshot:restoreLocation:)`
**web interactive 路径**
1. `RDEPUBPaginator.calculate(parser:hostingView:presentation:completion:)`
2. 回调收到页数数组后 `applyPaginationSnapshot(snapshot:restoreLocation:)`
三条路径汇合 `finishPagination(restoreLocation:)`
- `readerView.reloadData()`
- `restoreReadingLocation(restoreLocation)`
### 3.3 页面内容工厂
`pageContentView(readerView:pageNum:containerView:)`
- 有 textBook → 出队/创建 `RDEPUBTextContentView`,调用 `configure(page:pageNumber:totalPages:configuration:searchState:)`
- 无 textBook → 出队/创建 `RDEPUBWebContentView`,调用 `configure(publication:request:pageNumber:totalPages:theme:)`
`pageIdentifier(readerView:pageNum:)`:返回对应类名字符串用于 cell 出队
### 3.4 工具栏交互
- RDReaderView 内部处理点击手势,中心 1/3 区域切换工具栏
- 顶部工具栏:返回按钮 pop/dismiss标题设为 metadata.title
- 底部工具栏4 个按钮按配置 flags 控制可见性
- 目录按钮 → present `RDEPUBReaderChapterListController`.pageSheet
- 批注按钮 → present `RDEPUBReaderHighlightsViewController`.pageSheet
- 标注按钮 → `presentAnnotationActionSheet(for:)`(需有 currentSelection否则 disabled可创建高亮、划线或带 note 的批注
- 设置按钮 → present `RDEPUBReaderSettingsViewController`.pageSheet
### 3.5 设置变更处理
- 字号变化:`configuration.fontSize = newValue` → `requiresRepagination``repaginatePreservingCurrentLocation()`
- 行高变化:同上
- 显示模式变化:`readerView.switchReaderDisplayType()` 重建容器
- 主题变化:`configuration.theme = newValue` → `requiresVisibleRefresh``refreshVisibleContentPreservingLocation()`
- 亮度变化:`UIScreen.main.brightness = value` + 持久化
### 3.6 标注管理
- 创建:`addAnnotation(from:style:color:note:)` → 从 currentSelection 构建 → 去重 → 追加到 activeHighlights → 持久化 → 刷新可见内容
- 兼容入口:`addHighlight(from:color:note:)` 仍保留,内部按 `.highlight` 创建
- 样式:`RDEPUBHighlightStyle.highlight` 使用背景色,`RDEPUBHighlightStyle.underline` 使用下划线;旧数据缺少 style 时默认按普通高亮解码
- 批注note 非空时同一条 `RDEPUBHighlight` 同时承担批注语义
- 删除:`removeHighlight(id:)` → 移除 → 持久化 → 刷新
- 编辑备注:`updateHighlightNote(id:note:)` → 更新 → 持久化 → 刷新
- 跳转:`go(toHighlightID:animated:)` → 从高亮 location 解析页码 → `transitionToPage`
- WebView 选区:`epub-bridge.js` 生成 `dom-range` rangeInfoWeb 内容通过 payload 的 `style` 字段恢复高亮或划线
- DT 选区:`RDEPUBTextContentView` 把页内 selectedRange 映射为章节全局 text-offset rangeInfo并在当前页与标注范围重叠时叠加背景或下划线
- 选区菜单WebView 与 DT 路径都通过自定义菜单动作收口到 `拷贝 / 高亮 / 批注`,动作最终复用同一套创建与批注输入流程
### 3.7 书签管理
- 创建/切换:`toggleBookmark(note:)` 基于 `currentVisibleLocation()` 生成当前位置书签;若同一语义位置已存在书签则执行取消
- 位置范围:书签使用 `RDEPUBLocation`,持久化字段包含 `href / fragment / progression`,不依赖正文选区
- 当前位置命中:优先比较标准化后的 `href`,若双方都有 `fragment` 则直接比较;否则按 `navigationProgression` 容差判断fixed-layout 0.01reflowable 0.05
- 标题补全:优先复用当前 TOC 项标题,否则根据扁平目录用 href 反查章节标题
- 列表管理:底部工具栏 `书签` 入口弹出 `RDEPUBReaderBookmarksViewController`,支持按创建时间倒序展示、跳转与删除
- 顶部状态TopToolView 右上角书签按钮显示当前页是否已加书签,点击可直接添加/取消
- 持久化:每次新增/删除都会通过 `RDEPUBReaderPersistence.saveBookmarks(_:for:)` 按书保存,并回调 `didUpdateBookmarks`
### 3.8 搜索流程
1. `search(keyword:)`:标准化输入
2. textBook 路径:`RDEPUBTextSearchEngine.search(keyword:)`
3. web 路径:`RDEPUBHTMLSearchEngine.search(keyword:)`
4. 存储结果到 `searchState`,导航到第一个匹配
5. `searchNext()` / `searchPrevious()`:循环推进 currentMatchIndex 并导航
6. `clearSearch()`:清除状态,刷新可见内容
### 3.9 阅读位置持久化
**保存时机**
- 每次翻页:`pageNum(readerView:pageNum:)` → `persist(location:)`
- WebView 滚动:`epubWebContentView(_:didUpdateLocation:)` → `persist(location:)`
**恢复时机**
- 初始加载:`loadPublication()` 读取 → 传递到 `finishPagination()``restoreReadingLocation()`
- 显式导航:`go(to:)` / `go(toTableOfContentsHref:)` / `go(toHighlightID:)` / 搜索导航
**恢复逻辑**
- textBook 路径:`textBook.pageNumber(for:resolver:bookIdentifier:)` → `readerView.transitionToPage`
- readingSession 路径:`readingSession.queueNavigation(to:)` → `transitionToPage`
### 3.9 横竖屏适配
- `viewWillTransition(to:with:)`:等待旋转过渡完成后,统一触发 viewport 变化处理
- `viewDidLayoutSubviews`用于补齐分屏、多窗口、safe area 变化等非旋转型视口变化
- 控制器内部使用 viewport signature 做去重,避免一次旋转触发多次正文重分页
- RDReaderView 的 `readerViewOrientationWillChange` 仅保留容器级双页布局和 pageCurl 重建职责,不再直接驱动 EPUBUI 重新分页
## 4. 异常与边界处理
- EPUB 解析失败:显示 errorLabeldelegate 收到 `didFailWithError`
- 分页失败:主线程 `handle(error:)` 处理
- `paginationToken` 防竞态:每次加载/分页生成新 UUID后台完成时校验 token 一致才应用结果
- 标注去重:`addAnnotation` 检查相同 location、text、rangeInfo 和 style 是否已存在标注
- 自定义主题不可持久化:仅 6 个内置 preset 可序列化,自定义主题仅当前会话有效
- 目录页码缺失:`flattenedTableOfContents` 中 pageNumber 可能为 nil无法定位到对应页面的 TOC 项)
## 5. 数据结构与字段映射
### 5.1 持久化键值
| 数据 | UserDefaults Key | 编码 |
|------|-----------------|------|
| 阅读位置 | `ssreader.epub.location.<bookIdentifier>` | JSON → `RDEPUBLocation` |
| 高亮列表 | `ssreader.epub.highlights.<bookIdentifier>` | JSON → `[RDEPUBHighlight]` |
| 书签列表 | `ssreader.epub.bookmarks.<bookIdentifier>` | JSON → `[RDEPUBBookmark]` |
| 阅读设置 | `ssreader.epub.settings` | JSON → `RDEPUBReaderSettings` |
### 5.2 Book Identifier
- 优先使用 `parser.metadata.identifier`
- 回退使用 `epubURL.lastPathComponent`
### 5.3 设置快照
```swift
RDEPUBReaderSettings (Codable)
├── brightness: CGFloat
├── fontSize: CGFloat
├── lineHeightMultiple: CGFloat
├── displayMode: RDEPUBReaderDisplayMode
└── themePreset: RDEPUBReaderThemePreset?
```
### 5.4 目录扁平化项
```swift
RDEPUBReaderTableOfContentsItem
├── title: String
├── href: String
├── depth: Int // 缩进层级
└── pageNumber: Int? // 可能为 nil
```
## 6. 视图绑定规则
- 规则 1内容视图通过 class name 注册到 RDReaderView出队时根据 textBook 是否存在选择类型
- 规则 2RDEPUBTextContentView 展示 attributedText + 搜索高亮叠加 + 页码标签("N / M"
- 规则 3RDEPUBWebContentView 包装 RDEPUBWebView + 页码标签
- 规则 4顶部工具栏固定提供返回与书签切换入口底部工具栏提供目录、书签、标注、设置等管理入口
- 规则 5设置面板所有变更实时生效闭包回调无需确认按钮
## 7. 通知协作与回调机制
### 外部通信Delegate
- `RDEPUBReaderDelegate`11 个可选方法,覆盖打开、位置更新、到达末尾、选择变化、高亮更新、书签更新、搜索更新、目录项更新、外部链接、错误
### 内部通信(闭包)
- `RDEPUBReaderTopToolView.onBack / onToggleBookmark`
- `RDEPUBReaderBottomToolView.onShowTableOfContents / onShowBookmarks / onShowHighlights / onAddHighlight / onShowSettings`
- `RDEPUBReaderChapterListController.onSelectItem`
- `RDEPUBReaderHighlightsViewController.onSelectHighlight / onUpdateHighlight / onDeleteHighlight`
- `RDEPUBReaderSettingsViewController.onBrightnessChange / onFontSizeChange / onLineHeightChange / onDisplayTypeChange / onThemeChange`
### 容器通信(协议)
- `RDReaderDataSource` / `RDReaderDelegate`RDEPUBReaderController 实现,为 RDReaderView 提供页面数据和接收页面变化事件
- `RDEPUBWebContentViewDelegate`RDEPUBReaderController 实现,接收 WebView 的位置更新、选择变化、链接激活、JS 错误
## 8. 联调与排查建议
- 排查 1阅读器打开后空白
- 检查 `RDEPUBParser.parse(epubURL:)` 是否成功
- 检查 `startInitialLoadIfNeeded()` 是否被调用(需要 viewDidAppear
- 检查 errorLabel 是否显示
- 排查 2翻页后位置不恢复
- 检查 `persistence.loadLocation(for:)` 是否返回非 nil
- 检查 `restoreReadingLocation` 是否在 `finishPagination` 中被调用
- textBook 路径检查 `textBook.pageNumber(for:)` 返回值
- 排查 3设置变更后无效果
- 字号/行高变化需触发 `repaginatePreservingCurrentLocation()`
- 主题变化只需 `refreshVisibleContentPreservingLocation()`
- 检查 `requiresRepagination` / `requiresVisibleRefresh` 的判断逻辑
- 排查 4高亮不显示
- 确认 `currentSelection` 非 nil
- 确认 `addHighlight` 成功返回
- 确认 `refreshVisibleContentPreservingLocation()` 被调用
- textBook 路径检查 `RDEPUBTextContentView.configure` 中的搜索高亮逻辑
- 排查 5搜索无结果
- textBook 路径确认 `RDEPUBTextSearchEngine.search` 被调用
- web 路径确认 `RDEPUBHTMLSearchEngine.search` 被调用
- 检查搜索关键词是否为空
- 排查 6横屏布局异常
- 确认 `landscapeDualPageEnabled` 是否为 true
- 检查 `viewDidLayoutSubviews` 是否触发了重新分页
- 检查 RDReaderView 的 orientation change 处理