- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
16 KiB
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(旧版数据源,已废弃)
@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(推荐数据源)
@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
@objc public protocol RDEpubReaderDelegate: NSObjectProtocol {
func pageNum(readerView: RDEpubReaderView, pageNum: Int)
@objc optional func readerViewOrientationWillChange(readerView: RDEpubReaderView, isLandscape: Bool)
}
2.4 RDEpubReaderPageNavigating
public protocol RDEpubReaderPageNavigating: AnyObject {
var currentPage: Int { get }
func reloadPages()
func transition(to page: Int, animated: Bool)
}
RDEpubReaderView 遵循此协议,提供统一的页面导航接口。
2.5 枚举类型
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 关键方法
/// 切换显示模式(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 模式下的翻页请求队列,防止动画冲突。
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 关键方法
/// 为指定页获取视图(优先从缓存取)
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: 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
纯函数式结构体,负责双页模式下的页面配对和导航计算。
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
将屏幕三等分,判定点击属于左/中/右区域。
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 协议
public protocol RDEpubReaderFlowLayoutDataSoure: NSObjectProtocol {
func heigtOfVerticalScrollPage(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) -> CGFloat?
}
@objc public protocol RDEpubReaderFlowLayoutDelegate: NSObjectProtocol {
func pageNum(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int)
}
8.3 关键方法
/// 计算指定页码的 contentOffset
func currentContentOffset(count: Int) -> CGPoint
8.4 封面页布局逻辑
双页模式下,封面页占满整屏宽度,后续页面两两配对占半屏宽度。coverAwareFrame(for:screenWidth:halfWidth:height:) 方法根据页码计算对应的 frame。
9. 内容 Cell:RDEpubReaderContentCell
文件: RDEpubReaderContentCell.swift
UICollectionViewCell 子类,用于 Scroll 模式下承载页面内容视图。
class RDEpubReaderContentCell: UICollectionViewCell {
var containerView: UIView? // 设置时自动添加到 contentView,移除旧视图
}
10. PageCurl 子控制器:RDEpubReaderPageChildViewController
文件: RDEpubReaderPageChildViewController.swift
UIViewController 子类,作为 UIPageViewController 的页面 VC。
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:)— 提供后一页 VCpageViewController(_:didFinishAnimating:...)— 完成动画后更新 currentPagepageViewController(_:willTransitionTo:)— 即将翻页时预加载
特殊页码:
RDEpubReaderView.blankPageNum(Int.max)— 双页模式下的空白页RDEpubReaderView.blankEndPageNum(Int.max - 1)— 末尾空白页
11.2 RDEpubReaderView+CollectionView
实现 UICollectionViewDataSource、RDEpubReaderFlowLayoutDelegate、RDEpubReaderFlowLayoutDataSoure:
collectionView(_:cellForItemAt:)— 复用预加载视图或创建新 CellcollectionView(_:numberOfItemsInSection:)— 返回总页数pageNum(flowLayout:pageIndex:)— 滚动时更新当前页码
11.3 RDEpubReaderView+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 RDEpubReaderView+ToolView
管理顶部/底部工具栏的安装、显示/隐藏动画:
/// 切换工具栏显示状态(点击中心区域触发)
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() → 预加载周围页面