- 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
456 lines
16 KiB
Markdown
456 lines
16 KiB
Markdown
# 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() → 预加载周围页面
|
||
```
|