- 更新模块文件数(EPUBCore 31+2, EPUBTextRendering 13, ReaderView 5, EPUBUI 19) - 更新 Swift 版本(5.10)、iOS 版本(15.0)、行数等基本信息 - 补充新增文件文档(TextAnchor, RenderRequest, WebViewDebug, AssetRepository, TextIndexTable, TextPerformanceSampler, ChapterData, PageInteractionController 等) - 更新 RDEPUBLocation 模型(新增 rangeAnchor 字段) - 更新 RDEPUBReaderConfiguration(13 项配置)、Delegate 签名、Persistence(8 方法) - 修正 RDURLReaderController 归属(ReaderView → EPUBUI) - 移除过时的 LegacyRDReaderController 引用 - 标记 SS→RD 命名迁移为已完成
18 KiB
18 KiB
EPUBUI 功能实现逻辑
1. 范围与目标
- 代码范围:
Sources/RDReaderView/EPUBUI/(19 个 Swift 文件) - 目标:说明 EPUBUI 如何作为开箱即用的阅读器 UI 层,协调 EPUBCore 解析、EPUBTextRendering 文本渲染、RDReaderView 分页容器,提供完整的阅读体验(工具栏、目录、高亮批注、设置面板、阅读位置持久化、搜索)。
- 主链路关键词:
RDEPUBReaderController.init -> 解析 EPUB -> 分页 -> 渲染 -> 用户交互(翻页/工具栏/设置/高亮/搜索)-> 持久化。
2. 关键对象职责
2.1 主控制器 RDEPUBReaderController
- 文件:
EPUBUI/RDEPUBReaderController.swift(~1995 行) - 入口方法:
init(epubURL:configuration:persistence:)— 标准 EPUB 阅读入口init(textBook:bookIdentifier:title:textFileURL:configuration:)— 纯文本书籍阅读入口(由RDPlainTextBookBuilder构建RDEPUBTextBook后传入)
- 职责:
- 加载 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(默认 top:40 left:16 bottom:40 right: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 - 12 个可选方法(全部有默认空实现):
epubReader(_:didOpen:):成功打开出版物epubReader(_:didUpdateLocation:):翻页或滚动时位置更新epubReaderDidReachEnd(_:):到达最后一页epubReader(_:didChangeSelection:):文本选择变化或清除epubReader(_:didUpdateHighlights:):高亮增删改epubReader(_:didUpdateBookmarks:):书签增删改epubReader(_:didUpdateSearchResult:):搜索状态变化epubReader(_:didChangeCurrentSearchMatch:):当前搜索匹配项变化epubReader(_:didUpdateCurrentTableOfContentsItem:):翻页时匹配的目录项epubReader(_:didActivateExternalLink:):外部链接点击epubReader(_:didFailWithError:):解析或分页错误epubReader(_:configureTopToolView:):自定义顶部工具栏
2.4 持久化 RDEPUBReaderPersistence
- 文件:
EPUBUI/RDEPUBReaderPersistence.swift - 协议定义 8 个方法:loadLocation / saveLocation / loadHighlights / saveHighlights / loadBookmarks / saveBookmarks / loadReaderSettings / saveReaderSettings
- 默认实现
RDEPUBUserDefaultsPersistence:- 位置:
ssreader.epub.location.<bookIdentifier>(JSON 编码) - 高亮:
ssreader.epub.highlights.<bookIdentifier>(JSON 编码) - 书签:
ssreader.epub.bookmarks.<bookIdentifier>(JSON 编码) - 设置:
ssreader.epub.settings(全局,非按书)
- 位置:
2.5 主题 RDEPUBReaderTheme
- 文件:
EPUBUI/RDEPUBReaderTheme.swift(~125 行) - 6 个颜色属性:contentBackgroundColor、contentTextColor、toolBackgroundColor、toolControlTextColor、toolControlBorderUnselectColor、toolLineColor
- 6 个内置预设:
.light、.dark、.yellow、.green、.pink、.blue RDEPUBReaderThemePreset枚举将预设映射为可序列化值,用于持久化- 计算属性
themeBackgroundColorCSS/themeTextColorCSS用于 Web 渲染路径
2.6 设置面板 RDEPUBReaderSettingsViewController
- 文件:
EPUBUI/RDEPUBReaderSettingsViewController.swift(~311 行) - 5 个控制项:
- 亮度滑块(0-1)
- 字号 A-/A+(范围 12-36,步长 1)
- 行高分段(紧凑 1.3 / 标准 1.6 / 宽松 1.9)
- 显示模式分段(仿真 / 横滑 / 竖滑 / 覆盖)
- 主题选择(6 个圆形色块按钮)
- 所有变更通过闭包实时回调:onBrightnessChange / onFontSizeChange / onLineHeightChange / onDisplayTypeChange / onThemeChange
2.7 内容视图
RDEPUBTextContentView(EPUBUI/RDEPUBTextContentView.swift):textReflowable 路径,UITextView 展示富文本,支持系统选区、标注菜单、用户标注叠加和搜索高亮叠加RDEPUBWebContentView(EPUBUI/RDEPUBWebContentView.swift):web 路径,包装RDEPUBWebView,转发位置、选区、标注菜单、链接和 JS 错误事件
2.8 其他 UI 组件
RDEPUBReaderToolView(EPUBUI/RDEPUBReaderToolView.swift):工具栏基类,提供分隔线和主题适配的通用逻辑RDEPUBReaderTopToolView:顶部导航栏(返回按钮 + 书签切换 + 标题)RDEPUBReaderBottomToolView:底部工具栏(目录 / 书签 / 标注 / 设置 4 个按钮)RDEPUBReaderChapterListController:目录列表(modal UITableViewController,当前项高亮 systemBlue)RDEPUBReaderHighlightsViewController:标注管理(全部 / 批注 / 划线过滤,列表 + 编辑备注 + 删除 + 跳转,空态按过滤条件提示)RDEPUBReaderSettings(EPUBUI/RDEPUBReaderSettings.swift):可持久化用户设置模型(brightness / fontSize / lineHeightMultiple / displayMode / themePreset),含RDEPUBReaderDisplayMode和RDEPUBReaderThemePreset枚举RDEPUBReaderTableOfContentsItem(EPUBUI/RDEPUBReaderTableOfContentsItem.swift):展平后的目录条目(title / href / depth / pageNumber)RDEPUBPageInteractionController(EPUBUI/RDEPUBPageInteractionController.swift):CoreText 页面级交互控制器,处理文本选区和高亮装饰RDEPUBSelectionOverlayView(EPUBUI/RDEPUBSelectionOverlayView.swift):选区覆盖视图,显示选中文本的高亮装饰RDEPUBPageLayoutSnapshot(EPUBUI/RDEPUBPageLayoutSnapshot.swift):页面几何模型,存储 CoreText 渲染的页面布局信息RDURLReaderController(EPUBUI/RDURLReaderController.swift,~221 行):最简 URL 入口,传入 URL 即可打开书籍(.epub → RDEPUBReaderController,其他 → RDPlainTextBookBuilder 构建后传入),分页失败时回退到纯 UITextView 展示
3. 主流程(代码级)
3.1 初始化与加载
init(epubURL:configuration:persistence:):- 加载持久化的阅读设置,叠加到 configuration
- 恢复屏幕亮度
viewDidLoad:- 设置背景色
setupReaderView():添加 RDReaderView 全屏约束,注册RDEPUBTextContentView和RDEPUBWebContentViewsetupLoadingIndicator()、setupErrorLabel()
viewDidAppear:调用startInitialLoadIfNeeded()- 加载序列:
loadPublication()→ 读取持久化位置- 后台
RDEPUBParser.parse(epubURL:)解析 - 主线程
applyParsedPublication()→paginatePublication()
3.2 分页流程
paginatePublication(restoreLocation:) 根据 readingProfile 分三条路径:
textReflowable 路径:
- 后台队列:
RDEPUBTextBookBuilder.build(parser:publication:pageSize:style:) - 主线程
applyTextBook(textBook:):保存 textBook,reloadData
fixed layout 路径:
- 创建快照:
pageCounts: Array(repeating: 1, count: publication.spine.count) applyPaginationSnapshot(snapshot:restoreLocation:)
web interactive 路径:
RDEPUBPaginator.calculate(parser:hostingView:presentation:completion:)- 回调收到页数数组后
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)
- 目录按钮 → present
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-rangerangeInfo,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 搜索流程
search(keyword:):标准化输入- textBook 路径:
RDEPUBTextSearchEngine.search(keyword:) - web 路径:
RDEPUBHTMLSearchEngine.search(keyword:) - 存储结果到
searchState,导航到第一个匹配 searchNext()/searchPrevious():循环推进 currentMatchIndex 并导航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.<bookIdentifier> |
JSON → RDEPUBLocation |
| 高亮列表 | ssreader.epub.highlights.<bookIdentifier> |
JSON → [RDEPUBHighlight] |
| 书签列表 | ssreader.epub.bookmarks.<bookIdentifier> |
JSON → [RDEPUBBookmark] |
| 阅读设置 | ssreader.epub.settings |
JSON → RDEPUBReaderSettings |
5.2 设置模型
RDEPUBReaderSettings (Codable)
├── brightness: CGFloat?
├── fontSize: CGFloat?
├── lineHeightMultiple: CGFloat?
├── displayMode: RDEPUBReaderDisplayMode?
└── themePreset: RDEPUBReaderThemePreset?
RDEPUBReaderDisplayMode:可序列化的翻页模式枚举(pageCurl / horizontalScroll / verticalScroll / horizontalCoverScroll),与 RDReaderView.DisplayType 相互转换。
RDEPUBReaderThemePreset:可序列化的主题预设枚举(light / yellow / green / pink / blue / dark),与 RDEPUBReaderTheme 相互转换。
5.3 Book Identifier
- 优先使用
parser.metadata.identifier - 回退使用
epubURL.lastPathComponent
5.4 目录扁平化项
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:12 个可选方法,覆盖打开、位置更新、到达末尾、选择变化、高亮更新、书签更新、搜索更新、目录项更新、外部链接、错误、工具栏自定义
内部通信(闭包)
RDEPUBReaderTopToolView.onBack / onToggleBookmarkRDEPUBReaderBottomToolView.onShowTableOfContents / onShowBookmarks / onShowHighlights / onAddHighlight / onShowSettingsRDEPUBReaderChapterListController.onSelectItemRDEPUBReaderHighlightsViewController.onSelectHighlight / onUpdateHighlight / onDeleteHighlightRDEPUBReaderSettingsViewController.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被调用 - 检查搜索关键词是否为空
- textBook 路径确认
- 排查 6:横屏布局异常
- 确认
landscapeDualPageEnabled是否为 true - 检查
viewDidLayoutSubviews是否触发了重新分页 - 检查 RDReaderView 的 orientation change 处理
- 确认