- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
16 KiB
ReaderView 模块代码级参考文档
最后更新:2026-06-18
1. 模块概述
ReaderView 是 ReadViewSDK 的通用翻页容器层,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 RDReaderPageProvider 协议提供页面内容视图,ReaderView 负责容器管理和翻页调度。
文件清单(14 个 Swift 文件):
| 文件 | 核心类型 | 职责 |
|---|---|---|
RDReaderView.swift |
RDReaderView |
主容器视图,协调所有子组件 |
RDReaderViewProtocols.swift |
协议 + 枚举 | 数据源、代理、导航协议定义 |
RDReaderFlowLayout.swift |
RDReaderFlowLayout |
UICollectionView 自定义布局 |
RDReaderGestureController.swift |
RDReaderGestureController |
手势控制器(预留) |
RDReaderContentCell.swift |
RDReaderContentCell |
CollectionView 内容 Cell |
RDReaderPageChildViewController.swift |
RDReaderPageChildViewController |
PageCurl 模式子 VC |
RDReaderView+PageCurl.swift |
Extension | UIPageViewController 数据源/代理 |
RDReaderView+CollectionView.swift |
Extension | UICollectionView 数据源/布局代理 |
RDReaderView+ContentAccess.swift |
Extension | 内容视图访问与复用 |
RDReaderView+ToolView.swift |
Extension | 工具栏安装与动画 |
Paging/RDReaderPagingController.swift |
RDReaderPagingController |
翻页状态机 |
Paging/RDReaderPreloadController.swift |
RDReaderPreloadController |
页面预加载与缓存 |
Paging/RDReaderSpreadResolver.swift |
RDReaderSpreadResolver |
双页展开计算 |
Paging/RDReaderTapRegionHandler.swift |
RDReaderTapRegionHandler |
点击区域判定 |
2. 协议定义(RDReaderViewProtocols.swift)
2.1 RDReaderDataSource(旧版数据源,已废弃)
@objc public protocol RDReaderDataSource: NSObjectProtocol {
func pageCountOfReaderView(readerView: RDReaderView) -> Int
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
}
向后兼容保留,新代码应使用
RDReaderPageProvider。
2.2 RDReaderPageProvider(推荐数据源)
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
func numberOfPages(in readerView: RDReaderView) -> Int
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
}
| 方法 | 说明 |
|---|---|
numberOfPages(in:) |
返回总页数 |
readerView(_:viewForPageAt:reusableView:) |
为指定页码提供内容视图,reusableView 可复用 |
pageIdentifier(in:index:) |
返回页视图复用标识符,用于 CollectionView 注册 |
readerViewTopChrome(_:) |
返回顶部工具栏视图 |
readerViewBottomChrome(_:) |
返回底部工具栏视图 |
2.3 RDReaderDelegate
@objc public protocol RDReaderDelegate: NSObjectProtocol {
func pageNum(readerView: RDReaderView, pageNum: Int)
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
}
2.4 RDReaderPageNavigating
public protocol RDReaderPageNavigating: AnyObject {
var currentPage: Int { get }
func reloadPages()
func transition(to page: Int, animated: Bool)
}
RDReaderView 遵循此协议,提供统一的页面导航接口。
2.5 枚举类型
extension RDReaderView {
public enum DisplayType {
case pageCurl // 仿真翻页(UIPageViewController)
case horizontalScroll // 水平滑动(UICollectionView)
case verticalScroll // 垂直滚动(UICollectionView)
}
public enum PageDirection {
case leftToRight // LTR
case rightToLeft // RTL
}
}
3. 核心类:RDReaderView
文件: RDReaderView.swift
RDReaderView 是一个 UIView 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
- PageCurl 模式:使用
UIPageViewController实现仿真翻页 - Scroll 模式:使用
UICollectionView+ 自定义RDReaderFlowLayout实现滑动翻页
3.1 关键属性
| 属性 | 类型 | 说明 |
|---|---|---|
currentPage |
Int |
当前页码,变化时通知 delegate |
currentDisplayType |
DisplayType |
当前显示模式 |
pageDirection |
PageDirection |
页面方向(LTR/RTL) |
landscapeDualPageEnabled |
Bool |
是否启用横屏双页 |
coverPageIndex |
Int? |
封面页索引(独占一屏) |
pagesPerScreen |
Int |
每屏页数(横屏双页时为 2) |
preloadRadius |
Int |
预加载半径(默认 1) |
dataSource |
RDReaderDataSource? |
旧版数据源 |
pageProvider |
RDReaderPageProvider? |
推荐数据源 |
delegate |
RDReaderDelegate? |
事件代理 |
toolViewAnimationDuration |
TimeInterval |
工具栏动画时长(0.3s) |
3.2 关键方法
/// 切换显示模式(pageCurl / horizontalScroll / verticalScroll)
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType)
/// 跳转到指定页
public func transitionToPage(pageNum: Int, animated: Bool = false)
/// 重新加载所有页面
public func reloadData()
/// 仅重载页数(不重建内容)
public func reloadPageCountOnly()
/// 判断指定页是否为全屏页(封面页独占一屏)
public func isFullScreenPage(_ pageNum: Int) -> Bool
3.3 内部子组件
| 组件 | 类型 | 职责 |
|---|---|---|
pageViewController |
UIPageViewController |
PageCurl 翻页引擎 |
collectionView |
UICollectionView |
Scroll 翻页引擎 |
layout |
RDReaderFlowLayout |
CollectionView 自定义布局 |
spreadResolver |
RDReaderSpreadResolver |
双页配对计算 |
tapRegionHandler |
RDReaderTapRegionHandler |
点击区域判定 |
preloadController |
RDReaderPreloadController |
页面预加载与缓存 |
pagingController |
RDReaderPagingController |
翻页状态管理 |
4. 翻页状态机:RDReaderPagingController
文件: Paging/RDReaderPagingController.swift
管理 PageCurl 模式下的翻页请求队列,防止动画冲突。
struct RDReaderPagingController {
struct PageTransitionRequest: Equatable {
let pageNum: Int
let animated: Bool
}
var pendingTransitionRequest: PageTransitionRequest? // 待处理请求
var isTransitioning: Bool // 是否正在翻页动画中
var didBuildUI: Bool // UI 是否已构建
/// 判断是否应排队请求(PageCurl 模式下动画中返回 true)
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool
/// 完成翻页动画,返回待处理的请求
mutating func finishPageCurlTransition() -> PageTransitionRequest?
/// 重置状态(用于故障恢复)
mutating func resetPendingState()
}
5. 页面预加载:RDReaderPreloadController
文件: Paging/RDReaderPreloadController.swift
负责在当前页周围预渲染页面视图,减少翻页时的白屏时间。
5.1 核心机制
- 缓存签名(CacheSignature):基于
displayType + isLandscape + pagesPerScreen + boundsSize生成签名,签名变化时清空缓存 - 双缓存池:
preloadedPageViews(预加载池)和pageCurlCachedViews(PageCurl 缓存池) - 预测性预加载:根据
preferredForward方向多预加载一页
5.2 关键方法
/// 为指定页获取视图(优先从缓存取)
func pageViewForDisplay(pageNum: Int, environment: Environment, contentViewProvider: (Int, UIView?) -> UIView?) -> UIView
/// 在指定页周围预加载
func prime(around pageNum: Int, preferredForward: Bool?, parentView: UIView, environment: Environment, contentViewProvider: (Int, UIView?) -> UIView?)
/// 取走预加载的视图(用于 CollectionView 复用)
func takePreloadedView(for pageNum: Int) -> UIView?
/// 使缓存失效
func invalidate(environment: Environment)
5.3 Environment 结构体
struct Environment {
let displayType: RDReaderView.DisplayType
let isLandscape: Bool
let pagesPerScreen: Int
let boundsSize: CGSize
let landscapeDualPageEnabled: Bool
let coverPageIndex: Int?
let totalPages: Int
let spreadResolver: RDReaderSpreadResolver
}
6. 双页展开计算:RDReaderSpreadResolver
文件: Paging/RDReaderSpreadResolver.swift
纯函数式结构体,负责双页模式下的页面配对和导航计算。
struct RDReaderSpreadResolver {
/// 判断是否为全屏页(封面页独占一屏)
func isFullScreenPage(_ pageNum: Int, landscapeDualPageEnabled: Bool, isLandscape: Bool, coverPageIndex: Int?) -> Bool
/// 计算双页配对(left, right?),right 为 nil 表示独占一屏
func dualPagePair(for pageNum: Int, totalPages: Int, coverPageIndex: Int?) -> (left: Int, right: Int?)
/// 计算相邻双页的起始页码
func adjacentDualPage(from pageNum: Int, totalPages: Int, coverPageIndex: Int?, forward: Bool) -> Int?
/// 计算下一页(支持单页和双页模式)
func nextPage(from currentPage: Int, totalPages: Int, pagesPerScreen: Int, coverPageIndex: Int?, forward: Bool) -> Int?
}
封面页逻辑:
- 封面页(
coverPageIndex)独占一屏,不与其他页配对 - 封面后的页面从
coverIndex + 1开始两两配对
7. 点击区域判定:RDReaderTapRegionHandler
文件: Paging/RDReaderTapRegionHandler.swift
将屏幕三等分,判定点击属于左/中/右区域。
struct RDReaderTapRegionHandler {
func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDReaderView.TapEvent
}
逻辑:
- 左 1/3 →
.left(上一页),工具栏可见时改为.center - 中 1/3 →
.center(切换工具栏) - 右 1/3 →
.right(下一页),工具栏可见时改为.center
8. 流式布局:RDReaderFlowLayout
文件: RDReaderFlowLayout.swift
UICollectionViewFlowLayout 子类,支持水平滚动和垂直滚动两种模式。
8.1 关键属性
| 属性 | 类型 | 说明 |
|---|---|---|
displayType |
RDReaderView.DisplayType |
布局模式 |
isLandscapeDualPage |
Bool |
是否横屏双页 |
coverPageIndex |
Int? |
封面页索引 |
pagesPerScreen |
Int |
每屏页数 |
8.2 协议
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
}
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
}
8.3 关键方法
/// 计算指定页码的 contentOffset
func currentContentOffset(count: Int) -> CGPoint
8.4 封面页布局逻辑
双页模式下,封面页占满整屏宽度,后续页面两两配对占半屏宽度。coverAwareFrame(for:screenWidth:halfWidth:height:) 方法根据页码计算对应的 frame。
9. 内容 Cell:RDReaderContentCell
文件: RDReaderContentCell.swift
UICollectionViewCell 子类,用于 Scroll 模式下承载页面内容视图。
class RDReaderContentCell: UICollectionViewCell {
var containerView: UIView? // 设置时自动添加到 contentView,移除旧视图
}
10. PageCurl 子控制器:RDReaderPageChildViewController
文件: RDReaderPageChildViewController.swift
UIViewController 子类,作为 UIPageViewController 的页面 VC。
class RDReaderPageChildViewController: UIViewController {
var contentView: UIView? // 内容视图,设置时自动安装到容器
var pageNum: Int // 对应页码
init(contentView: UIView?, pageNum: Int = 0)
}
11. Extension 汇总
11.1 RDReaderView+PageCurl
实现 UIPageViewControllerDataSource 和 UIPageViewControllerDelegate:
pageViewController(_:viewControllerBefore:)— 提供前一页 VC(RTL 时逻辑反转)pageViewController(_:viewControllerAfter:)— 提供后一页 VCpageViewController(_:didFinishAnimating:...)— 完成动画后更新 currentPagepageViewController(_:willTransitionTo:)— 即将翻页时预加载
特殊页码:
RDReaderView.blankPageNum(Int.max)— 双页模式下的空白页RDReaderView.blankEndPageNum(Int.max - 1)— 末尾空白页
11.2 RDReaderView+CollectionView
实现 UICollectionViewDataSource、RDReaderFlowLayoutDelegate、RDReaderFlowLayoutDataSoure:
collectionView(_:cellForItemAt:)— 复用预加载视图或创建新 CellcollectionView(_:numberOfItemsInSection:)— 返回总页数pageNum(flowLayout:pageIndex:)— 滚动时更新当前页码
11.3 RDReaderView+ContentAccess
提供内容视图的注册、复用和查询:
/// 注册内容视图类型(类似 UICollectionView 的 register)
public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String)
/// 获取可复用的内容视图
public func dequeueReusableContentView(withReuseIdentifier identifier: String, for pageNum: Int) -> UIView
/// 获取指定页的内容视图
public func pageContentView(pageNum: Int) -> UIView?
/// 计算单页尺寸(考虑双页模式)
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize
11.4 RDReaderView+ToolView
管理顶部/底部工具栏的安装、显示/隐藏动画:
/// 切换工具栏显示状态(点击中心区域触发)
func tapCenter()
/// 安装工具栏视图到指定位置
func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition)
/// 更新工具栏高度约束
func updateToolViewHeightConstraintsIfNeeded()
动画效果: 顶部工具栏从上方滑入,底部工具栏从下方滑入。
12. 设计模式总结
| 模式 | 应用 |
|---|---|
| 策略模式 | DisplayType 切换 PageCurl / Scroll 两种翻页策略 |
| 适配器模式 | RDReaderLegacyDataSourceAdapter 将旧 RDReaderDataSource 适配为 RDReaderPageProvider |
| 命令队列 | RDReaderPagingController 管理翻页请求队列 |
| 缓存签名 | RDReaderPreloadController.CacheSignature 检测环境变化自动失效 |
| 关注点分离 | Extension 将不同功能拆分到独立文件 |
| 纯函数 | RDReaderSpreadResolver 和 RDReaderTapRegionHandler 无状态计算 |
13. 数据流图
用户点击屏幕
│
▼
RDReaderTapRegionHandler.resolveTapEvent()
│
├── .left → goPreviousPage() ──→ spreadResolver.nextPage(forward: false)
├── .right → goNextPage() ──→ spreadResolver.nextPage(forward: true)
└── .center → tapCenter() ──→ 显示/隐藏工具栏
transitionToPage(pageNum:)
│
├── PageCurl 模式
│ ├── pagingController.shouldQueuePageTransition() → 排队或执行
│ ├── pageViewForDisplay() → preloadController 取缓存视图
│ ├── pageViewController.setViewControllers()
│ └── primePageCache() → 预加载周围页面
│
└── Scroll 模式
├── collectionView.reloadData()
├── collectionView.setContentOffset()
└── primePageCache() → 预加载周围页面