ReadViewSDK/Doc/EPUBUI_功能实现逻辑.md

16 KiB
Raw Blame History

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 阅读入口
    • init(textBook:bookIdentifier:title:textFileURL:configuration:) — 纯文本书籍阅读入口(由 RDPlainTextBookBuilder 构建 RDEPUBTextBook 后传入)
  • 职责:
    • 加载 EPUB后台解析 → 应用分页 → 恢复阅读位置
    • 实现 RDReaderDataSource / RDReaderDelegate 为容器提供数据
    • 实现 RDEPUBWebContentViewDelegate 接收 WebView 事件
    • 管理工具栏显隐、目录面板、高亮管理、设置面板、搜索
    • 持久化阅读位置、高亮、用户设置
    • 对外暴露 RDEPUBReaderDelegate 回调宿主 App

2.2 配置 RDEPUBReaderConfiguration

  • 文件:EPUBUI/RDEPUBReaderConfiguration.swift
  • 13 个配置项:
    • fontSize(默认 15lineHeightMultiple(默认 1.6
    • displayType(默认 .pageCurllandscapeDualPageEnabled(默认 true
    • showsTableOfContents(默认 trueallowsHighlights(默认 trueshowsSettingsPanel(默认 true
    • reflowableContentInsets(默认 40/16/40/16fixedContentInset(默认 .zero
    • theme(默认 .light
    • fixedLayoutFit(默认 .pagefixedLayoutSpreadMode(默认 .automatic
    • textRenderingEngine(默认 .dtCoreText
  • 变更检测:
    • requiresRepaginationfontSize / lineHeightMultiple / contentInsets / fixedLayoutFit / fixedLayoutSpreadMode / textRenderingEngine 变化 → 完整重新分页
    • requiresVisibleRefreshtheme 变化 → 刷新可见内容(不重新分页)

2.3 委托协议 RDEPUBReaderDelegate

  • 文件:EPUBUI/RDEPUBReaderDelegate.swift
  • 12 个可选方法(全部有默认空实现):
    • didOpen:成功打开出版物
    • didUpdateLocation:翻页或滚动时位置更新
    • didReachEnd:到达最后一页
    • didChangeSelection:文本选择变化或清除
    • didUpdateHighlights:高亮增删改
    • didUpdateBookmarks:书签增删改
    • didUpdateSearchResult:搜索状态变化
    • didChangeCurrentSearchMatch:当前搜索匹配项变化
    • didUpdateCurrentTableOfContentsItem:翻页时匹配的目录项
    • didActivateExternalLink:外部链接点击
    • didFailWithError:解析或分页错误
    • configureTopToolView:自定义顶部工具栏

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 内容视图

  • RDEPUBTextContentViewEPUBUI/RDEPUBTextContentView.swifttextReflowable 路径UITextView 展示富文本,支持系统选区、标注菜单、用户标注叠加和搜索高亮叠加
  • RDEPUBWebContentViewEPUBUI/RDEPUBWebContentView.swiftweb 路径,包装 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 全屏约束,注册 RDEPUBTextContentViewRDEPUBWebContentView
    • 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 = newValuerequiresRepaginationrepaginatePreservingCurrentLocation()
  • 行高变化:同上
  • 显示模式变化:readerView.switchReaderDisplayType() 重建容器
  • 主题变化:configuration.theme = newValuerequiresVisibleRefreshrefreshVisibleContentPreservingLocation()
  • 亮度变化: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 设置快照

RDEPUBReaderSettings (Codable)
  ├── brightness: CGFloat
  ├── fontSize: CGFloat
  ├── lineHeightMultiple: CGFloat
  ├── displayMode: RDEPUBReaderDisplayMode
  └── themePreset: RDEPUBReaderThemePreset?

5.4 目录扁平化项

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

  • RDEPUBReaderDelegate12 个可选方法,覆盖打开、位置更新、到达末尾、选择变化、高亮更新、书签更新、搜索更新、目录项更新、外部链接、错误、工具栏自定义

内部通信(闭包)

  • RDEPUBReaderTopToolView.onBack / onToggleBookmark
  • RDEPUBReaderBottomToolView.onShowTableOfContents / onShowBookmarks / onShowHighlights / onAddHighlight / onShowSettings
  • RDEPUBReaderChapterListController.onSelectItem
  • RDEPUBReaderHighlightsViewController.onSelectHighlight / onUpdateHighlight / onDeleteHighlight
  • RDEPUBReaderSettingsViewController.onBrightnessChange / onFontSizeChange / onLineHeightChange / onDisplayTypeChange / onThemeChange

容器通信(协议)

  • RDReaderDataSource / RDReaderDelegateRDEPUBReaderController 实现,为 RDReaderView 提供页面数据和接收页面变化事件
  • RDEPUBWebContentViewDelegateRDEPUBReaderController 实现,接收 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 处理