# 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.`(JSON 编码) - 高亮:`ssreader.epub.highlights.`(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:)`:保存 textBook,reloadData **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` rangeInfo,Web 内容通过 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.01,reflowable 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 解析失败:显示 errorLabel,delegate 收到 `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.` | JSON → `RDEPUBLocation` | | 高亮列表 | `ssreader.epub.highlights.` | JSON → `[RDEPUBHighlight]` | | 书签列表 | `ssreader.epub.bookmarks.` | 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 是否存在选择类型 - 规则 2:RDEPUBTextContentView 展示 attributedText + 搜索高亮叠加 + 页码标签("N / M") - 规则 3:RDEPUBWebContentView 包装 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 处理