# ReaderView 模块代码级参考文档 > 最后更新:2026-06-18 --- ## 1. 模块概述 `ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDEpubReaderPageProvider` 协议提供页面内容视图,ReaderView 负责容器管理和翻页调度。 **文件清单(14 个 Swift 文件):** | 文件 | 核心类型 | 职责 | |------|----------|------| | `RDEpubReaderView.swift` | `RDEpubReaderView` | 主容器视图,协调所有子组件 | | `RDEpubReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 | | `RDEpubReaderFlowLayout.swift` | `RDEpubReaderFlowLayout` | UICollectionView 自定义布局 | | `RDEpubReaderGestureController.swift` | `RDEpubReaderGestureController` | 手势控制器(预留) | | `RDEpubReaderContentCell.swift` | `RDEpubReaderContentCell` | CollectionView 内容 Cell | | `RDEpubReaderPageChildViewController.swift` | `RDEpubReaderPageChildViewController` | PageCurl 模式子 VC | | `RDEpubReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 | | `RDEpubReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 | | `RDEpubReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 | | `RDEpubReaderView+ToolView.swift` | Extension | 工具栏安装与动画 | | `Paging/RDEpubReaderPagingController.swift` | `RDEpubReaderPagingController` | 翻页状态机 | | `Paging/RDEpubReaderPreloadController.swift` | `RDEpubReaderPreloadController` | 页面预加载与缓存 | | `Paging/RDEpubReaderSpreadResolver.swift` | `RDEpubReaderSpreadResolver` | 双页展开计算 | | `Paging/RDEpubReaderTapRegionHandler.swift` | `RDEpubReaderTapRegionHandler` | 点击区域判定 | --- ## 2. 协议定义(RDEpubReaderViewProtocols.swift) ### 2.1 RDEpubReaderDataSource(旧版数据源,已废弃) ```swift @objc public protocol RDEpubReaderDataSource: NSObjectProtocol { func pageCountOfReaderView(readerView: RDEpubReaderView) -> Int func pageContentView(readerView: RDEpubReaderView, pageNum: Int, containerView: UIView?) -> UIView func pageIdentifier(readerView: RDEpubReaderView, pageNum: Int) -> String? @objc optional func topToolView(readerView: RDEpubReaderView) -> UIView? @objc optional func bottomToolView(readerView: RDEpubReaderView) -> UIView? } ``` > 向后兼容保留,新代码应使用 `RDEpubReaderPageProvider`。 ### 2.2 RDEpubReaderPageProvider(推荐数据源) ```swift @objc public protocol RDEpubReaderPageProvider: NSObjectProtocol { func numberOfPages(in readerView: RDEpubReaderView) -> Int func readerView(_ readerView: RDEpubReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView @objc optional func pageIdentifier(in readerView: RDEpubReaderView, index: Int) -> String? @objc optional func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView? @objc optional func readerViewBottomChrome(_ readerView: RDEpubReaderView) -> UIView? } ``` | 方法 | 说明 | |------|------| | `numberOfPages(in:)` | 返回总页数 | | `readerView(_:viewForPageAt:reusableView:)` | 为指定页码提供内容视图,`reusableView` 可复用 | | `pageIdentifier(in:index:)` | 返回页视图复用标识符,用于 CollectionView 注册 | | `readerViewTopChrome(_:)` | 返回顶部工具栏视图 | | `readerViewBottomChrome(_:)` | 返回底部工具栏视图 | ### 2.3 RDEpubReaderDelegate ```swift @objc public protocol RDEpubReaderDelegate: NSObjectProtocol { func pageNum(readerView: RDEpubReaderView, pageNum: Int) @objc optional func readerViewOrientationWillChange(readerView: RDEpubReaderView, isLandscape: Bool) } ``` ### 2.4 RDEpubReaderPageNavigating ```swift public protocol RDEpubReaderPageNavigating: AnyObject { var currentPage: Int { get } func reloadPages() func transition(to page: Int, animated: Bool) } ``` `RDEpubReaderView` 遵循此协议,提供统一的页面导航接口。 ### 2.5 枚举类型 ```swift extension RDEpubReaderView { public enum DisplayType { case pageCurl // 仿真翻页(UIPageViewController) case horizontalScroll // 水平滑动(UICollectionView) case verticalScroll // 垂直滚动(UICollectionView) } public enum PageDirection { case leftToRight // LTR case rightToLeft // RTL } } ``` --- ## 3. 核心类:RDEpubReaderView **文件:** `RDEpubReaderView.swift` `RDEpubReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎: - **PageCurl 模式**:使用 `UIPageViewController` 实现仿真翻页 - **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDEpubReaderFlowLayout` 实现滑动翻页 ### 3.1 关键属性 | 属性 | 类型 | 说明 | |------|------|------| | `currentPage` | `Int` | 当前页码,变化时通知 delegate | | `currentDisplayType` | `DisplayType` | 当前显示模式 | | `pageDirection` | `PageDirection` | 页面方向(LTR/RTL) | | `landscapeDualPageEnabled` | `Bool` | 是否启用横屏双页 | | `coverPageIndex` | `Int?` | 封面页索引(独占一屏) | | `pagesPerScreen` | `Int` | 每屏页数(横屏双页时为 2) | | `preloadRadius` | `Int` | 预加载半径(默认 1) | | `dataSource` | `RDEpubReaderDataSource?` | 旧版数据源 | | `pageProvider` | `RDEpubReaderPageProvider?` | 推荐数据源 | | `delegate` | `RDEpubReaderDelegate?` | 事件代理 | | `toolViewAnimationDuration` | `TimeInterval` | 工具栏动画时长(0.3s) | ### 3.2 关键方法 ```swift /// 切换显示模式(pageCurl / horizontalScroll / verticalScroll) public func switchReaderDisplayType(_ displayType: RDEpubReaderView.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` | `RDEpubReaderFlowLayout` | CollectionView 自定义布局 | | `spreadResolver` | `RDEpubReaderSpreadResolver` | 双页配对计算 | | `tapRegionHandler` | `RDEpubReaderTapRegionHandler` | 点击区域判定 | | `preloadController` | `RDEpubReaderPreloadController` | 页面预加载与缓存 | | `pagingController` | `RDEpubReaderPagingController` | 翻页状态管理 | --- ## 4. 翻页状态机:RDEpubReaderPagingController **文件:** `Paging/RDEpubReaderPagingController.swift` 管理 PageCurl 模式下的翻页请求队列,防止动画冲突。 ```swift struct RDEpubReaderPagingController { 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: RDEpubReaderView.DisplayType) -> Bool /// 完成翻页动画,返回待处理的请求 mutating func finishPageCurlTransition() -> PageTransitionRequest? /// 重置状态(用于故障恢复) mutating func resetPendingState() } ``` --- ## 5. 页面预加载:RDEpubReaderPreloadController **文件:** `Paging/RDEpubReaderPreloadController.swift` 负责在当前页周围预渲染页面视图,减少翻页时的白屏时间。 ### 5.1 核心机制 - **缓存签名(CacheSignature)**:基于 `displayType + isLandscape + pagesPerScreen + boundsSize` 生成签名,签名变化时清空缓存 - **双缓存池**:`preloadedPageViews`(预加载池)和 `pageCurlCachedViews`(PageCurl 缓存池) - **预测性预加载**:根据 `preferredForward` 方向多预加载一页 ### 5.2 关键方法 ```swift /// 为指定页获取视图(优先从缓存取) 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 结构体 ```swift struct Environment { let displayType: RDEpubReaderView.DisplayType let isLandscape: Bool let pagesPerScreen: Int let boundsSize: CGSize let landscapeDualPageEnabled: Bool let coverPageIndex: Int? let totalPages: Int let spreadResolver: RDEpubReaderSpreadResolver } ``` --- ## 6. 双页展开计算:RDEpubReaderSpreadResolver **文件:** `Paging/RDEpubReaderSpreadResolver.swift` 纯函数式结构体,负责双页模式下的页面配对和导航计算。 ```swift struct RDEpubReaderSpreadResolver { /// 判断是否为全屏页(封面页独占一屏) 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. 点击区域判定:RDEpubReaderTapRegionHandler **文件:** `Paging/RDEpubReaderTapRegionHandler.swift` 将屏幕三等分,判定点击属于左/中/右区域。 ```swift struct RDEpubReaderTapRegionHandler { func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDEpubReaderView.TapEvent } ``` **逻辑:** - 左 1/3 → `.left`(上一页),工具栏可见时改为 `.center` - 中 1/3 → `.center`(切换工具栏) - 右 1/3 → `.right`(下一页),工具栏可见时改为 `.center` --- ## 8. 流式布局:RDEpubReaderFlowLayout **文件:** `RDEpubReaderFlowLayout.swift` `UICollectionViewFlowLayout` 子类,支持水平滚动和垂直滚动两种模式。 ### 8.1 关键属性 | 属性 | 类型 | 说明 | |------|------|------| | `displayType` | `RDEpubReaderView.DisplayType` | 布局模式 | | `isLandscapeDualPage` | `Bool` | 是否横屏双页 | | `coverPageIndex` | `Int?` | 封面页索引 | | `pagesPerScreen` | `Int` | 每屏页数 | ### 8.2 协议 ```swift public protocol RDEpubReaderFlowLayoutDataSoure: NSObjectProtocol { func heigtOfVerticalScrollPage(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) -> CGFloat? } @objc public protocol RDEpubReaderFlowLayoutDelegate: NSObjectProtocol { func pageNum(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) } ``` ### 8.3 关键方法 ```swift /// 计算指定页码的 contentOffset func currentContentOffset(count: Int) -> CGPoint ``` ### 8.4 封面页布局逻辑 双页模式下,封面页占满整屏宽度,后续页面两两配对占半屏宽度。`coverAwareFrame(for:screenWidth:halfWidth:height:)` 方法根据页码计算对应的 frame。 --- ## 9. 内容 Cell:RDEpubReaderContentCell **文件:** `RDEpubReaderContentCell.swift` `UICollectionViewCell` 子类,用于 Scroll 模式下承载页面内容视图。 ```swift class RDEpubReaderContentCell: UICollectionViewCell { var containerView: UIView? // 设置时自动添加到 contentView,移除旧视图 } ``` --- ## 10. PageCurl 子控制器:RDEpubReaderPageChildViewController **文件:** `RDEpubReaderPageChildViewController.swift` `UIViewController` 子类,作为 `UIPageViewController` 的页面 VC。 ```swift class RDEpubReaderPageChildViewController: UIViewController { var contentView: UIView? // 内容视图,设置时自动安装到容器 var pageNum: Int // 对应页码 init(contentView: UIView?, pageNum: Int = 0) } ``` --- ## 11. Extension 汇总 ### 11.1 RDEpubReaderView+PageCurl 实现 `UIPageViewControllerDataSource` 和 `UIPageViewControllerDelegate`: - `pageViewController(_:viewControllerBefore:)` — 提供前一页 VC(RTL 时逻辑反转) - `pageViewController(_:viewControllerAfter:)` — 提供后一页 VC - `pageViewController(_:didFinishAnimating:...)` — 完成动画后更新 currentPage - `pageViewController(_:willTransitionTo:)` — 即将翻页时预加载 **特殊页码:** - `RDEpubReaderView.blankPageNum`(`Int.max`)— 双页模式下的空白页 - `RDEpubReaderView.blankEndPageNum`(`Int.max - 1`)— 末尾空白页 ### 11.2 RDEpubReaderView+CollectionView 实现 `UICollectionViewDataSource`、`RDEpubReaderFlowLayoutDelegate`、`RDEpubReaderFlowLayoutDataSoure`: - `collectionView(_:cellForItemAt:)` — 复用预加载视图或创建新 Cell - `collectionView(_:numberOfItemsInSection:)` — 返回总页数 - `pageNum(flowLayout:pageIndex:)` — 滚动时更新当前页码 ### 11.3 RDEpubReaderView+ContentAccess 提供内容视图的注册、复用和查询: ```swift /// 注册内容视图类型(类似 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 RDEpubReaderView+ToolView 管理顶部/底部工具栏的安装、显示/隐藏动画: ```swift /// 切换工具栏显示状态(点击中心区域触发) func tapCenter() /// 安装工具栏视图到指定位置 func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition) /// 更新工具栏高度约束 func updateToolViewHeightConstraintsIfNeeded() ``` **动画效果:** 顶部工具栏从上方滑入,底部工具栏从下方滑入。 --- ## 12. 设计模式总结 | 模式 | 应用 | |------|------| | **策略模式** | `DisplayType` 切换 PageCurl / Scroll 两种翻页策略 | | **适配器模式** | `RDEpubReaderLegacyDataSourceAdapter` 将旧 `RDEpubReaderDataSource` 适配为 `RDEpubReaderPageProvider` | | **命令队列** | `RDEpubReaderPagingController` 管理翻页请求队列 | | **缓存签名** | `RDEpubReaderPreloadController.CacheSignature` 检测环境变化自动失效 | | **关注点分离** | Extension 将不同功能拆分到独立文件 | | **纯函数** | `RDEpubReaderSpreadResolver` 和 `RDEpubReaderTapRegionHandler` 无状态计算 | --- ## 13. 数据流图 ``` 用户点击屏幕 │ ▼ RDEpubReaderTapRegionHandler.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() → 预加载周围页面 ```