refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- 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
This commit is contained in:
parent
d5a7755702
commit
d7fcda345d
18
.gitignore
vendored
18
.gitignore
vendored
@ -6,3 +6,21 @@ xcuserdata/
|
|||||||
.artifacts/
|
.artifacts/
|
||||||
.deriveddata/
|
.deriveddata/
|
||||||
.swift-module-cache/
|
.swift-module-cache/
|
||||||
|
|
||||||
|
# This repository checks in the Demo Pods project. Keep generated files for the
|
||||||
|
# local RDEpubReaderView pod visible even when a global gitignore excludes Pods.
|
||||||
|
!ReadViewDemo/Pods/
|
||||||
|
!ReadViewDemo/Pods/Local Podspecs/
|
||||||
|
!ReadViewDemo/Pods/Local Podspecs/RDEpubReaderView.podspec.json
|
||||||
|
!ReadViewDemo/Pods/Target Support Files/
|
||||||
|
!ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/
|
||||||
|
!ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/**
|
||||||
|
|
||||||
|
# macOS system files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Local backup directories
|
||||||
|
*手动解包备份*
|
||||||
|
|
||||||
|
# AI tool output
|
||||||
|
_ssoft-output/
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# ReadViewSDK 代码审查报告
|
# ReadViewSDK 代码审查报告
|
||||||
|
|
||||||
> 审查日期:2026-06-26
|
> 审查日期:2026-06-26
|
||||||
> 审查范围:Sources/RDReaderView 全部源码
|
> 审查范围:Sources/RDEpubReaderView 全部源码
|
||||||
> 审查方法:逐文件阅读 + 交叉验证 + 线程模型分析
|
> 审查方法:逐文件阅读 + 交叉验证 + 线程模型分析
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -148,4 +148,4 @@ func currentTextPageSize() -> CGSize {
|
|||||||
| 🟡 P2 | P2-2 | javaScriptStringLiteral 实现依赖隐式假设 | 可维护性 |
|
| 🟡 P2 | P2-2 | javaScriptStringLiteral 实现依赖隐式假设 | 可维护性 |
|
||||||
| 🔵 P3 | P3-1 | 后台线程同步 hop 主线程获取布局信息 | 线程模型 |
|
| 🔵 P3 | P3-1 | 后台线程同步 hop 主线程获取布局信息 | 线程模型 |
|
||||||
|
|
||||||
**整体评价**: SDK 架构设计良好,模块分层清晰,引用管理(unowned/weak)使用正确,分页取消机制有 `paginationToken` + `cancellationController` 兜底。主要值得修复的是三个性能类 P1 问题(主线程阻塞搜索、资源加载阻塞、缓存无清理),两个 P2 鲁棒性/可维护性问题,以及一个 P3 线程模型优化建议。
|
**整体评价**: SDK 架构设计良好,模块分层清晰,引用管理(unowned/weak)使用正确,分页取消机制有 `paginationToken` + `cancellationController` 兜底。主要值得修复的是三个性能类 P1 问题(主线程阻塞搜索、资源加载阻塞、缓存无清理),两个 P2 鲁棒性/可维护性问题,以及一个 P3 线程模型优化建议。
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## 1. RDEPUBReaderController — 入口控制器
|
## 1. RDEPUBReaderController — 入口控制器
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift` 及扩展
|
**文件**:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift` 及扩展
|
||||||
|
|
||||||
### 1.1 初始化
|
### 1.1 初始化
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ func nativeTextSemanticSummary() -> String?
|
|||||||
|
|
||||||
## 2. RDEPUBReaderDelegate — 委托协议
|
## 2. RDEPUBReaderDelegate — 委托协议
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBUI/RDEPUBReaderDelegate.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderDelegate.swift`
|
||||||
|
|
||||||
所有方法均有默认空实现,可按需实现。
|
所有方法均有默认空实现,可按需实现。
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ public protocol RDEPUBReaderDelegate: AnyObject {
|
|||||||
|
|
||||||
## 3. RDEPUBReaderPersistence — 持久化协议
|
## 3. RDEPUBReaderPersistence — 持久化协议
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public protocol RDEPUBReaderPersistence: AnyObject {
|
public protocol RDEPUBReaderPersistence: AnyObject {
|
||||||
@ -264,7 +264,7 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
|||||||
|
|
||||||
## 4. RDEPUBReaderConfiguration — 配置模型
|
## 4. RDEPUBReaderConfiguration — 配置模型
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift`
|
||||||
|
|
||||||
| 属性 | 类型 | 默认值 | 说明 |
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|------|------|--------|------|
|
|------|------|--------|------|
|
||||||
@ -273,7 +273,7 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
|||||||
| `fontChoice` | `RDEPUBReaderFontChoice` | `.system` | 字体选择(system/serif/rounded/monospaced) |
|
| `fontChoice` | `RDEPUBReaderFontChoice` | `.system` | 字体选择(system/serif/rounded/monospaced) |
|
||||||
| `numberOfColumns` | `Int` | `1` | 每页列数(1 或 2) |
|
| `numberOfColumns` | `Int` | `1` | 每页列数(1 或 2) |
|
||||||
| `columnGap` | `CGFloat` | `20` | 列间距 |
|
| `columnGap` | `CGFloat` | `20` | 列间距 |
|
||||||
| `displayType` | `RDReaderView.DisplayType` | `.pageCurl` | 翻页模式 |
|
| `displayType` | `RDEpubReaderView.DisplayType` | `.pageCurl` | 翻页模式 |
|
||||||
| `landscapeDualPageEnabled` | `Bool` | `true` | 横屏双页 |
|
| `landscapeDualPageEnabled` | `Bool` | `true` | 横屏双页 |
|
||||||
| `showsTableOfContents` | `Bool` | `true` | 显示目录 |
|
| `showsTableOfContents` | `Bool` | `true` | 显示目录 |
|
||||||
| `allowsHighlights` | `Bool` | `true` | 允许高亮 |
|
| `allowsHighlights` | `Bool` | `true` | 允许高亮 |
|
||||||
@ -301,50 +301,50 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
|||||||
|
|
||||||
## 5. 翻页容器协议
|
## 5. 翻页容器协议
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift`
|
**文件**:`Sources/RDEpubReaderView/ReaderView/RDEpubReaderViewProtocols.swift`
|
||||||
|
|
||||||
### 5.1 RDReaderPageProvider(推荐)
|
### 5.1 RDEpubReaderPageProvider(推荐)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
|
@objc public protocol RDEpubReaderPageProvider: NSObjectProtocol {
|
||||||
|
|
||||||
/// 总页数
|
/// 总页数
|
||||||
func numberOfPages(in readerView: RDReaderView) -> Int
|
func numberOfPages(in readerView: RDEpubReaderView) -> Int
|
||||||
|
|
||||||
/// 返回指定页的视图
|
/// 返回指定页的视图
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - index: 页码索引(0-based)
|
/// - index: 页码索引(0-based)
|
||||||
/// - reusableView: 可复用的旧视图(可能为 nil)
|
/// - reusableView: 可复用的旧视图(可能为 nil)
|
||||||
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
func readerView(_ readerView: RDEpubReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
||||||
|
|
||||||
/// 页面标识符(用于缓存去重)
|
/// 页面标识符(用于缓存去重)
|
||||||
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
|
@objc optional func pageIdentifier(in readerView: RDEpubReaderView, index: Int) -> String?
|
||||||
|
|
||||||
/// 顶部工具栏视图
|
/// 顶部工具栏视图
|
||||||
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
|
@objc optional func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||||
|
|
||||||
/// 底部工具栏视图
|
/// 底部工具栏视图
|
||||||
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
|
@objc optional func readerViewBottomChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 RDReaderDelegate
|
### 5.2 RDEpubReaderDelegate
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@objc public protocol RDReaderDelegate: NSObjectProtocol {
|
@objc public protocol RDEpubReaderDelegate: NSObjectProtocol {
|
||||||
|
|
||||||
/// 页面变化回调
|
/// 页面变化回调
|
||||||
func pageNum(readerView: RDReaderView, pageNum: Int)
|
func pageNum(readerView: RDEpubReaderView, pageNum: Int)
|
||||||
|
|
||||||
/// 屏幕方向即将变化
|
/// 屏幕方向即将变化
|
||||||
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
|
@objc optional func readerViewOrientationWillChange(readerView: RDEpubReaderView, isLandscape: Bool)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 RDReaderPageNavigating
|
### 5.3 RDEpubReaderPageNavigating
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public protocol RDReaderPageNavigating: AnyObject {
|
public protocol RDEpubReaderPageNavigating: AnyObject {
|
||||||
|
|
||||||
/// 当前页码
|
/// 当前页码
|
||||||
var currentPage: Int { get }
|
var currentPage: Int { get }
|
||||||
@ -357,19 +357,19 @@ public protocol RDReaderPageNavigating: AnyObject {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.4 RDReaderDataSource(遗留)
|
### 5.4 RDEpubReaderDataSource(遗留)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@objc public protocol RDReaderDataSource: NSObjectProtocol {
|
@objc public protocol RDEpubReaderDataSource: NSObjectProtocol {
|
||||||
func pageCountOfReaderView(readerView: RDReaderView) -> Int
|
func pageCountOfReaderView(readerView: RDEpubReaderView) -> Int
|
||||||
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
func pageContentView(readerView: RDEpubReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||||||
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
|
func pageIdentifier(readerView: RDEpubReaderView, pageNum: Int) -> String?
|
||||||
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
|
@objc optional func topToolView(readerView: RDEpubReaderView) -> UIView?
|
||||||
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
|
@objc optional func bottomToolView(readerView: RDEpubReaderView) -> UIView?
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
通过 `RDReaderLegacyDataSourceAdapter` 自动适配到 `RDReaderPageProvider`。
|
通过 `RDEpubReaderLegacyDataSourceAdapter` 自动适配到 `RDEpubReaderPageProvider`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK,支持文本重排(Reflowable
|
|||||||
│ ReadViewDemo (Demo App) │
|
│ ReadViewDemo (Demo App) │
|
||||||
│ ViewController · LaunchAutomationPlan · UITests │
|
│ ViewController · LaunchAutomationPlan · UITests │
|
||||||
└──────────────────────────────┬──────────────────────────────┘
|
└──────────────────────────────┬──────────────────────────────┘
|
||||||
│ imports RDReaderView
|
│ imports RDEpubReaderView
|
||||||
┌──────────────────────────────▼──────────────────────────────┐
|
┌──────────────────────────────▼──────────────────────────────┐
|
||||||
│ EPUBUI 层 │
|
│ EPUBUI 层 │
|
||||||
│ RDEPUBReaderController · Coordinators · Settings · TextPage │
|
│ RDEPUBReaderController · Coordinators · Settings · TextPage │
|
||||||
@ -30,8 +30,8 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK,支持文本重排(Reflowable
|
|||||||
└──────────────────────────────┬──────────────────────────────┘
|
└──────────────────────────────┬──────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌──────────────────────────────▼──────────────────────────────┐
|
┌──────────────────────────────▼──────────────────────────────┐
|
||||||
│ RDReaderView 层 │
|
│ RDEpubReaderView 层 │
|
||||||
│ RDReaderView · FlowLayout · PreloadController │
|
│ RDEpubReaderView · FlowLayout · PreloadController │
|
||||||
│ SpreadResolver · TapRegionHandler · PagingController │
|
│ SpreadResolver · TapRegionHandler · PagingController │
|
||||||
└──────────────────────────────┬──────────────────────────────┘
|
└──────────────────────────────┬──────────────────────────────┘
|
||||||
│
|
│
|
||||||
@ -54,7 +54,7 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK,支持文本重排(Reflowable
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **EPUBCore** | EPUB 文件解析、资源管理、WebView 渲染、JS 桥接 | `RDEPUBParser`, `RDEPUBPublication`, `RDEPUBWebView` |
|
| **EPUBCore** | EPUB 文件解析、资源管理、WebView 渲染、JS 桥接 | `RDEPUBParser`, `RDEPUBPublication`, `RDEPUBWebView` |
|
||||||
| **EPUBTextRendering** | HTML→NSAttributedString 转换、排版、分页、全书构建 | `RDEPUBTextBookBuilder`, `RDEPUBCoreTextPageFrameFactory` |
|
| **EPUBTextRendering** | HTML→NSAttributedString 转换、排版、分页、全书构建 | `RDEPUBTextBookBuilder`, `RDEPUBCoreTextPageFrameFactory` |
|
||||||
| **RDReaderView** | 通用翻页容器、手势识别、页面预加载、双页布局 | `RDReaderView`, `RDReaderFlowLayout`, `RDReaderPreloadController` |
|
| **RDEpubReaderView** | 通用翻页容器、手势识别、页面预加载、双页布局 | `RDEpubReaderView`, `RDEpubReaderFlowLayout`, `RDEpubReaderPreloadController` |
|
||||||
| **EPUBUI** | 阅读器控制器、协调器模式、设置面板、按需加载、磁盘缓存 | `RDEPUBReaderController`, `RDEPUBReaderPaginationCoordinator` |
|
| **EPUBUI** | 阅读器控制器、协调器模式、设置面板、按需加载、磁盘缓存 | `RDEPUBReaderController`, `RDEPUBReaderPaginationCoordinator` |
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -310,10 +310,10 @@ RDEPUBWebView (UIView)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 翻页容器架构(RDReaderView)
|
## 7. 翻页容器架构(RDEpubReaderView)
|
||||||
|
|
||||||
```
|
```
|
||||||
RDReaderView (UIView)
|
RDEpubReaderView (UIView)
|
||||||
│
|
│
|
||||||
├─ 三种翻页模式:
|
├─ 三种翻页模式:
|
||||||
│ ├─ .pageCurl → UIPageViewController (翻页动画)
|
│ ├─ .pageCurl → UIPageViewController (翻页动画)
|
||||||
@ -321,14 +321,14 @@ RDReaderView (UIView)
|
|||||||
│ └─ .verticalScroll → UICollectionView (垂直滚动)
|
│ └─ .verticalScroll → UICollectionView (垂直滚动)
|
||||||
│
|
│
|
||||||
├─ 组合对象:
|
├─ 组合对象:
|
||||||
│ ├─ RDReaderPagingController // 翻页状态管理、请求队列
|
│ ├─ RDEpubReaderPagingController // 翻页状态管理、请求队列
|
||||||
│ ├─ RDReaderPreloadController // 页面预加载、缓存管理
|
│ ├─ RDEpubReaderPreloadController // 页面预加载、缓存管理
|
||||||
│ ├─ RDReaderSpreadResolver // 双页展开计算
|
│ ├─ RDEpubReaderSpreadResolver // 双页展开计算
|
||||||
│ └─ RDReaderTapRegionHandler // 点击区域分类(左/中/右)
|
│ └─ RDEpubReaderTapRegionHandler // 点击区域分类(左/中/右)
|
||||||
│
|
│
|
||||||
├─ 数据源协议:
|
├─ 数据源协议:
|
||||||
│ ├─ RDReaderPageProvider (新) // 格式无关,优先级高
|
│ ├─ RDEpubReaderPageProvider (新) // 格式无关,优先级高
|
||||||
│ └─ RDReaderDataSource (旧) // 遗留兼容,通过 Adapter 适配
|
│ └─ RDEpubReaderDataSource (旧) // 遗留兼容,通过 Adapter 适配
|
||||||
│
|
│
|
||||||
└─ 手势流:
|
└─ 手势流:
|
||||||
点击 → TapRegionHandler → 分类(左/中/右)
|
点击 → TapRegionHandler → 分类(左/中/右)
|
||||||
@ -351,7 +351,7 @@ struct RDEPUBReaderConfiguration {
|
|||||||
var numberOfColumns: Int // 1 或 2
|
var numberOfColumns: Int // 1 或 2
|
||||||
var columnGap: CGFloat // 默认 20
|
var columnGap: CGFloat // 默认 20
|
||||||
var theme: ReaderTheme // 6 种主题
|
var theme: ReaderTheme // 6 种主题
|
||||||
var displayType: RDReaderView.DisplayType // pageCurl/horizontal/vertical
|
var displayType: RDEpubReaderView.DisplayType // pageCurl/horizontal/vertical
|
||||||
var onDemandChapterWindowSize: Int // 默认 3(奇数,最小 3,最大 15)
|
var onDemandChapterWindowSize: Int // 默认 3(奇数,最小 3,最大 15)
|
||||||
var metadataParsingConcurrency: Int // 默认 CPU 核心数
|
var metadataParsingConcurrency: Int // 默认 CPU 核心数
|
||||||
var chapterWindowRadius: Int // 内存缓存窗口半径
|
var chapterWindowRadius: Int // 内存缓存窗口半径
|
||||||
@ -374,7 +374,7 @@ struct RDEPUBReaderConfiguration {
|
|||||||
## 9. 目录结构
|
## 9. 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
Sources/RDReaderView/
|
Sources/RDEpubReaderView/
|
||||||
├── EPUBCore/ # EPUB 解析与 WebView 渲染
|
├── EPUBCore/ # EPUB 解析与 WebView 渲染
|
||||||
│ ├── Models/ # 数据模型
|
│ ├── Models/ # 数据模型
|
||||||
│ │ ├── RDEPUBAnnotationModels.swift
|
│ │ ├── RDEPUBAnnotationModels.swift
|
||||||
@ -428,19 +428,19 @@ Sources/RDReaderView/
|
|||||||
│ ├── RDEPUBDTCoreTextRenderer.swift # DTCoreText 渲染器实现
|
│ ├── RDEPUBDTCoreTextRenderer.swift # DTCoreText 渲染器实现
|
||||||
│ ├── RDEPUBChapterData.swift # 章节查询门面
|
│ ├── RDEPUBChapterData.swift # 章节查询门面
|
||||||
│ ├── RDEPUBTextIndexTable.swift # 全书索引表
|
│ ├── RDEPUBTextIndexTable.swift # 全书索引表
|
||||||
│ └── RDPlainTextBookBuilder.swift # 纯文本 (.txt) 构建器
|
│ └── RDEpubPlainTextBookBuilder.swift # 纯文本 (.txt) 构建器
|
||||||
│
|
│
|
||||||
├── ReaderView/ # 通用翻页容器
|
├── ReaderView/ # 通用翻页容器
|
||||||
│ ├── Paging/ # 翻页子系统
|
│ ├── Paging/ # 翻页子系统
|
||||||
│ │ ├── RDReaderPagingController.swift
|
│ │ ├── RDEpubReaderPagingController.swift
|
||||||
│ │ ├── RDReaderPreloadController.swift
|
│ │ ├── RDEpubReaderPreloadController.swift
|
||||||
│ │ ├── RDReaderSpreadResolver.swift
|
│ │ ├── RDEpubReaderSpreadResolver.swift
|
||||||
│ │ └── RDReaderTapRegionHandler.swift
|
│ │ └── RDEpubReaderTapRegionHandler.swift
|
||||||
│ ├── RDReaderView.swift # 主容器视图
|
│ ├── RDEpubReaderView.swift # 主容器视图
|
||||||
│ ├── RDReaderView+*.swift # 扩展(CollectionView/PageCurl/ToolView/ContentAccess)
|
│ ├── RDEpubReaderView+*.swift # 扩展(CollectionView/PageCurl/ToolView/ContentAccess)
|
||||||
│ ├── RDReaderFlowLayout.swift # 自定义 CollectionView 布局
|
│ ├── RDEpubReaderFlowLayout.swift # 自定义 CollectionView 布局
|
||||||
│ ├── RDReaderContentCell.swift # 滚动模式 Cell
|
│ ├── RDEpubReaderContentCell.swift # 滚动模式 Cell
|
||||||
│ └── RDReaderPageChildViewController.swift // 翻页模式子 VC
|
│ └── RDEpubReaderPageChildViewController.swift // 翻页模式子 VC
|
||||||
│
|
│
|
||||||
└── EPUBUI/ # 阅读器 UI
|
└── EPUBUI/ # 阅读器 UI
|
||||||
├── ReaderController/ # 控制器与协调器
|
├── ReaderController/ # 控制器与协调器
|
||||||
@ -494,7 +494,7 @@ Sources/RDReaderView/
|
|||||||
| **Builder** | `RDEPUBBookPageMap.Builder` 增量构建页码映射 |
|
| **Builder** | `RDEPUBBookPageMap.Builder` 增量构建页码映射 |
|
||||||
| **Strategy** | `RDEPUBTextRenderer` 协议,可替换渲染器实现 |
|
| **Strategy** | `RDEPUBTextRenderer` 协议,可替换渲染器实现 |
|
||||||
| **Pipeline** | `RDEPUBTextTypesetterPipeline` 排版管线(8 个逻辑阶段,封装为 5-6 个顶层调用) |
|
| **Pipeline** | `RDEPUBTextTypesetterPipeline` 排版管线(8 个逻辑阶段,封装为 5-6 个顶层调用) |
|
||||||
| **Adapter** | `RDReaderLegacyDataSourceAdapter` 适配旧数据源协议 |
|
| **Adapter** | `RDEpubReaderLegacyDataSourceAdapter` 适配旧数据源协议 |
|
||||||
| **三级缓存** | 内存 → 磁盘摘要 → 全书分页,逐级降级 |
|
| **三级缓存** | 内存 → 磁盘摘要 → 全书分页,逐级降级 |
|
||||||
| **Token 取消** | `paginationToken` 确保过期异步任务不干扰新任务 |
|
| **Token 取消** | `paginationToken` 确保过期异步任务不干扰新任务 |
|
||||||
| **Frozen Parameters** | 后台任务冻结 `renderSignature`,避免运行中参数漂移 |
|
| **Frozen Parameters** | 后台任务冻结 `renderSignature`,避免运行中参数漂移 |
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# ReadViewSDK 代码审查报告
|
# ReadViewSDK 代码审查报告
|
||||||
|
|
||||||
> 审查范围:`Sources/RDReaderView/` 全部 184 个 Swift 文件(约 32,300 行)
|
> 审查范围:`Sources/RDEpubReaderView/` 全部 184 个 Swift 文件(约 32,300 行)
|
||||||
> 审查维度:并发安全、内存管理、API 正确性、Swift/iOS 平台特定、架构与依赖
|
> 审查维度:并发安全、内存管理、API 正确性、Swift/iOS 平台特定、架构与依赖
|
||||||
> 审查日期:2026-06-26
|
> 审查日期:2026-06-26
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ let stillChanged = abs(currentSize.width - self.lastTextPaginationPageSize!.widt
|
|||||||
|
|
||||||
### H-10 · 依赖版本未锁定
|
### H-10 · 依赖版本未锁定
|
||||||
|
|
||||||
`RDReaderView.podspec` 中 DTCoreText、SnapKit、SSAlertSwift 无版本约束:
|
`RDEpubReaderView.podspec` 中 DTCoreText、SnapKit、SSAlertSwift 无版本约束:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
s.dependency 'ZIPFoundation', '~> 0.9' # ✅ 已锁定
|
s.dependency 'ZIPFoundation', '~> 0.9' # ✅ 已锁定
|
||||||
@ -143,7 +143,7 @@ s.dependency 'SnapKit' # ❌ 无约束
|
|||||||
s.dependency 'SSAlertSwift' # ❌ 无约束
|
s.dependency 'SSAlertSwift' # ❌ 无约束
|
||||||
```
|
```
|
||||||
|
|
||||||
**位置:** `RDReaderView.podspec:17-20`
|
**位置:** `RDEpubReaderView.podspec:17-20`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -298,14 +298,14 @@ return builder.build()
|
|||||||
|
|
||||||
**触发条件:** `landscapeDualPageEnabled && isLandscape && !verticalScroll`
|
**触发条件:** `landscapeDualPageEnabled && isLandscape && !verticalScroll`
|
||||||
|
|
||||||
**页面配对逻辑(RDReaderSpreadResolver):**
|
**页面配对逻辑(RDEpubReaderSpreadResolver):**
|
||||||
|
|
||||||
- 有封面页:封面独占一屏,后续页面两两配对
|
- 有封面页:封面独占一屏,后续页面两两配对
|
||||||
- 配对规则:`(coverIndex, nil)`, `(1, 2)`, `(3, 4)`, ...
|
- 配对规则:`(coverIndex, nil)`, `(1, 2)`, `(3, 4)`, ...
|
||||||
- 无封面页:标准偶奇配对
|
- 无封面页:标准偶奇配对
|
||||||
- 配对规则:`(0, 1)`, `(2, 3)`, `(4, 5)`, ...
|
- 配对规则:`(0, 1)`, `(2, 3)`, `(4, 5)`, ...
|
||||||
|
|
||||||
**封面感知布局(RDReaderFlowLayout):**
|
**封面感知布局(RDEpubReaderFlowLayout):**
|
||||||
|
|
||||||
```
|
```
|
||||||
封面页: [====全屏宽度====]
|
封面页: [====全屏宽度====]
|
||||||
@ -314,7 +314,7 @@ return builder.build()
|
|||||||
|
|
||||||
### 5.3 页面预加载
|
### 5.3 页面预加载
|
||||||
|
|
||||||
`RDReaderPreloadController` 管理两个缓存:
|
`RDEpubReaderPreloadController` 管理两个缓存:
|
||||||
- `preloadedPageViews` - 预渲染的页面视图
|
- `preloadedPageViews` - 预渲染的页面视图
|
||||||
- `pageCurlCachedViews` - 当前显示的页面视图
|
- `pageCurlCachedViews` - 当前显示的页面视图
|
||||||
|
|
||||||
@ -580,7 +580,7 @@ protocol RDEPUBReaderPersistence {
|
|||||||
|
|
||||||
## 9. 纯文本 (.txt) 支持
|
## 9. 纯文本 (.txt) 支持
|
||||||
|
|
||||||
`RDPlainTextBookBuilder` 复用 EPUB 渲染管线处理 .txt 文件:
|
`RDEpubPlainTextBookBuilder` 复用 EPUB 渲染管线处理 .txt 文件:
|
||||||
|
|
||||||
1. **解码:** 依次尝试 UTF-8 → GB18030 → GBK
|
1. **解码:** 依次尝试 UTF-8 → GB18030 → GBK
|
||||||
2. **分章:** 正则匹配 `^(第[零一二三四五六七八九十百千万\d]+[章节回卷].*)$`
|
2. **分章:** 正则匹配 `^(第[零一二三四五六七八九十百千万\d]+[章节回卷].*)$`
|
||||||
|
|||||||
@ -34,7 +34,7 @@ epubcfi(/6/4!ch01.xhtml/4/2/1:3)
|
|||||||
|
|
||||||
顶层 CFI 模型,对应一个完整的 `epubcfi(...)` 字符串。
|
顶层 CFI 模型,对应一个完整的 `epubcfi(...)` 字符串。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFI.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFI.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public struct RDEPUBCFI: Codable, Equatable, Hashable {
|
public struct RDEPUBCFI: Codable, Equatable, Hashable {
|
||||||
@ -66,7 +66,7 @@ public enum RDEPUBCFISideBias: String, Codable {
|
|||||||
|
|
||||||
路径模型,由一组 `RDEPUBCFIStep` 组成,描述从根节点到目标节点的遍历序列。
|
路径模型,由一组 `RDEPUBCFIStep` 组成,描述从根节点到目标节点的遍历序列。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public struct RDEPUBCFIPath: Codable, Equatable, Hashable {
|
public struct RDEPUBCFIPath: Codable, Equatable, Hashable {
|
||||||
@ -95,7 +95,7 @@ public struct RDEPUBCFIStep: Codable, Equatable, Hashable {
|
|||||||
|
|
||||||
范围模型,表示文档中的一个连续区域,由父级 CFI 和起止 CFI 组成。
|
范围模型,表示文档中的一个连续区域,由父级 CFI 和起止 CFI 组成。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRange.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIRange.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
|
public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
|
||||||
@ -116,7 +116,7 @@ public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
|
|||||||
|
|
||||||
将 CFI 字符串解析为结构化模型。
|
将 CFI 字符串解析为结构化模型。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public enum RDEPUBCFIParser {
|
public enum RDEPUBCFIParser {
|
||||||
@ -154,7 +154,7 @@ print(cfi.textAssertion?.exact) // Optional("目标文本")
|
|||||||
|
|
||||||
将 CFI 模型序列化为标准字符串。
|
将 CFI 模型序列化为标准字符串。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public enum RDEPUBCFISerializer {
|
public enum RDEPUBCFISerializer {
|
||||||
@ -192,7 +192,7 @@ let serialized = RDEPUBCFISerializer.serialize(cfi)
|
|||||||
|
|
||||||
将 CFI 解析为可直接用于资源定位的结果。
|
将 CFI 解析为可直接用于资源定位的结果。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public struct RDEPUBCFIResolverResult: Equatable {
|
public struct RDEPUBCFIResolverResult: Equatable {
|
||||||
@ -228,7 +228,7 @@ print(result.chapterOffset) // Optional(80)
|
|||||||
|
|
||||||
从已知的章节信息生成 CFI。
|
从已知的章节信息生成 CFI。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public enum RDEPUBCFIGenerator {
|
public enum RDEPUBCFIGenerator {
|
||||||
@ -274,7 +274,7 @@ public enum RDEPUBCFIGenerator {
|
|||||||
|
|
||||||
从 HTML 源码中提取带有 `id` 属性的元素路径映射。
|
从 HTML 源码中提取带有 `id` 属性的元素路径映射。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public enum RDEPUBCFIDOMPathBuilder {
|
public enum RDEPUBCFIDOMPathBuilder {
|
||||||
@ -296,7 +296,7 @@ public enum RDEPUBCFIDOMPathBuilder {
|
|||||||
|
|
||||||
当 CFI 精确定位失败时(如 DOM 结构变更),恢复引擎通过多级降级策略尝试找到最佳匹配位置。
|
当 CFI 精确定位失败时(如 DOM 结构变更),恢复引擎通过多级降级策略尝试找到最佳匹配位置。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift`
|
||||||
|
|
||||||
### 5.1 恢复置信度
|
### 5.1 恢复置信度
|
||||||
|
|
||||||
@ -361,7 +361,7 @@ let result = RDEPUBCFIRecoveryEngine.recover(
|
|||||||
|
|
||||||
文本断言用于在 CFI 定位后验证所指位置的文本内容是否符合预期,增强定位鲁棒性。
|
文本断言用于在 CFI 定位后验证所指位置的文本内容是否符合预期,增强定位鲁棒性。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public struct RDEPUBCFITextAssertion: Codable, Equatable, Hashable {
|
public struct RDEPUBCFITextAssertion: Codable, Equatable, Hashable {
|
||||||
@ -393,7 +393,7 @@ print(assertion?.suffix) // Optional("后续内容")
|
|||||||
|
|
||||||
提供宽松解析接口,兼容非标准 CFI 格式。
|
提供宽松解析接口,兼容非标准 CFI 格式。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFICompatibility.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFICompatibility.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public enum RDEPUBCFICompatibility {
|
public enum RDEPUBCFICompatibility {
|
||||||
@ -418,7 +418,7 @@ public enum RDEPUBCFICompatibility {
|
|||||||
|
|
||||||
CFI 映射是章节级别的索引结构,将 CFI 路径映射到章节文本偏移量,是容错恢复引擎的核心数据源。
|
CFI 映射是章节级别的索引结构,将 CFI 路径映射到章节文本偏移量,是容错恢复引擎的核心数据源。
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift`
|
||||||
|
|
||||||
### 8.1 RDEPUBCFIMap
|
### 8.1 RDEPUBCFIMap
|
||||||
|
|
||||||
@ -506,7 +506,7 @@ public struct RDEPUBCFITokenAnchor: Codable, Equatable {
|
|||||||
|
|
||||||
## 9. 错误类型(RDEPUBCFIError)
|
## 9. 错误类型(RDEPUBCFIError)
|
||||||
|
|
||||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIError.swift`
|
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIError.swift`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public enum RDEPUBCFIError: Error, Equatable {
|
public enum RDEPUBCFIError: Error, Equatable {
|
||||||
|
|||||||
@ -16,8 +16,8 @@
|
|||||||
- 渐进补全:后台逐步补全所有章节的页码信息
|
- 渐进补全:后台逐步补全所有章节的页码信息
|
||||||
|
|
||||||
**关键文件**:
|
**关键文件**:
|
||||||
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/` 目录
|
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/ChapterRuntime/` 目录
|
||||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift`
|
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -105,15 +105,15 @@
|
|||||||
## Evidence(仓库路径)
|
## Evidence(仓库路径)
|
||||||
|
|
||||||
- Build flags:`Podfile`
|
- Build flags:`Podfile`
|
||||||
- Pod metadata/toolchain:`RDReaderView.podspec`
|
- Pod metadata/toolchain:`RDEpubReaderView.podspec`
|
||||||
- Archive extraction:`Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift`
|
- Archive extraction:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser+Archive.swift`
|
||||||
- Resource path validation(post-extraction):`Sources/RDReaderView/EPUBCore/RDEPUBParser+Resources.swift`
|
- Resource path validation(post-extraction):`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser+Resources.swift`
|
||||||
- Scheme handler reads full file bytes:`Sources/RDReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift`
|
- Scheme handler reads full file bytes:`Sources/RDEpubReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift`
|
||||||
- WebView bridge + message handling:`Sources/RDReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift`
|
- WebView bridge + message handling:`Sources/RDEpubReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift`
|
||||||
- External link opening:`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
|
- External link opening:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift`
|
||||||
- UserDefaults persistence implementation:`Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
- UserDefaults persistence implementation:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
||||||
- Hidden paginator web view:`Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift`
|
- Hidden paginator web view:`Sources/RDEpubReaderView/EPUBCore/RDEPUBPaginator.swift`
|
||||||
- Debug logging:`Sources/RDReaderView/EPUBCore/RDEPUBWebViewDebug.swift`
|
- Debug logging:`Sources/RDEpubReaderView/EPUBCore/RDEPUBWebViewDebug.swift`
|
||||||
- Vendored dependencies:`Pods/`、`ReadViewDemo/Pods/`
|
- Vendored dependencies:`Pods/`、`ReadViewDemo/Pods/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -6,43 +6,43 @@
|
|||||||
|
|
||||||
## 语言与工程约束
|
## 语言与工程约束
|
||||||
|
|
||||||
- **主要语言**:Swift(Podspec 声明 `s.swift_versions = ["5.10"]`,见 `RDReaderView.podspec`)
|
- **主要语言**:Swift(Podspec 声明 `s.swift_versions = ["5.10"]`,见 `RDEpubReaderView.podspec`)
|
||||||
- **最低系统版本**:Podspec `iOS 15.0`(`RDReaderView.podspec`),示例工程 Podfile/构建设置里常见为 `iOS 15.6`(`Podfile`)
|
- **最低系统版本**:Podspec `iOS 15.0`(`RDEpubReaderView.podspec`),示例工程 Podfile/构建设置里常见为 `iOS 15.6`(`Podfile`)
|
||||||
- **依赖管理**:CocoaPods(`Podfile`、`ReadViewDemo/Podfile`、`Podfile.lock`)
|
- **依赖管理**:CocoaPods(`Podfile`、`ReadViewDemo/Podfile`、`Podfile.lock`)
|
||||||
|
|
||||||
## 命名约定
|
## 命名约定
|
||||||
|
|
||||||
**文件/类型命名(Swift):**
|
**文件/类型命名(Swift):**
|
||||||
- 以类型名为文件名的单文件组织较常见:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
|
- 以类型名为文件名的单文件组织较常见:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`
|
||||||
- 大量使用前缀区分模块域:
|
- 大量使用前缀区分模块域:
|
||||||
- `RD...`:阅读器 UI/控制器相关(如 `Sources/RDReaderView/RDReaderView.swift`、`Sources/RDReaderView/RDURLReaderController.swift`)
|
- `RD...`:阅读器 UI/控制器相关(如 `Sources/RDEpubReaderView/RDEpubReaderView.swift`、`Sources/RDEpubReaderView/RDEpubURLReaderController.swift`)
|
||||||
- `RDEPUB...`:EPUB Core/UI/渲染相关(如 `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
- `RDEPUB...`:EPUB Core/UI/渲染相关(如 `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||||
- Extension 文件使用 `+` 命名:`Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift`
|
- Extension 文件使用 `+` 命名:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift`
|
||||||
|
|
||||||
**变量/函数命名:**
|
**变量/函数命名:**
|
||||||
- 基本遵循 Swift lowerCamelCase:`parse(epubURL:)`(`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`)
|
- 基本遵循 Swift lowerCamelCase:`parse(epubURL:)`(`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`)
|
||||||
- 常量多用 `static let`:`kRDEPUBHighlightAttributeName`(`Sources/RDReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift`)
|
- 常量多用 `static let`:`kRDEPUBHighlightAttributeName`(`Sources/RDEpubReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift`)
|
||||||
|
|
||||||
## 代码风格与排版(从现有代码归纳)
|
## 代码风格与排版(从现有代码归纳)
|
||||||
|
|
||||||
**缩进与换行:**
|
**缩进与换行:**
|
||||||
- 多数文件使用 4 空格缩进(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift`、`Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
- 多数文件使用 4 空格缩进(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift`、`Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||||
- 历史代码中更常见”强制换行/多行括号”风格
|
- 历史代码中更常见”强制换行/多行括号”风格
|
||||||
|
|
||||||
**空行与分组:**
|
**空行与分组:**
|
||||||
- UI 相关文件常用空行分隔属性/初始化/布局段落
|
- UI 相关文件常用空行分隔属性/初始化/布局段落
|
||||||
- `// MARK:` 用于分区组织(示例:`Sources/RDReaderView/RDReaderGestureController.swift`、`Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift`)
|
- `// MARK:` 用于分区组织(示例:`Sources/RDEpubReaderView/RDEpubReaderGestureController.swift`、`Sources/RDEpubReaderView/EPUBTextRendering/RDEpubPlainTextBookBuilder.swift`)
|
||||||
|
|
||||||
**类型组织:**
|
**类型组织:**
|
||||||
- 偏好用 `extension` 拆分职责/协议实现(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift` 的 `UITableViewDataSource/Delegate`)
|
- 偏好用 `extension` 拆分职责/协议实现(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift` 的 `UITableViewDataSource/Delegate`)
|
||||||
- API 暴露处使用 `public`、`public final class`、`public enum/struct`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
- API 暴露处使用 `public`、`public final class`、`public enum/struct`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||||
- “对外只读、内部可写”常用 `public internal(set)`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`)
|
- “对外只读、内部可写”常用 `public internal(set)`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`)
|
||||||
|
|
||||||
## 导入与依赖使用
|
## 导入与依赖使用
|
||||||
|
|
||||||
**import:**
|
**import:**
|
||||||
- UIKit/UI 文件:`import UIKit`(大量文件)
|
- UIKit/UI 文件:`import UIKit`(大量文件)
|
||||||
- Core/模型文件:`import Foundation`(如 `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
- Core/模型文件:`import Foundation`(如 `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||||
- 三方依赖按需引入:
|
- 三方依赖按需引入:
|
||||||
- `SnapKit`:布局
|
- `SnapKit`:布局
|
||||||
- `SSAlertSwift`:弹窗/提示
|
- `SSAlertSwift`:弹窗/提示
|
||||||
@ -52,15 +52,15 @@
|
|||||||
|
|
||||||
## 错误处理与日志
|
## 错误处理与日志
|
||||||
|
|
||||||
- Core 解析层倾向用 `throws` + 自定义 `Error`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`、`Sources/RDReaderView/EPUBCore/RDEPUBModels.swift` 的 `RDEPUBParserError`)
|
- Core 解析层倾向用 `throws` + 自定义 `Error`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`、`Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift` 的 `RDEPUBParserError`)
|
||||||
- UI/控制器层常见 `guard` 早返回(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift`)
|
- UI/控制器层常见 `guard` 早返回(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift`)
|
||||||
- 未检测到统一日志框架(未发现专用 logging package/config);出现时以系统 API/局部输出为主(需按具体文件核对)。
|
- 未检测到统一日志框架(未发现专用 logging package/config);出现时以系统 API/局部输出为主(需按具体文件核对)。
|
||||||
|
|
||||||
## 注释与文档
|
## 注释与文档
|
||||||
|
|
||||||
- **项目规则(强约束)**:代码标识符保持英文,但**代码注释/文档/提交信息使用中文**(见 `CONTEXT.md`)。
|
- **项目规则(强约束)**:代码标识符保持英文,但**代码注释/文档/提交信息使用中文**(见 `CONTEXT.md`)。
|
||||||
- 历史文件常带 Xcode 头部注释块(示例:`Sources/RDReaderView/ReaderView/RDReaderView.swift`)。
|
- 历史文件常带 Xcode 头部注释块(示例:`Sources/RDEpubReaderView/ReaderView/RDEpubReaderView.swift`)。
|
||||||
- 公共 API 处存在少量三斜线文档注释(示例:`Sources/RDReaderView/RDReaderView.swift` 的中文说明)。
|
- 公共 API 处存在少量三斜线文档注释(示例:`Sources/RDEpubReaderView/RDEpubReaderView.swift` 的中文说明)。
|
||||||
|
|
||||||
## Lint / Formatter / 静态检查
|
## Lint / Formatter / 静态检查
|
||||||
|
|
||||||
@ -77,9 +77,9 @@
|
|||||||
|
|
||||||
- `CONTEXT.md`
|
- `CONTEXT.md`
|
||||||
- `Podfile`
|
- `Podfile`
|
||||||
- `RDReaderView.podspec`
|
- `RDEpubReaderView.podspec`
|
||||||
- `ReadViewDemo/ReadViewDemo/ViewController.swift`
|
- `ReadViewDemo/ReadViewDemo/ViewController.swift`
|
||||||
- `Sources/RDReaderView/ReaderView/RDReaderView.swift`
|
- `Sources/RDEpubReaderView/ReaderView/RDEpubReaderView.swift`
|
||||||
- `Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
|
- `Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`
|
||||||
- `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`
|
- `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`
|
||||||
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
|
- `Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift`
|
||||||
|
|||||||
@ -471,7 +471,7 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
|||||||
|
|
||||||
## 9. 纯文本构建器
|
## 9. 纯文本构建器
|
||||||
|
|
||||||
**文件:** `RDPlainTextBookBuilder.swift`
|
**文件:** `RDEpubPlainTextBookBuilder.swift`
|
||||||
|
|
||||||
从纯文本(.txt)文件构建 `RDEPUBTextBook`,用于支持 TXT 格式阅读。
|
从纯文本(.txt)文件构建 `RDEPUBTextBook`,用于支持 TXT 格式阅读。
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
let epubURL: URL // EPUB 文件路径
|
let epubURL: URL // EPUB 文件路径
|
||||||
let persistence: RDEPUBReaderPersistence? // 持久化代理
|
let persistence: RDEPUBReaderPersistence? // 持久化代理
|
||||||
let dependencies: RDEPUBReaderDependencies // 依赖注入
|
let dependencies: RDEPUBReaderDependencies // 依赖注入
|
||||||
let readerView = RDReaderView() // 翻页容器
|
let readerView = RDEpubReaderView() // 翻页容器
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `RDEPUBReaderController+PublicAPI.swift` | 公开 API(跳转、搜索、标注、书签) |
|
| `RDEPUBReaderController+PublicAPI.swift` | 公开 API(跳转、搜索、标注、书签) |
|
||||||
| `RDEPUBReaderController+DataSource.swift` | `RDReaderPageProvider` 实现 |
|
| `RDEPUBReaderController+DataSource.swift` | `RDEpubReaderPageProvider` 实现 |
|
||||||
| `RDEPUBReaderController+ContentDelegates.swift` | WebView/TextContentView 代理路由 |
|
| `RDEPUBReaderController+ContentDelegates.swift` | WebView/TextContentView 代理路由 |
|
||||||
| `RDEPUBReaderController+LocationResolution.swift` | 页码/位置解析、阅读状态同步 |
|
| `RDEPUBReaderController+LocationResolution.swift` | 页码/位置解析、阅读状态同步 |
|
||||||
| `RDEPUBReaderController+ExternalLinks.swift` | 外部链接处理(白名单、确认弹窗) |
|
| `RDEPUBReaderController+ExternalLinks.swift` | 外部链接处理(白名单、确认弹窗) |
|
||||||
@ -120,7 +120,7 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
|||||||
```swift
|
```swift
|
||||||
final class RDEPUBReaderContext {
|
final class RDEPUBReaderContext {
|
||||||
weak var controller: RDEPUBReaderController?
|
weak var controller: RDEPUBReaderController?
|
||||||
weak var readerView: RDReaderView?
|
weak var readerView: RDEpubReaderView?
|
||||||
var dependencies: RDEPUBReaderDependencies
|
var dependencies: RDEPUBReaderDependencies
|
||||||
var runtime: RDEPUBReaderRuntime?
|
var runtime: RDEPUBReaderRuntime?
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ public struct RDEPUBReaderDependencies {
|
|||||||
public var makeParser: () -> RDEPUBParser
|
public var makeParser: () -> RDEPUBParser
|
||||||
public var makePaginator: () -> RDEPUBPaginator
|
public var makePaginator: () -> RDEPUBPaginator
|
||||||
public var makeTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextBookCache?, RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder
|
public var makeTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextBookCache?, RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder
|
||||||
public var makePlainTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder
|
public var makePlainTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDEpubPlainTextBookBuilder
|
||||||
public var makeTextRenderer: (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer
|
public var makeTextRenderer: (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer
|
||||||
|
|
||||||
public static var live: RDEPUBReaderDependencies // 默认实现
|
public static var live: RDEPUBReaderDependencies // 默认实现
|
||||||
@ -659,9 +659,9 @@ struct RDEPUBReaderUIState {
|
|||||||
|
|
||||||
页图协调器(按需分页与全量分页的结果合并)。
|
页图协调器(按需分页与全量分页的结果合并)。
|
||||||
|
|
||||||
### 10.6 RDURLReaderController
|
### 10.6 RDEpubURLReaderController
|
||||||
|
|
||||||
**文件:** `RDURLReaderController.swift`
|
**文件:** `RDEpubURLReaderController.swift`
|
||||||
|
|
||||||
URL 阅读器控制器(用于打开单个 URL)。
|
URL 阅读器控制器(用于打开单个 URL)。
|
||||||
|
|
||||||
@ -715,12 +715,12 @@ RDEPUBReaderRuntime.applyParsedPublication()
|
|||||||
│
|
│
|
||||||
├── RDEPUBBookPageMap 生成
|
├── RDEPUBBookPageMap 生成
|
||||||
│
|
│
|
||||||
└── RDReaderView.transitionToPage() → 显示页面
|
└── RDEpubReaderView.transitionToPage() → 显示页面
|
||||||
|
|
||||||
用户翻页
|
用户翻页
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
RDReaderView.currentPage 变化
|
RDEpubReaderView.currentPage 变化
|
||||||
│
|
│
|
||||||
├── RDEPUBReaderLocationCoordinator.recordPageChangeIfNeeded()
|
├── RDEPUBReaderLocationCoordinator.recordPageChangeIfNeeded()
|
||||||
│ └── persistence.saveLocation()
|
│ └── persistence.saveLocation()
|
||||||
|
|||||||
@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
生成点:
|
生成点:
|
||||||
|
|
||||||
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494`(`buildPagesFromRanges`)
|
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494`(`buildPagesFromRanges`)
|
||||||
- `Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294`
|
- `Sources/RDEpubReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294`
|
||||||
- `Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift:78`
|
- `Sources/RDEpubReaderView/EPUBTextRendering/RDEpubPlainTextBookBuilder.swift:78`
|
||||||
|
|
||||||
现存全部消费点(已盘点确认,仅 3 处):
|
现存全部消费点(已盘点确认,仅 3 处):
|
||||||
|
|
||||||
@ -27,7 +27,7 @@
|
|||||||
**任务**:
|
**任务**:
|
||||||
|
|
||||||
- [x] 将 `RDEPUBTextPage.content` 改为按需构造:已改为基于 `chapterContent` + `contentRange` 的计算属性(带范围钳制),公开读取 API 不变
|
- [x] 将 `RDEPUBTextPage.content` 改为按需构造:已改为基于 `chapterContent` + `contentRange` 的计算属性(带范围钳制),公开读取 API 不变
|
||||||
- [x] 三个构造点(`RDEPUBChapterLoader` / `RDEPUBTextBookBuilder` / `RDPlainTextBookBuilder`)不再生成每页子串;消费点经计算属性透明按需构造
|
- [x] 三个构造点(`RDEPUBChapterLoader` / `RDEPUBTextBookBuilder` / `RDEpubPlainTextBookBuilder`)不再生成每页子串;消费点经计算属性透明按需构造
|
||||||
- [x] `Equatable` 确认:`content` 为派生值,由 `contentRange` + `chapterContent` 判等覆盖,语义不变
|
- [x] `Equatable` 确认:`content` 为派生值,由 `contentRange` + `chapterContent` 判等覆盖,语义不变
|
||||||
|
|
||||||
**API 兼容性注意**:
|
**API 兼容性注意**:
|
||||||
@ -47,7 +47,7 @@
|
|||||||
### P0-3 保持现有缓存限制,防止错误复用
|
### P0-3 保持现有缓存限制,防止错误复用
|
||||||
|
|
||||||
- [x] `shouldAvoidReaderPageCaching`(`RDEPUBTextContentView.swift:319`)保持现状,已补注释说明放开的前置条件是 P1-1 落地
|
- [x] `shouldAvoidReaderPageCaching`(`RDEPUBTextContentView.swift:319`)保持现状,已补注释说明放开的前置条件是 P1-1 落地
|
||||||
- [x] 本阶段未改 `RDReaderPreloadController` 行为
|
- [x] 本阶段未改 `RDEpubReaderPreloadController` 行为
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
前置:P1-1~P1-3 全部落地后,页面视图不再持有整章副本。
|
前置:P1-1~P1-3 全部落地后,页面视图不再持有整章副本。
|
||||||
|
|
||||||
- [ ] 调整 `shouldAvoidReaderPageCaching` 判定,允许轻量化后的文本页进入 `RDReaderPreloadController` 缓存
|
- [ ] 调整 `shouldAvoidReaderPageCaching` 判定,允许轻量化后的文本页进入 `RDEpubReaderPreloadController` 缓存
|
||||||
- [ ] 对比放开前后的翻页流畅度与峰值内存,数据不佳则回退此项
|
- [ ] 对比放开前后的翻页流畅度与峰值内存,数据不佳则回退此项
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -6,54 +6,54 @@
|
|||||||
|
|
||||||
## 1. 模块概述
|
## 1. 模块概述
|
||||||
|
|
||||||
`ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDReaderPageProvider` 协议提供页面内容视图,ReaderView 负责容器管理和翻页调度。
|
`ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDEpubReaderPageProvider` 协议提供页面内容视图,ReaderView 负责容器管理和翻页调度。
|
||||||
|
|
||||||
**文件清单(14 个 Swift 文件):**
|
**文件清单(14 个 Swift 文件):**
|
||||||
|
|
||||||
| 文件 | 核心类型 | 职责 |
|
| 文件 | 核心类型 | 职责 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| `RDReaderView.swift` | `RDReaderView` | 主容器视图,协调所有子组件 |
|
| `RDEpubReaderView.swift` | `RDEpubReaderView` | 主容器视图,协调所有子组件 |
|
||||||
| `RDReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 |
|
| `RDEpubReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 |
|
||||||
| `RDReaderFlowLayout.swift` | `RDReaderFlowLayout` | UICollectionView 自定义布局 |
|
| `RDEpubReaderFlowLayout.swift` | `RDEpubReaderFlowLayout` | UICollectionView 自定义布局 |
|
||||||
| `RDReaderGestureController.swift` | `RDReaderGestureController` | 手势控制器(预留) |
|
| `RDEpubReaderGestureController.swift` | `RDEpubReaderGestureController` | 手势控制器(预留) |
|
||||||
| `RDReaderContentCell.swift` | `RDReaderContentCell` | CollectionView 内容 Cell |
|
| `RDEpubReaderContentCell.swift` | `RDEpubReaderContentCell` | CollectionView 内容 Cell |
|
||||||
| `RDReaderPageChildViewController.swift` | `RDReaderPageChildViewController` | PageCurl 模式子 VC |
|
| `RDEpubReaderPageChildViewController.swift` | `RDEpubReaderPageChildViewController` | PageCurl 模式子 VC |
|
||||||
| `RDReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 |
|
| `RDEpubReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 |
|
||||||
| `RDReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 |
|
| `RDEpubReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 |
|
||||||
| `RDReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 |
|
| `RDEpubReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 |
|
||||||
| `RDReaderView+ToolView.swift` | Extension | 工具栏安装与动画 |
|
| `RDEpubReaderView+ToolView.swift` | Extension | 工具栏安装与动画 |
|
||||||
| `Paging/RDReaderPagingController.swift` | `RDReaderPagingController` | 翻页状态机 |
|
| `Paging/RDEpubReaderPagingController.swift` | `RDEpubReaderPagingController` | 翻页状态机 |
|
||||||
| `Paging/RDReaderPreloadController.swift` | `RDReaderPreloadController` | 页面预加载与缓存 |
|
| `Paging/RDEpubReaderPreloadController.swift` | `RDEpubReaderPreloadController` | 页面预加载与缓存 |
|
||||||
| `Paging/RDReaderSpreadResolver.swift` | `RDReaderSpreadResolver` | 双页展开计算 |
|
| `Paging/RDEpubReaderSpreadResolver.swift` | `RDEpubReaderSpreadResolver` | 双页展开计算 |
|
||||||
| `Paging/RDReaderTapRegionHandler.swift` | `RDReaderTapRegionHandler` | 点击区域判定 |
|
| `Paging/RDEpubReaderTapRegionHandler.swift` | `RDEpubReaderTapRegionHandler` | 点击区域判定 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 协议定义(RDReaderViewProtocols.swift)
|
## 2. 协议定义(RDEpubReaderViewProtocols.swift)
|
||||||
|
|
||||||
### 2.1 RDReaderDataSource(旧版数据源,已废弃)
|
### 2.1 RDEpubReaderDataSource(旧版数据源,已废弃)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@objc public protocol RDReaderDataSource: NSObjectProtocol {
|
@objc public protocol RDEpubReaderDataSource: NSObjectProtocol {
|
||||||
func pageCountOfReaderView(readerView: RDReaderView) -> Int
|
func pageCountOfReaderView(readerView: RDEpubReaderView) -> Int
|
||||||
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
func pageContentView(readerView: RDEpubReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||||||
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
|
func pageIdentifier(readerView: RDEpubReaderView, pageNum: Int) -> String?
|
||||||
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
|
@objc optional func topToolView(readerView: RDEpubReaderView) -> UIView?
|
||||||
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
|
@objc optional func bottomToolView(readerView: RDEpubReaderView) -> UIView?
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> 向后兼容保留,新代码应使用 `RDReaderPageProvider`。
|
> 向后兼容保留,新代码应使用 `RDEpubReaderPageProvider`。
|
||||||
|
|
||||||
### 2.2 RDReaderPageProvider(推荐数据源)
|
### 2.2 RDEpubReaderPageProvider(推荐数据源)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
|
@objc public protocol RDEpubReaderPageProvider: NSObjectProtocol {
|
||||||
func numberOfPages(in readerView: RDReaderView) -> Int
|
func numberOfPages(in readerView: RDEpubReaderView) -> Int
|
||||||
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
func readerView(_ readerView: RDEpubReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
||||||
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
|
@objc optional func pageIdentifier(in readerView: RDEpubReaderView, index: Int) -> String?
|
||||||
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
|
@objc optional func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||||
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
|
@objc optional func readerViewBottomChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -65,31 +65,31 @@
|
|||||||
| `readerViewTopChrome(_:)` | 返回顶部工具栏视图 |
|
| `readerViewTopChrome(_:)` | 返回顶部工具栏视图 |
|
||||||
| `readerViewBottomChrome(_:)` | 返回底部工具栏视图 |
|
| `readerViewBottomChrome(_:)` | 返回底部工具栏视图 |
|
||||||
|
|
||||||
### 2.3 RDReaderDelegate
|
### 2.3 RDEpubReaderDelegate
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@objc public protocol RDReaderDelegate: NSObjectProtocol {
|
@objc public protocol RDEpubReaderDelegate: NSObjectProtocol {
|
||||||
func pageNum(readerView: RDReaderView, pageNum: Int)
|
func pageNum(readerView: RDEpubReaderView, pageNum: Int)
|
||||||
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
|
@objc optional func readerViewOrientationWillChange(readerView: RDEpubReaderView, isLandscape: Bool)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.4 RDReaderPageNavigating
|
### 2.4 RDEpubReaderPageNavigating
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public protocol RDReaderPageNavigating: AnyObject {
|
public protocol RDEpubReaderPageNavigating: AnyObject {
|
||||||
var currentPage: Int { get }
|
var currentPage: Int { get }
|
||||||
func reloadPages()
|
func reloadPages()
|
||||||
func transition(to page: Int, animated: Bool)
|
func transition(to page: Int, animated: Bool)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`RDReaderView` 遵循此协议,提供统一的页面导航接口。
|
`RDEpubReaderView` 遵循此协议,提供统一的页面导航接口。
|
||||||
|
|
||||||
### 2.5 枚举类型
|
### 2.5 枚举类型
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
extension RDReaderView {
|
extension RDEpubReaderView {
|
||||||
public enum DisplayType {
|
public enum DisplayType {
|
||||||
case pageCurl // 仿真翻页(UIPageViewController)
|
case pageCurl // 仿真翻页(UIPageViewController)
|
||||||
case horizontalScroll // 水平滑动(UICollectionView)
|
case horizontalScroll // 水平滑动(UICollectionView)
|
||||||
@ -105,13 +105,13 @@ extension RDReaderView {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 核心类:RDReaderView
|
## 3. 核心类:RDEpubReaderView
|
||||||
|
|
||||||
**文件:** `RDReaderView.swift`
|
**文件:** `RDEpubReaderView.swift`
|
||||||
|
|
||||||
`RDReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
|
`RDEpubReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
|
||||||
- **PageCurl 模式**:使用 `UIPageViewController` 实现仿真翻页
|
- **PageCurl 模式**:使用 `UIPageViewController` 实现仿真翻页
|
||||||
- **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDReaderFlowLayout` 实现滑动翻页
|
- **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDEpubReaderFlowLayout` 实现滑动翻页
|
||||||
|
|
||||||
### 3.1 关键属性
|
### 3.1 关键属性
|
||||||
|
|
||||||
@ -124,16 +124,16 @@ extension RDReaderView {
|
|||||||
| `coverPageIndex` | `Int?` | 封面页索引(独占一屏) |
|
| `coverPageIndex` | `Int?` | 封面页索引(独占一屏) |
|
||||||
| `pagesPerScreen` | `Int` | 每屏页数(横屏双页时为 2) |
|
| `pagesPerScreen` | `Int` | 每屏页数(横屏双页时为 2) |
|
||||||
| `preloadRadius` | `Int` | 预加载半径(默认 1) |
|
| `preloadRadius` | `Int` | 预加载半径(默认 1) |
|
||||||
| `dataSource` | `RDReaderDataSource?` | 旧版数据源 |
|
| `dataSource` | `RDEpubReaderDataSource?` | 旧版数据源 |
|
||||||
| `pageProvider` | `RDReaderPageProvider?` | 推荐数据源 |
|
| `pageProvider` | `RDEpubReaderPageProvider?` | 推荐数据源 |
|
||||||
| `delegate` | `RDReaderDelegate?` | 事件代理 |
|
| `delegate` | `RDEpubReaderDelegate?` | 事件代理 |
|
||||||
| `toolViewAnimationDuration` | `TimeInterval` | 工具栏动画时长(0.3s) |
|
| `toolViewAnimationDuration` | `TimeInterval` | 工具栏动画时长(0.3s) |
|
||||||
|
|
||||||
### 3.2 关键方法
|
### 3.2 关键方法
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
/// 切换显示模式(pageCurl / horizontalScroll / verticalScroll)
|
/// 切换显示模式(pageCurl / horizontalScroll / verticalScroll)
|
||||||
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType)
|
public func switchReaderDisplayType(_ displayType: RDEpubReaderView.DisplayType)
|
||||||
|
|
||||||
/// 跳转到指定页
|
/// 跳转到指定页
|
||||||
public func transitionToPage(pageNum: Int, animated: Bool = false)
|
public func transitionToPage(pageNum: Int, animated: Bool = false)
|
||||||
@ -154,22 +154,22 @@ public func isFullScreenPage(_ pageNum: Int) -> Bool
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `pageViewController` | `UIPageViewController` | PageCurl 翻页引擎 |
|
| `pageViewController` | `UIPageViewController` | PageCurl 翻页引擎 |
|
||||||
| `collectionView` | `UICollectionView` | Scroll 翻页引擎 |
|
| `collectionView` | `UICollectionView` | Scroll 翻页引擎 |
|
||||||
| `layout` | `RDReaderFlowLayout` | CollectionView 自定义布局 |
|
| `layout` | `RDEpubReaderFlowLayout` | CollectionView 自定义布局 |
|
||||||
| `spreadResolver` | `RDReaderSpreadResolver` | 双页配对计算 |
|
| `spreadResolver` | `RDEpubReaderSpreadResolver` | 双页配对计算 |
|
||||||
| `tapRegionHandler` | `RDReaderTapRegionHandler` | 点击区域判定 |
|
| `tapRegionHandler` | `RDEpubReaderTapRegionHandler` | 点击区域判定 |
|
||||||
| `preloadController` | `RDReaderPreloadController` | 页面预加载与缓存 |
|
| `preloadController` | `RDEpubReaderPreloadController` | 页面预加载与缓存 |
|
||||||
| `pagingController` | `RDReaderPagingController` | 翻页状态管理 |
|
| `pagingController` | `RDEpubReaderPagingController` | 翻页状态管理 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 翻页状态机:RDReaderPagingController
|
## 4. 翻页状态机:RDEpubReaderPagingController
|
||||||
|
|
||||||
**文件:** `Paging/RDReaderPagingController.swift`
|
**文件:** `Paging/RDEpubReaderPagingController.swift`
|
||||||
|
|
||||||
管理 PageCurl 模式下的翻页请求队列,防止动画冲突。
|
管理 PageCurl 模式下的翻页请求队列,防止动画冲突。
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
struct RDReaderPagingController {
|
struct RDEpubReaderPagingController {
|
||||||
struct PageTransitionRequest: Equatable {
|
struct PageTransitionRequest: Equatable {
|
||||||
let pageNum: Int
|
let pageNum: Int
|
||||||
let animated: Bool
|
let animated: Bool
|
||||||
@ -180,7 +180,7 @@ struct RDReaderPagingController {
|
|||||||
var didBuildUI: Bool // UI 是否已构建
|
var didBuildUI: Bool // UI 是否已构建
|
||||||
|
|
||||||
/// 判断是否应排队请求(PageCurl 模式下动画中返回 true)
|
/// 判断是否应排队请求(PageCurl 模式下动画中返回 true)
|
||||||
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool
|
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDEpubReaderView.DisplayType) -> Bool
|
||||||
|
|
||||||
/// 完成翻页动画,返回待处理的请求
|
/// 完成翻页动画,返回待处理的请求
|
||||||
mutating func finishPageCurlTransition() -> PageTransitionRequest?
|
mutating func finishPageCurlTransition() -> PageTransitionRequest?
|
||||||
@ -192,9 +192,9 @@ struct RDReaderPagingController {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 页面预加载:RDReaderPreloadController
|
## 5. 页面预加载:RDEpubReaderPreloadController
|
||||||
|
|
||||||
**文件:** `Paging/RDReaderPreloadController.swift`
|
**文件:** `Paging/RDEpubReaderPreloadController.swift`
|
||||||
|
|
||||||
负责在当前页周围预渲染页面视图,减少翻页时的白屏时间。
|
负责在当前页周围预渲染页面视图,减少翻页时的白屏时间。
|
||||||
|
|
||||||
@ -224,27 +224,27 @@ func invalidate(environment: Environment)
|
|||||||
|
|
||||||
```swift
|
```swift
|
||||||
struct Environment {
|
struct Environment {
|
||||||
let displayType: RDReaderView.DisplayType
|
let displayType: RDEpubReaderView.DisplayType
|
||||||
let isLandscape: Bool
|
let isLandscape: Bool
|
||||||
let pagesPerScreen: Int
|
let pagesPerScreen: Int
|
||||||
let boundsSize: CGSize
|
let boundsSize: CGSize
|
||||||
let landscapeDualPageEnabled: Bool
|
let landscapeDualPageEnabled: Bool
|
||||||
let coverPageIndex: Int?
|
let coverPageIndex: Int?
|
||||||
let totalPages: Int
|
let totalPages: Int
|
||||||
let spreadResolver: RDReaderSpreadResolver
|
let spreadResolver: RDEpubReaderSpreadResolver
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 双页展开计算:RDReaderSpreadResolver
|
## 6. 双页展开计算:RDEpubReaderSpreadResolver
|
||||||
|
|
||||||
**文件:** `Paging/RDReaderSpreadResolver.swift`
|
**文件:** `Paging/RDEpubReaderSpreadResolver.swift`
|
||||||
|
|
||||||
纯函数式结构体,负责双页模式下的页面配对和导航计算。
|
纯函数式结构体,负责双页模式下的页面配对和导航计算。
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
struct RDReaderSpreadResolver {
|
struct RDEpubReaderSpreadResolver {
|
||||||
/// 判断是否为全屏页(封面页独占一屏)
|
/// 判断是否为全屏页(封面页独占一屏)
|
||||||
func isFullScreenPage(_ pageNum: Int, landscapeDualPageEnabled: Bool, isLandscape: Bool, coverPageIndex: Int?) -> Bool
|
func isFullScreenPage(_ pageNum: Int, landscapeDualPageEnabled: Bool, isLandscape: Bool, coverPageIndex: Int?) -> Bool
|
||||||
|
|
||||||
@ -265,15 +265,15 @@ struct RDReaderSpreadResolver {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 点击区域判定:RDReaderTapRegionHandler
|
## 7. 点击区域判定:RDEpubReaderTapRegionHandler
|
||||||
|
|
||||||
**文件:** `Paging/RDReaderTapRegionHandler.swift`
|
**文件:** `Paging/RDEpubReaderTapRegionHandler.swift`
|
||||||
|
|
||||||
将屏幕三等分,判定点击属于左/中/右区域。
|
将屏幕三等分,判定点击属于左/中/右区域。
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
struct RDReaderTapRegionHandler {
|
struct RDEpubReaderTapRegionHandler {
|
||||||
func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDReaderView.TapEvent
|
func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDEpubReaderView.TapEvent
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -284,9 +284,9 @@ struct RDReaderTapRegionHandler {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 流式布局:RDReaderFlowLayout
|
## 8. 流式布局:RDEpubReaderFlowLayout
|
||||||
|
|
||||||
**文件:** `RDReaderFlowLayout.swift`
|
**文件:** `RDEpubReaderFlowLayout.swift`
|
||||||
|
|
||||||
`UICollectionViewFlowLayout` 子类,支持水平滚动和垂直滚动两种模式。
|
`UICollectionViewFlowLayout` 子类,支持水平滚动和垂直滚动两种模式。
|
||||||
|
|
||||||
@ -294,7 +294,7 @@ struct RDReaderTapRegionHandler {
|
|||||||
|
|
||||||
| 属性 | 类型 | 说明 |
|
| 属性 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `displayType` | `RDReaderView.DisplayType` | 布局模式 |
|
| `displayType` | `RDEpubReaderView.DisplayType` | 布局模式 |
|
||||||
| `isLandscapeDualPage` | `Bool` | 是否横屏双页 |
|
| `isLandscapeDualPage` | `Bool` | 是否横屏双页 |
|
||||||
| `coverPageIndex` | `Int?` | 封面页索引 |
|
| `coverPageIndex` | `Int?` | 封面页索引 |
|
||||||
| `pagesPerScreen` | `Int` | 每屏页数 |
|
| `pagesPerScreen` | `Int` | 每屏页数 |
|
||||||
@ -302,12 +302,12 @@ struct RDReaderTapRegionHandler {
|
|||||||
### 8.2 协议
|
### 8.2 协议
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
|
public protocol RDEpubReaderFlowLayoutDataSoure: NSObjectProtocol {
|
||||||
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
|
func heigtOfVerticalScrollPage(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) -> CGFloat?
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
|
@objc public protocol RDEpubReaderFlowLayoutDelegate: NSObjectProtocol {
|
||||||
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
|
func pageNum(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -324,28 +324,28 @@ func currentContentOffset(count: Int) -> CGPoint
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 内容 Cell:RDReaderContentCell
|
## 9. 内容 Cell:RDEpubReaderContentCell
|
||||||
|
|
||||||
**文件:** `RDReaderContentCell.swift`
|
**文件:** `RDEpubReaderContentCell.swift`
|
||||||
|
|
||||||
`UICollectionViewCell` 子类,用于 Scroll 模式下承载页面内容视图。
|
`UICollectionViewCell` 子类,用于 Scroll 模式下承载页面内容视图。
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
class RDReaderContentCell: UICollectionViewCell {
|
class RDEpubReaderContentCell: UICollectionViewCell {
|
||||||
var containerView: UIView? // 设置时自动添加到 contentView,移除旧视图
|
var containerView: UIView? // 设置时自动添加到 contentView,移除旧视图
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. PageCurl 子控制器:RDReaderPageChildViewController
|
## 10. PageCurl 子控制器:RDEpubReaderPageChildViewController
|
||||||
|
|
||||||
**文件:** `RDReaderPageChildViewController.swift`
|
**文件:** `RDEpubReaderPageChildViewController.swift`
|
||||||
|
|
||||||
`UIViewController` 子类,作为 `UIPageViewController` 的页面 VC。
|
`UIViewController` 子类,作为 `UIPageViewController` 的页面 VC。
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
class RDReaderPageChildViewController: UIViewController {
|
class RDEpubReaderPageChildViewController: UIViewController {
|
||||||
var contentView: UIView? // 内容视图,设置时自动安装到容器
|
var contentView: UIView? // 内容视图,设置时自动安装到容器
|
||||||
var pageNum: Int // 对应页码
|
var pageNum: Int // 对应页码
|
||||||
|
|
||||||
@ -357,7 +357,7 @@ class RDReaderPageChildViewController: UIViewController {
|
|||||||
|
|
||||||
## 11. Extension 汇总
|
## 11. Extension 汇总
|
||||||
|
|
||||||
### 11.1 RDReaderView+PageCurl
|
### 11.1 RDEpubReaderView+PageCurl
|
||||||
|
|
||||||
实现 `UIPageViewControllerDataSource` 和 `UIPageViewControllerDelegate`:
|
实现 `UIPageViewControllerDataSource` 和 `UIPageViewControllerDelegate`:
|
||||||
|
|
||||||
@ -367,18 +367,18 @@ class RDReaderPageChildViewController: UIViewController {
|
|||||||
- `pageViewController(_:willTransitionTo:)` — 即将翻页时预加载
|
- `pageViewController(_:willTransitionTo:)` — 即将翻页时预加载
|
||||||
|
|
||||||
**特殊页码:**
|
**特殊页码:**
|
||||||
- `RDReaderView.blankPageNum`(`Int.max`)— 双页模式下的空白页
|
- `RDEpubReaderView.blankPageNum`(`Int.max`)— 双页模式下的空白页
|
||||||
- `RDReaderView.blankEndPageNum`(`Int.max - 1`)— 末尾空白页
|
- `RDEpubReaderView.blankEndPageNum`(`Int.max - 1`)— 末尾空白页
|
||||||
|
|
||||||
### 11.2 RDReaderView+CollectionView
|
### 11.2 RDEpubReaderView+CollectionView
|
||||||
|
|
||||||
实现 `UICollectionViewDataSource`、`RDReaderFlowLayoutDelegate`、`RDReaderFlowLayoutDataSoure`:
|
实现 `UICollectionViewDataSource`、`RDEpubReaderFlowLayoutDelegate`、`RDEpubReaderFlowLayoutDataSoure`:
|
||||||
|
|
||||||
- `collectionView(_:cellForItemAt:)` — 复用预加载视图或创建新 Cell
|
- `collectionView(_:cellForItemAt:)` — 复用预加载视图或创建新 Cell
|
||||||
- `collectionView(_:numberOfItemsInSection:)` — 返回总页数
|
- `collectionView(_:numberOfItemsInSection:)` — 返回总页数
|
||||||
- `pageNum(flowLayout:pageIndex:)` — 滚动时更新当前页码
|
- `pageNum(flowLayout:pageIndex:)` — 滚动时更新当前页码
|
||||||
|
|
||||||
### 11.3 RDReaderView+ContentAccess
|
### 11.3 RDEpubReaderView+ContentAccess
|
||||||
|
|
||||||
提供内容视图的注册、复用和查询:
|
提供内容视图的注册、复用和查询:
|
||||||
|
|
||||||
@ -396,7 +396,7 @@ public func pageContentView(pageNum: Int) -> UIView?
|
|||||||
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize
|
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize
|
||||||
```
|
```
|
||||||
|
|
||||||
### 11.4 RDReaderView+ToolView
|
### 11.4 RDEpubReaderView+ToolView
|
||||||
|
|
||||||
管理顶部/底部工具栏的安装、显示/隐藏动画:
|
管理顶部/底部工具栏的安装、显示/隐藏动画:
|
||||||
|
|
||||||
@ -420,11 +420,11 @@ func updateToolViewHeightConstraintsIfNeeded()
|
|||||||
| 模式 | 应用 |
|
| 模式 | 应用 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **策略模式** | `DisplayType` 切换 PageCurl / Scroll 两种翻页策略 |
|
| **策略模式** | `DisplayType` 切换 PageCurl / Scroll 两种翻页策略 |
|
||||||
| **适配器模式** | `RDReaderLegacyDataSourceAdapter` 将旧 `RDReaderDataSource` 适配为 `RDReaderPageProvider` |
|
| **适配器模式** | `RDEpubReaderLegacyDataSourceAdapter` 将旧 `RDEpubReaderDataSource` 适配为 `RDEpubReaderPageProvider` |
|
||||||
| **命令队列** | `RDReaderPagingController` 管理翻页请求队列 |
|
| **命令队列** | `RDEpubReaderPagingController` 管理翻页请求队列 |
|
||||||
| **缓存签名** | `RDReaderPreloadController.CacheSignature` 检测环境变化自动失效 |
|
| **缓存签名** | `RDEpubReaderPreloadController.CacheSignature` 检测环境变化自动失效 |
|
||||||
| **关注点分离** | Extension 将不同功能拆分到独立文件 |
|
| **关注点分离** | Extension 将不同功能拆分到独立文件 |
|
||||||
| **纯函数** | `RDReaderSpreadResolver` 和 `RDReaderTapRegionHandler` 无状态计算 |
|
| **纯函数** | `RDEpubReaderSpreadResolver` 和 `RDEpubReaderTapRegionHandler` 无状态计算 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -434,7 +434,7 @@ func updateToolViewHeightConstraintsIfNeeded()
|
|||||||
用户点击屏幕
|
用户点击屏幕
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
RDReaderTapRegionHandler.resolveTapEvent()
|
RDEpubReaderTapRegionHandler.resolveTapEvent()
|
||||||
│
|
│
|
||||||
├── .left → goPreviousPage() ──→ spreadResolver.nextPage(forward: false)
|
├── .left → goPreviousPage() ──→ spreadResolver.nextPage(forward: false)
|
||||||
├── .right → goNextPage() ──→ spreadResolver.nextPage(forward: true)
|
├── .right → goNextPage() ──→ spreadResolver.nextPage(forward: true)
|
||||||
|
|||||||
@ -116,7 +116,7 @@ xcodebuild test \
|
|||||||
建议同时记录以下观测项:
|
建议同时记录以下观测项:
|
||||||
|
|
||||||
- `prepareOnDemandChapter` 主线程 wall clock
|
- `prepareOnDemandChapter` 主线程 wall clock
|
||||||
- `RDReaderPreloadController` 预加载命中率
|
- `RDEpubReaderPreloadController` 预加载命中率
|
||||||
- `bookPageMap` partial extension / full replacement 次数
|
- `bookPageMap` partial extension / full replacement 次数
|
||||||
- 页面静态底图缓存命中率
|
- 页面静态底图缓存命中率
|
||||||
- CFI 延迟构建完成次数与耗时
|
- CFI 延迟构建完成次数与耗时
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
**输出**:`RDEPUBTypesettingOutput`(渲染请求、诊断信息、兼容性报告)
|
**输出**:`RDEPUBTypesettingOutput`(渲染请求、诊断信息、兼容性报告)
|
||||||
|
|
||||||
**关键文件**:`Sources/RDReaderView/EPUBTextRendering/Typesetter/`
|
**关键文件**:`Sources/RDEpubReaderView/EPUBTextRendering/Typesetter/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -31,12 +31,12 @@ graph TB
|
|||||||
IndexTable[RDEPUBTextIndexTable]
|
IndexTable[RDEPUBTextIndexTable]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph RDReaderView
|
subgraph RDEpubReaderView
|
||||||
ReaderView[RDReaderView]
|
ReaderView[RDEpubReaderView]
|
||||||
FlowLayout[RDReaderFlowLayout]
|
FlowLayout[RDEpubReaderFlowLayout]
|
||||||
Preload[RDReaderPreloadController]
|
Preload[RDEpubReaderPreloadController]
|
||||||
Spread[RDReaderSpreadResolver]
|
Spread[RDEpubReaderSpreadResolver]
|
||||||
TapRegion[RDReaderTapRegionHandler]
|
TapRegion[RDEpubReaderTapRegionHandler]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph EPUBUI
|
subgraph EPUBUI
|
||||||
@ -297,11 +297,11 @@ classDiagram
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
class RDReaderView {
|
class RDEpubReaderView {
|
||||||
+currentPage: Int
|
+currentPage: Int
|
||||||
+currentDisplayType: DisplayType
|
+currentDisplayType: DisplayType
|
||||||
+pageProvider: RDReaderPageProvider?
|
+pageProvider: RDEpubReaderPageProvider?
|
||||||
+delegate: RDReaderDelegate?
|
+delegate: RDEpubReaderDelegate?
|
||||||
+landscapeDualPageEnabled: Bool
|
+landscapeDualPageEnabled: Bool
|
||||||
+pageDirection: PageDirection
|
+pageDirection: PageDirection
|
||||||
+coverPageIndex: Int?
|
+coverPageIndex: Int?
|
||||||
@ -318,14 +318,14 @@ classDiagram
|
|||||||
verticalScroll
|
verticalScroll
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderPageProvider {
|
class RDEpubReaderPageProvider {
|
||||||
<<protocol>>
|
<<protocol>>
|
||||||
+numberOfPages(in:) Int
|
+numberOfPages(in:) Int
|
||||||
+readerView(_:viewForPageAt:reusableView:) UIView
|
+readerView(_:viewForPageAt:reusableView:) UIView
|
||||||
+pageIdentifier(in:index:) String?
|
+pageIdentifier(in:index:) String?
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderFlowLayout {
|
class RDEpubReaderFlowLayout {
|
||||||
+displayType: DisplayType
|
+displayType: DisplayType
|
||||||
+isLandscapeDualPage: Bool
|
+isLandscapeDualPage: Bool
|
||||||
+coverPageIndex: Int?
|
+coverPageIndex: Int?
|
||||||
@ -333,7 +333,7 @@ classDiagram
|
|||||||
+currentPage: Int
|
+currentPage: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderPreloadController {
|
class RDEpubReaderPreloadController {
|
||||||
+radius: Int
|
+radius: Int
|
||||||
+pageViewForDisplay(pageNum:environment:contentViewProvider:) UIView?
|
+pageViewForDisplay(pageNum:environment:contentViewProvider:) UIView?
|
||||||
+takePreloadedView(for:) UIView?
|
+takePreloadedView(for:) UIView?
|
||||||
@ -341,13 +341,13 @@ classDiagram
|
|||||||
+invalidate(environment:)
|
+invalidate(environment:)
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderSpreadResolver {
|
class RDEpubReaderSpreadResolver {
|
||||||
+isFullScreenPage(...) Bool
|
+isFullScreenPage(...) Bool
|
||||||
+dualPagePair(for:totalPages:coverPageIndex:) (Int, Int?)
|
+dualPagePair(for:totalPages:coverPageIndex:) (Int, Int?)
|
||||||
+nextPage(from:totalPages:pagesPerScreen:coverPageIndex:forward:) Int?
|
+nextPage(from:totalPages:pagesPerScreen:coverPageIndex:forward:) Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderTapRegionHandler {
|
class RDEpubReaderTapRegionHandler {
|
||||||
+resolveTapEvent(point:viewFrame:isToolViewVisible:) TapEvent
|
+resolveTapEvent(point:viewFrame:isToolViewVisible:) TapEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,7 +359,7 @@ classDiagram
|
|||||||
right
|
right
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderPagingController {
|
class RDEpubReaderPagingController {
|
||||||
+isTransitioning: Bool
|
+isTransitioning: Bool
|
||||||
+didBuildUI: Bool
|
+didBuildUI: Bool
|
||||||
+pendingTransitionRequest: PageTransitionRequest?
|
+pendingTransitionRequest: PageTransitionRequest?
|
||||||
@ -367,25 +367,25 @@ classDiagram
|
|||||||
+finishPageCurlTransition() PageTransitionRequest?
|
+finishPageCurlTransition() PageTransitionRequest?
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderContentCell {
|
class RDEpubReaderContentCell {
|
||||||
+containerView: UIView?
|
+containerView: UIView?
|
||||||
}
|
}
|
||||||
|
|
||||||
class RDReaderPageChildViewController {
|
class RDEpubReaderPageChildViewController {
|
||||||
+contentView: UIView?
|
+contentView: UIView?
|
||||||
+pageNum: Int
|
+pageNum: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
RDReaderView --> DisplayType : uses
|
RDEpubReaderView --> DisplayType : uses
|
||||||
RDReaderView --> RDReaderPageProvider : delegates
|
RDEpubReaderView --> RDEpubReaderPageProvider : delegates
|
||||||
RDReaderView --> RDReaderFlowLayout : owns
|
RDEpubReaderView --> RDEpubReaderFlowLayout : owns
|
||||||
RDReaderView --> RDReaderPreloadController : owns
|
RDEpubReaderView --> RDEpubReaderPreloadController : owns
|
||||||
RDReaderView --> RDReaderSpreadResolver : owns
|
RDEpubReaderView --> RDEpubReaderSpreadResolver : owns
|
||||||
RDReaderView --> RDReaderTapRegionHandler : owns
|
RDEpubReaderView --> RDEpubReaderTapRegionHandler : owns
|
||||||
RDReaderView --> RDReaderPagingController : owns
|
RDEpubReaderView --> RDEpubReaderPagingController : owns
|
||||||
RDReaderView --> RDReaderContentCell : creates
|
RDEpubReaderView --> RDEpubReaderContentCell : creates
|
||||||
RDReaderView --> RDReaderPageChildViewController : creates
|
RDEpubReaderView --> RDEpubReaderPageChildViewController : creates
|
||||||
RDReaderTapRegionHandler --> TapEvent : produces
|
RDEpubReaderTapRegionHandler --> TapEvent : produces
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -403,7 +403,7 @@ classDiagram
|
|||||||
+highlights: [RDEPUBHighlight]
|
+highlights: [RDEPUBHighlight]
|
||||||
+bookmarks: [RDEPUBBookmark]
|
+bookmarks: [RDEPUBBookmark]
|
||||||
+tableOfContents: [EPUBTableOfContentsItem]
|
+tableOfContents: [EPUBTableOfContentsItem]
|
||||||
+readerView: RDReaderView
|
+readerView: RDEpubReaderView
|
||||||
+readerContext: RDEPUBReaderContext
|
+readerContext: RDEPUBReaderContext
|
||||||
+runtime: RDEPUBReaderRuntime?
|
+runtime: RDEPUBReaderRuntime?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
## 项目信息
|
## 项目信息
|
||||||
|
|
||||||
- **模块总数:** 4 个(EPUBCore、EPUBTextRendering、RDReaderView、EPUBUI)
|
- **模块总数:** 4 个(EPUBCore、EPUBTextRendering、RDEpubReaderView、EPUBUI)
|
||||||
- **Swift 文件数:** 142 个(SDK Sources)
|
- **Swift 文件数:** 142 个(SDK Sources)
|
||||||
- **测试用例数:** 23 个测试类,约 99 个测试方法(UI 测试)
|
- **测试用例数:** 23 个测试类,约 99 个测试方法(UI 测试)
|
||||||
- **最低 iOS 版本:** 15.6
|
- **最低 iOS 版本:** 15.6
|
||||||
|
|||||||
2
Podfile
2
Podfile
@ -5,7 +5,7 @@ workspace 'ReadViewSDK.xcworkspace'
|
|||||||
project 'ReadViewSDK.xcodeproj'
|
project 'ReadViewSDK.xcodeproj'
|
||||||
|
|
||||||
target 'ReadViewSDK' do
|
target 'ReadViewSDK' do
|
||||||
pod 'RDReaderView', :path => '..'
|
pod 'RDEpubReaderView', :path => '..'
|
||||||
end
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
|
|||||||
19
RDEpubReaderView.podspec
Normal file
19
RDEpubReaderView.podspec
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = "RDEpubReaderView"
|
||||||
|
s.module_name = "RDEpubReaderView"
|
||||||
|
s.version = "0.0.1"
|
||||||
|
s.summary = "A reader view for EPUB and plain-text books"
|
||||||
|
s.platform = :ios, "15.0"
|
||||||
|
s.swift_versions = ["5.10"]
|
||||||
|
s.homepage = "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.git"
|
||||||
|
s.author = { "shenlei" => "shenlei@touchread.com" }
|
||||||
|
s.source = { :git => "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.git", :tag => "#{s.version}" }
|
||||||
|
s.license = "MIT"
|
||||||
|
s.source_files = "Sources/RDEpubReaderView/**/*.{swift}"
|
||||||
|
s.resource_bundles = {
|
||||||
|
"RDEpubReaderViewAssets" => ["Sources/RDEpubReaderView/EPUBCore/Resources/**/*"]
|
||||||
|
}
|
||||||
|
s.dependency "ZIPFoundation", "~> 0.9"
|
||||||
|
s.dependency "DTCoreText", "~> 1.6"
|
||||||
|
s.requires_arc = true
|
||||||
|
end
|
||||||
@ -1,21 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
Pod::Spec.new do |s|
|
|
||||||
s.name = "RDReaderView"
|
|
||||||
s.version = "0.0.1"
|
|
||||||
s.summary = "A reader view for novel"
|
|
||||||
s.platform = :ios, "15.6"
|
|
||||||
s.swift_versions = ["5.10"]
|
|
||||||
s.homepage = "https://github.com/namesubai/RDReaderView.git"
|
|
||||||
s.author = { "subai" => "804663401@qq.com" }
|
|
||||||
s.source = { :git => "https://github.com/namesubai/RDReaderView.git", :tag => "#{s.version}"}
|
|
||||||
s.license = "MIT"
|
|
||||||
s.source_files = 'Sources/RDReaderView/**/*.{swift}'
|
|
||||||
s.resource_bundles = {
|
|
||||||
'RDReaderViewAssets' => ['Sources/RDReaderView/EPUBCore/Resources/**/*']
|
|
||||||
}
|
|
||||||
s.dependency 'ZIPFoundation', '~> 0.9'
|
|
||||||
s.dependency 'DTCoreText', '~> 1.6'
|
|
||||||
s.requires_arc = true
|
|
||||||
|
|
||||||
end
|
|
||||||
@ -3,7 +3,7 @@ platform :ios, '15.6'
|
|||||||
target 'ReadViewDemo' do
|
target 'ReadViewDemo' do
|
||||||
# Comment the next line if you don't want to use dynamic frameworks
|
# Comment the next line if you don't want to use dynamic frameworks
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
pod 'RDReaderView', :path => '..'
|
pod 'RDEpubReaderView', :path => '..'
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -16,13 +16,13 @@ PODS:
|
|||||||
- DTFoundation/Core
|
- DTFoundation/Core
|
||||||
- DTFoundation/UIKit (1.7.19):
|
- DTFoundation/UIKit (1.7.19):
|
||||||
- DTFoundation/Core
|
- DTFoundation/Core
|
||||||
- RDReaderView (0.0.1):
|
- RDEpubReaderView (0.0.1):
|
||||||
- DTCoreText (~> 1.6)
|
- DTCoreText (~> 1.6)
|
||||||
- ZIPFoundation (~> 0.9)
|
- ZIPFoundation (~> 0.9)
|
||||||
- ZIPFoundation (0.9.20)
|
- ZIPFoundation (0.9.20)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- RDReaderView (from `..`)
|
- RDEpubReaderView (from `..`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@ -31,15 +31,15 @@ SPEC REPOS:
|
|||||||
- ZIPFoundation
|
- ZIPFoundation
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
RDReaderView:
|
RDEpubReaderView:
|
||||||
:path: ".."
|
:path: ".."
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||||
RDReaderView: 2e0eeeff4bcfe8bbc09642344d326440af280ed5
|
RDEpubReaderView: 603761e6d74a34230472bf53138942168e350d41
|
||||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||||
|
|
||||||
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
|
PODFILE CHECKSUM: 966c18a0a8d86d2f90e367ab41338d4e476d39bb
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
37
ReadViewDemo/Pods/Local Podspecs/RDEpubReaderView.podspec.json
generated
Normal file
37
ReadViewDemo/Pods/Local Podspecs/RDEpubReaderView.podspec.json
generated
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "RDEpubReaderView",
|
||||||
|
"module_name": "RDEpubReaderView",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"summary": "A reader view for EPUB and plain-text books",
|
||||||
|
"platforms": {
|
||||||
|
"ios": "15.0"
|
||||||
|
},
|
||||||
|
"swift_versions": [
|
||||||
|
"5.10"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/namesubai/RDEpubReaderView.git",
|
||||||
|
"authors": {
|
||||||
|
"subai": "804663401@qq.com"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"git": "https://github.com/namesubai/RDEpubReaderView.git",
|
||||||
|
"tag": "0.0.1"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"source_files": "Sources/RDEpubReaderView/**/*.{swift}",
|
||||||
|
"resource_bundles": {
|
||||||
|
"RDEpubReaderViewAssets": [
|
||||||
|
"Sources/RDEpubReaderView/EPUBCore/Resources/**/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ZIPFoundation": [
|
||||||
|
"~> 0.9"
|
||||||
|
],
|
||||||
|
"DTCoreText": [
|
||||||
|
"~> 1.6"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"requires_arc": true,
|
||||||
|
"swift_version": "5.10"
|
||||||
|
}
|
||||||
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "RDReaderView",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"summary": "A reader view for novel",
|
|
||||||
"platforms": {
|
|
||||||
"ios": "15.6"
|
|
||||||
},
|
|
||||||
"swift_versions": [
|
|
||||||
"5.10"
|
|
||||||
],
|
|
||||||
"homepage": "https://github.com/namesubai/RDReaderView.git",
|
|
||||||
"authors": {
|
|
||||||
"subai": "804663401@qq.com"
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"git": "https://github.com/namesubai/RDReaderView.git",
|
|
||||||
"tag": "0.0.1"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"source_files": "Sources/RDReaderView/**/*.{swift}",
|
|
||||||
"resource_bundles": {
|
|
||||||
"RDReaderViewAssets": [
|
|
||||||
"Sources/RDReaderView/EPUBCore/Resources/**/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ZIPFoundation": [
|
|
||||||
"~> 0.9"
|
|
||||||
],
|
|
||||||
"DTCoreText": [
|
|
||||||
"~> 1.6"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"requires_arc": true,
|
|
||||||
"swift_version": "5.10"
|
|
||||||
}
|
|
||||||
10
ReadViewDemo/Pods/Manifest.lock
generated
10
ReadViewDemo/Pods/Manifest.lock
generated
@ -16,13 +16,13 @@ PODS:
|
|||||||
- DTFoundation/Core
|
- DTFoundation/Core
|
||||||
- DTFoundation/UIKit (1.7.19):
|
- DTFoundation/UIKit (1.7.19):
|
||||||
- DTFoundation/Core
|
- DTFoundation/Core
|
||||||
- RDReaderView (0.0.1):
|
- RDEpubReaderView (0.0.1):
|
||||||
- DTCoreText (~> 1.6)
|
- DTCoreText (~> 1.6)
|
||||||
- ZIPFoundation (~> 0.9)
|
- ZIPFoundation (~> 0.9)
|
||||||
- ZIPFoundation (0.9.20)
|
- ZIPFoundation (0.9.20)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- RDReaderView (from `..`)
|
- RDEpubReaderView (from `..`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@ -31,15 +31,15 @@ SPEC REPOS:
|
|||||||
- ZIPFoundation
|
- ZIPFoundation
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
RDReaderView:
|
RDEpubReaderView:
|
||||||
:path: ".."
|
:path: ".."
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||||
RDReaderView: 2e0eeeff4bcfe8bbc09642344d326440af280ed5
|
RDEpubReaderView: 603761e6d74a34230472bf53138942168e350d41
|
||||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||||
|
|
||||||
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
|
PODFILE CHECKSUM: 966c18a0a8d86d2f90e367ab41338d4e476d39bb
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
2948
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
2948
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh
|
${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh
|
||||||
${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework
|
${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework
|
||||||
${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework
|
${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework
|
||||||
${BUILT_PRODUCTS_DIR}/RDReaderView/RDReaderView.framework
|
${BUILT_PRODUCTS_DIR}/RDEpubReaderView/RDEpubReaderView.framework
|
||||||
${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework
|
${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework
|
||||||
@ -1,4 +1,4 @@
|
|||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTFoundation.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTFoundation.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RDReaderView.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RDEpubReaderView.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZIPFoundation.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZIPFoundation.framework
|
||||||
@ -1,5 +1,5 @@
|
|||||||
${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh
|
${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh
|
||||||
${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework
|
${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework
|
||||||
${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework
|
${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework
|
||||||
${BUILT_PRODUCTS_DIR}/RDReaderView/RDReaderView.framework
|
${BUILT_PRODUCTS_DIR}/RDEpubReaderView/RDEpubReaderView.framework
|
||||||
${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework
|
${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework
|
||||||
@ -1,4 +1,4 @@
|
|||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTFoundation.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTFoundation.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RDReaderView.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RDEpubReaderView.framework
|
||||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZIPFoundation.framework
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZIPFoundation.framework
|
||||||
@ -178,13 +178,13 @@ code_sign_if_enabled() {
|
|||||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/RDReaderView/RDReaderView.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/RDEpubReaderView/RDEpubReaderView.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework"
|
||||||
fi
|
fi
|
||||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/RDReaderView/RDReaderView.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/RDEpubReaderView/RDEpubReaderView.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework"
|
||||||
fi
|
fi
|
||||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText/DTCoreText.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation/DTFoundation.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView/RDReaderView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation/ZIPFoundation.framework/Headers" "$(SDKROOT)/usr/include/libxml2"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText/DTCoreText.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation/DTFoundation.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView/RDEpubReaderView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation/ZIPFoundation.framework/Headers" "$(SDKROOT)/usr/include/libxml2"
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
OTHER_LDFLAGS = $(inherited) -l"xml2" -framework "CoreGraphics" -framework "CoreText" -framework "DTCoreText" -framework "DTFoundation" -framework "ImageIO" -framework "MediaPlayer" -framework "QuartzCore" -framework "RDReaderView" -framework "ZIPFoundation"
|
OTHER_LDFLAGS = $(inherited) -l"xml2" -framework "CoreGraphics" -framework "CoreText" -framework "DTCoreText" -framework "DTFoundation" -framework "ImageIO" -framework "MediaPlayer" -framework "QuartzCore" -framework "RDEpubReaderView" -framework "ZIPFoundation"
|
||||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "-F${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "-F${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "-F${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "-F${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
||||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText/DTCoreText.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation/DTFoundation.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView/RDReaderView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation/ZIPFoundation.framework/Headers" "$(SDKROOT)/usr/include/libxml2"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText/DTCoreText.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation/DTFoundation.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView/RDEpubReaderView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation/ZIPFoundation.framework/Headers" "$(SDKROOT)/usr/include/libxml2"
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
OTHER_LDFLAGS = $(inherited) -l"xml2" -framework "CoreGraphics" -framework "CoreText" -framework "DTCoreText" -framework "DTFoundation" -framework "ImageIO" -framework "MediaPlayer" -framework "QuartzCore" -framework "RDReaderView" -framework "ZIPFoundation"
|
OTHER_LDFLAGS = $(inherited) -l"xml2" -framework "CoreGraphics" -framework "CoreText" -framework "DTCoreText" -framework "DTFoundation" -framework "ImageIO" -framework "MediaPlayer" -framework "QuartzCore" -framework "RDEpubReaderView" -framework "ZIPFoundation"
|
||||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "-F${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "-F${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "-F${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "-F${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
||||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
|||||||
5
ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/RDEpubReaderView-dummy.m
generated
Normal file
5
ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/RDEpubReaderView-dummy.m
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
@interface PodsDummy_RDEpubReaderView : NSObject
|
||||||
|
@end
|
||||||
|
@implementation PodsDummy_RDEpubReaderView
|
||||||
|
@end
|
||||||
@ -11,6 +11,6 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
FOUNDATION_EXPORT double RDReaderViewVersionNumber;
|
FOUNDATION_EXPORT double RDEpubReaderViewVersionNumber;
|
||||||
FOUNDATION_EXPORT const unsigned char RDReaderViewVersionString[];
|
FOUNDATION_EXPORT const unsigned char RDEpubReaderViewVersionString[];
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
6
ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/RDEpubReaderView.modulemap
generated
Normal file
6
ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/RDEpubReaderView.modulemap
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
framework module RDEpubReaderView {
|
||||||
|
umbrella header "RDEpubReaderView-umbrella.h"
|
||||||
|
|
||||||
|
export *
|
||||||
|
module * { export * }
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/RDReaderView
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/RDEpubReaderView
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DTCoreText" "${PODS_CONFIGURATION_BUILD_DIR}/DTFoundation" "${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#import <Foundation/Foundation.h>
|
|
||||||
@interface PodsDummy_RDReaderView : NSObject
|
|
||||||
@end
|
|
||||||
@implementation PodsDummy_RDReaderView
|
|
||||||
@end
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
framework module RDReaderView {
|
|
||||||
umbrella header "RDReaderView-umbrella.h"
|
|
||||||
|
|
||||||
export *
|
|
||||||
module * { export * }
|
|
||||||
}
|
|
||||||
@ -19,6 +19,7 @@
|
|||||||
1A2B3C4D00000014AABBCC01 /* FanrenParseTimeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */; };
|
1A2B3C4D00000014AABBCC01 /* FanrenParseTimeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */; };
|
||||||
1A2B3C4D00000015AABBCC01 /* AsyncChapterLoadingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.swift */; };
|
1A2B3C4D00000015AABBCC01 /* AsyncChapterLoadingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.swift */; };
|
||||||
1A2B3C4D00000021AABBCC01 /* MemoryFootprintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */; };
|
1A2B3C4D00000021AABBCC01 /* MemoryFootprintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */; };
|
||||||
|
1A2B3C4D00000030AABBCC01 /* FixedLayoutRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000031AABBCC01 /* FixedLayoutRotationTests.swift */; };
|
||||||
23BB1155EA379786DAA10A89 /* DisplayTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.swift */; };
|
23BB1155EA379786DAA10A89 /* DisplayTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.swift */; };
|
||||||
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */; };
|
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */; };
|
||||||
3BC5C96D7A0ACF35F2192CC7 /* XCUIApplication+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B4C44287820D68ED6570F8 /* XCUIApplication+Launch.swift */; };
|
3BC5C96D7A0ACF35F2192CC7 /* XCUIApplication+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B4C44287820D68ED6570F8 /* XCUIApplication+Launch.swift */; };
|
||||||
@ -55,6 +56,7 @@
|
|||||||
1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FanrenParseTimeTest.swift; sourceTree = "<group>"; };
|
1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FanrenParseTimeTest.swift; sourceTree = "<group>"; };
|
||||||
1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AsyncChapterLoadingTests.swift; sourceTree = "<group>"; };
|
1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AsyncChapterLoadingTests.swift; sourceTree = "<group>"; };
|
||||||
1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MemoryFootprintTests.swift; sourceTree = "<group>"; };
|
1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MemoryFootprintTests.swift; sourceTree = "<group>"; };
|
||||||
|
1A2B3C4D00000031AABBCC01 /* FixedLayoutRotationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FixedLayoutRotationTests.swift; sourceTree = "<group>"; };
|
||||||
201C2B482287866487EFAE66 /* SettingsPanelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsPanelTests.swift; sourceTree = "<group>"; };
|
201C2B482287866487EFAE66 /* SettingsPanelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsPanelTests.swift; sourceTree = "<group>"; };
|
||||||
20BD15E5D8F04E7E9A239E14 /* ReaderAnnotationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderAnnotationTests.swift; sourceTree = "<group>"; };
|
20BD15E5D8F04E7E9A239E14 /* ReaderAnnotationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderAnnotationTests.swift; sourceTree = "<group>"; };
|
||||||
3A43AED288BFCA3ADBA97DD7 /* Pods-ReadViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReadViewDemo.release.xcconfig"; path = "Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo.release.xcconfig"; sourceTree = "<group>"; };
|
3A43AED288BFCA3ADBA97DD7 /* Pods-ReadViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReadViewDemo.release.xcconfig"; path = "Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
@ -196,6 +198,7 @@
|
|||||||
CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */,
|
CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */,
|
||||||
1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */,
|
1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */,
|
||||||
1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.swift */,
|
1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.swift */,
|
||||||
|
1A2B3C4D00000031AABBCC01 /* FixedLayoutRotationTests.swift */,
|
||||||
);
|
);
|
||||||
path = ReaderUITests;
|
path = ReaderUITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -326,10 +329,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n";
|
||||||
@ -362,6 +369,7 @@
|
|||||||
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */,
|
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */,
|
||||||
1A2B3C4D00000021AABBCC01 /* MemoryFootprintTests.swift in Sources */,
|
1A2B3C4D00000021AABBCC01 /* MemoryFootprintTests.swift in Sources */,
|
||||||
1A2B3C4D00000015AABBCC01 /* AsyncChapterLoadingTests.swift in Sources */,
|
1A2B3C4D00000015AABBCC01 /* AsyncChapterLoadingTests.swift in Sources */,
|
||||||
|
1A2B3C4D00000030AABBCC01 /* FixedLayoutRotationTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RDReaderView
|
import RDEpubReaderView
|
||||||
|
|
||||||
final class ViewController: UIViewController {
|
final class ViewController: UIViewController {
|
||||||
private struct DemoBook: Equatable {
|
private struct DemoBook: Equatable {
|
||||||
@ -10,10 +10,10 @@ final class ViewController: UIViewController {
|
|||||||
|
|
||||||
private struct LaunchAutomationPlan {
|
private struct LaunchAutomationPlan {
|
||||||
let bookTitleQuery: String
|
let bookTitleQuery: String
|
||||||
let displayType: RDReaderView.DisplayType?
|
let displayType: RDEpubReaderView.DisplayType?
|
||||||
let pageNumber: Int?
|
let pageNumber: Int?
|
||||||
let resetsReaderState: Bool
|
let resetsReaderState: Bool
|
||||||
let displaySequence: [RDReaderView.DisplayType]
|
let displaySequence: [RDEpubReaderView.DisplayType]
|
||||||
let stepDelay: TimeInterval
|
let stepDelay: TimeInterval
|
||||||
let windowSize: Int?
|
let windowSize: Int?
|
||||||
let concurrency: Int?
|
let concurrency: Int?
|
||||||
@ -23,7 +23,7 @@ final class ViewController: UIViewController {
|
|||||||
let enablesVerboseWebLogs: Bool
|
let enablesVerboseWebLogs: Bool
|
||||||
let searchKeyword: String?
|
let searchKeyword: String?
|
||||||
|
|
||||||
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDReaderView.DisplayType? {
|
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDEpubReaderView.DisplayType? {
|
||||||
switch rawValue.lowercased() {
|
switch rawValue.lowercased() {
|
||||||
case "pagecurl", "curl":
|
case "pagecurl", "curl":
|
||||||
return .pageCurl
|
return .pageCurl
|
||||||
@ -213,8 +213,8 @@ final class ViewController: UIViewController {
|
|||||||
_ book: DemoBook,
|
_ book: DemoBook,
|
||||||
configuration: RDEPUBReaderConfiguration = .default,
|
configuration: RDEPUBReaderConfiguration = .default,
|
||||||
automationPlan: LaunchAutomationPlan? = nil
|
automationPlan: LaunchAutomationPlan? = nil
|
||||||
) -> RDURLReaderController {
|
) -> RDEpubURLReaderController {
|
||||||
let controller = RDURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
|
let controller = RDEpubURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
|
||||||
controller.title = book.title
|
controller.title = book.title
|
||||||
|
|
||||||
if let navigationController {
|
if let navigationController {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
这是 ReadViewSDK 的 Phase 5 纯文本验证样本。
|
这是 ReadViewSDK 的 Phase 5 纯文本验证样本。
|
||||||
|
|
||||||
它用于覆盖 RDURLReaderController 与 RDPlainTextBookBuilder 的 TXT 路径,
|
它用于覆盖 RDEpubURLReaderController 与 RDEpubPlainTextBookBuilder 的 TXT 路径,
|
||||||
确认纯文本分页、打开主流程,以及后续 reader 壳层回归不会失效。
|
确认纯文本分页、打开主流程,以及后续 reader 壳层回归不会失效。
|
||||||
|
|
||||||
为了让 UI 自动化可以稳定验证翻页、长按选区、书签和目录行为,
|
为了让 UI 自动化可以稳定验证翻页、长按选区、书签和目录行为,
|
||||||
|
|||||||
@ -394,6 +394,10 @@ final class AsyncChapterLoadingTests: XCTestCase {
|
|||||||
for _ in 0..<10 {
|
for _ in 0..<10 {
|
||||||
paging.swipeLeft()
|
paging.swipeLeft()
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.4))
|
RunLoop.current.run(until: Date().addingTimeInterval(0.4))
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.activityIndicators[IDs.readerLoadingSpinner].waitForNonExistence(timeout: 10),
|
||||||
|
"横屏翻页后的页面不应一直停留在 loading"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// 固定布局(pre-paginated)书籍的旋转回归:
|
||||||
|
/// 每次旋转会使预加载缓存签名失效并批量丢弃 WKWebView 页面,
|
||||||
|
/// 历史上曾因 script message handler 保留环导致 WebView 泄漏累积,
|
||||||
|
/// 多次旋转后在 CA commit 阶段 abort(CA::Render::Encoder::grow)。
|
||||||
|
final class FixedLayoutRotationTests: XCTestCase {
|
||||||
|
private let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
XCUIDevice.shared.orientation = .portrait
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
XCUIDevice.shared.orientation = .portrait
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFixedLayoutBookSurvivesRepeatedRotation() {
|
||||||
|
app.launchAndOpenSampleBook(bookTitleQuery: "熊爷爷")
|
||||||
|
app.waitForReader()
|
||||||
|
|
||||||
|
for _ in 0..<3 {
|
||||||
|
XCUIDevice.shared.orientation = .landscapeLeft
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(2))
|
||||||
|
XCUIDevice.shared.orientation = .portrait
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(app.state, .runningForeground, "反复旋转后 app 已退出(疑似崩溃)")
|
||||||
|
app.waitForReader()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RDReaderView
|
import RDEpubReaderView
|
||||||
|
|
||||||
final class ViewController: UIViewController {
|
final class ViewController: UIViewController {
|
||||||
private struct DemoBook: Equatable {
|
private struct DemoBook: Equatable {
|
||||||
@ -10,12 +10,12 @@ final class ViewController: UIViewController {
|
|||||||
|
|
||||||
private struct LaunchAutomationPlan {
|
private struct LaunchAutomationPlan {
|
||||||
let bookTitleQuery: String
|
let bookTitleQuery: String
|
||||||
let displayType: RDReaderView.DisplayType?
|
let displayType: RDEpubReaderView.DisplayType?
|
||||||
let pageNumber: Int?
|
let pageNumber: Int?
|
||||||
let displaySequence: [RDReaderView.DisplayType]
|
let displaySequence: [RDEpubReaderView.DisplayType]
|
||||||
let stepDelay: TimeInterval
|
let stepDelay: TimeInterval
|
||||||
|
|
||||||
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDReaderView.DisplayType? {
|
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDEpubReaderView.DisplayType? {
|
||||||
switch rawValue.lowercased() {
|
switch rawValue.lowercased() {
|
||||||
case "pagecurl", "curl":
|
case "pagecurl", "curl":
|
||||||
return .pageCurl
|
return .pageCurl
|
||||||
@ -328,7 +328,7 @@ final class ViewController: UIViewController {
|
|||||||
) -> (report: BookValidationReport, diagnostics: [String]) {
|
) -> (report: BookValidationReport, diagnostics: [String]) {
|
||||||
let fileExtension = book.fileURL.pathExtension.lowercased()
|
let fileExtension = book.fileURL.pathExtension.lowercased()
|
||||||
if fileExtension == "txt" {
|
if fileExtension == "txt" {
|
||||||
let builder = RDPlainTextBookBuilder()
|
let builder = RDEpubPlainTextBookBuilder()
|
||||||
do {
|
do {
|
||||||
let textBook = try builder.build(textFileURL: book.fileURL, pageSize: pageSize, style: style)
|
let textBook = try builder.build(textFileURL: book.fileURL, pageSize: pageSize, style: style)
|
||||||
let passed = !textBook.pages.isEmpty && !textBook.chapters.isEmpty
|
let passed = !textBook.pages.isEmpty && !textBook.chapters.isEmpty
|
||||||
@ -646,8 +646,8 @@ final class ViewController: UIViewController {
|
|||||||
_ book: DemoBook,
|
_ book: DemoBook,
|
||||||
configuration: RDEPUBReaderConfiguration = .default,
|
configuration: RDEPUBReaderConfiguration = .default,
|
||||||
automationPlan: LaunchAutomationPlan? = nil
|
automationPlan: LaunchAutomationPlan? = nil
|
||||||
) -> RDURLReaderController {
|
) -> RDEpubURLReaderController {
|
||||||
let controller = RDURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
|
let controller = RDEpubURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
|
||||||
controller.title = book.title
|
controller.title = book.title
|
||||||
|
|
||||||
if let navigationController {
|
if let navigationController {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public let kRDEPUBHighlightAttributeName = NSAttributedString.Key("com.rdreader.highlight")
|
public let kRDEPUBHighlightAttributeName = NSAttributedString.Key("com.RDEpubReader.highlight")
|
||||||
|
|
||||||
public let kRDEPUBUnderlineAttributeName = NSAttributedString.Key("com.rdreader.underline")
|
public let kRDEPUBUnderlineAttributeName = NSAttributedString.Key("com.RDEpubReader.underline")
|
||||||
|
|
||||||
public struct RDEPUBSelection: Codable, Equatable {
|
public struct RDEPUBSelection: Codable, Equatable {
|
||||||
|
|
||||||
@ -60,9 +60,11 @@ enum RDEPUBAssetRepository {
|
|||||||
private static var resolvedBundle: Bundle? = {
|
private static var resolvedBundle: Bundle? = {
|
||||||
let hostBundles = [Bundle(for: RDEPUBAssetBundleToken.self), Bundle.main] + Bundle.allFrameworks + Bundle.allBundles
|
let hostBundles = [Bundle(for: RDEPUBAssetBundleToken.self), Bundle.main] + Bundle.allFrameworks + Bundle.allBundles
|
||||||
for hostBundle in hostBundles {
|
for hostBundle in hostBundles {
|
||||||
if let url = hostBundle.url(forResource: "RDReaderViewAssets", withExtension: "bundle"),
|
for bundleName in ["RDEpubReaderViewAssets", "RDReaderViewAssets"] {
|
||||||
let bundle = Bundle(url: url) {
|
if let url = hostBundle.url(forResource: bundleName, withExtension: "bundle"),
|
||||||
return bundle
|
let bundle = Bundle(url: url) {
|
||||||
|
return bundle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
379
Sources/RDEpubReaderView/EPUBCore/RDEPUBPaginator.swift
Normal file
379
Sources/RDEpubReaderView/EPUBCore/RDEPUBPaginator.swift
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
public final class RDEPUBPaginator: NSObject {
|
||||||
|
|
||||||
|
private var parser: RDEPUBParser?
|
||||||
|
|
||||||
|
private weak var hostingView: UIView?
|
||||||
|
|
||||||
|
private var presentation = RDEPUBPresentationStyle(
|
||||||
|
viewportSize: .zero,
|
||||||
|
contentInsets: .zero,
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeightMultiple: 1.5
|
||||||
|
)
|
||||||
|
|
||||||
|
private var completion: (([Int]) -> Void)?
|
||||||
|
|
||||||
|
private var singlePageCountCompletion: ((Int) -> Void)?
|
||||||
|
|
||||||
|
private var pageCounts: [Int] = []
|
||||||
|
|
||||||
|
private var measurementIndices: [Int] = []
|
||||||
|
|
||||||
|
private var currentMeasurementOffset = 0
|
||||||
|
|
||||||
|
private var pendingMeasurementValue = 1
|
||||||
|
|
||||||
|
private var measurementPass = 0
|
||||||
|
|
||||||
|
private var activeSessionID = UUID()
|
||||||
|
|
||||||
|
private let debugScope = "PaginatorWebView"
|
||||||
|
|
||||||
|
private lazy var webView: WKWebView = {
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
configuration.websiteDataStore = .nonPersistent()
|
||||||
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||||
|
webView.navigationDelegate = self
|
||||||
|
webView.isOpaque = false
|
||||||
|
webView.backgroundColor = .clear
|
||||||
|
webView.scrollView.isScrollEnabled = false
|
||||||
|
webView.scrollView.bounces = false
|
||||||
|
webView.scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
webView.scrollView.showsVerticalScrollIndicator = false
|
||||||
|
webView.isUserInteractionEnabled = false
|
||||||
|
if #available(iOS 16.4, *) {
|
||||||
|
webView.isInspectable = RDEPUBWebViewDebug.isInspectableEnabled
|
||||||
|
}
|
||||||
|
RDEPUBWebViewDebug.log("PaginatorWebView", message: "configured webView=\(RDEPUBWebViewDebug.webViewID(webView))")
|
||||||
|
return webView
|
||||||
|
}()
|
||||||
|
|
||||||
|
public override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
cleanupMeasurementState()
|
||||||
|
webView.navigationDelegate = nil
|
||||||
|
webView.stopLoading()
|
||||||
|
webView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func calculate(
|
||||||
|
parser: RDEPUBParser,
|
||||||
|
hostingView: UIView,
|
||||||
|
presentation: RDEPUBPresentationStyle,
|
||||||
|
completion: @escaping ([Int]) -> Void
|
||||||
|
) {
|
||||||
|
self.parser = parser
|
||||||
|
self.hostingView = hostingView
|
||||||
|
self.presentation = presentation
|
||||||
|
self.completion = completion
|
||||||
|
self.singlePageCountCompletion = nil
|
||||||
|
self.pageCounts = Array(repeating: 1, count: parser.spine.count)
|
||||||
|
self.measurementIndices = Array(parser.spine.indices)
|
||||||
|
self.currentMeasurementOffset = 0
|
||||||
|
self.activeSessionID = UUID()
|
||||||
|
RDEPUBWebViewDebug.log(debugScope, message: "calculate session=\(activeSessionID) spineCount=\(parser.spine.count)")
|
||||||
|
|
||||||
|
guard parser.metadata.layout != .fixed else {
|
||||||
|
completion(self.pageCounts)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
webView.navigationDelegate = self
|
||||||
|
if webView.superview == nil {
|
||||||
|
hostingView.addSubview(webView)
|
||||||
|
}
|
||||||
|
webView.frame = CGRect(origin: .zero, size: presentation.viewportSize)
|
||||||
|
measureNextSpineItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func calculate(
|
||||||
|
parser: RDEPUBParser,
|
||||||
|
hostingView: UIView,
|
||||||
|
viewportSize: CGSize,
|
||||||
|
padding: UIEdgeInsets,
|
||||||
|
fontSize: CGFloat,
|
||||||
|
lineHeightMultiple: CGFloat,
|
||||||
|
completion: @escaping ([Int]) -> Void
|
||||||
|
) {
|
||||||
|
calculate(
|
||||||
|
parser: parser,
|
||||||
|
hostingView: hostingView,
|
||||||
|
presentation: RDEPUBPresentationStyle(
|
||||||
|
viewportSize: viewportSize,
|
||||||
|
contentInsets: padding,
|
||||||
|
fontSize: fontSize,
|
||||||
|
lineHeightMultiple: lineHeightMultiple
|
||||||
|
),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func calculateSingleSpinePageCount(
|
||||||
|
parser: RDEPUBParser,
|
||||||
|
spineIndex: Int,
|
||||||
|
hostingView: UIView,
|
||||||
|
presentation: RDEPUBPresentationStyle,
|
||||||
|
completion: @escaping (Int) -> Void
|
||||||
|
) {
|
||||||
|
self.parser = parser
|
||||||
|
self.hostingView = hostingView
|
||||||
|
self.presentation = presentation
|
||||||
|
self.completion = nil
|
||||||
|
self.singlePageCountCompletion = completion
|
||||||
|
self.pageCounts = Array(repeating: 1, count: parser.spine.count)
|
||||||
|
self.measurementIndices = parser.spine.indices.contains(spineIndex) ? [spineIndex] : []
|
||||||
|
self.currentMeasurementOffset = 0
|
||||||
|
self.activeSessionID = UUID()
|
||||||
|
RDEPUBWebViewDebug.log(debugScope, message: "calculateSingle session=\(activeSessionID) spineIndex=\(spineIndex)")
|
||||||
|
|
||||||
|
guard parser.metadata.layout != .fixed, !measurementIndices.isEmpty else {
|
||||||
|
completion(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
webView.navigationDelegate = self
|
||||||
|
if webView.superview == nil {
|
||||||
|
hostingView.addSubview(webView)
|
||||||
|
}
|
||||||
|
webView.frame = CGRect(origin: .zero, size: presentation.viewportSize)
|
||||||
|
measureNextSpineItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func calculateSingleSpinePageCount(
|
||||||
|
parser: RDEPUBParser,
|
||||||
|
spineIndex: Int,
|
||||||
|
hostingView: UIView,
|
||||||
|
viewportSize: CGSize,
|
||||||
|
padding: UIEdgeInsets,
|
||||||
|
fontSize: CGFloat,
|
||||||
|
lineHeightMultiple: CGFloat,
|
||||||
|
completion: @escaping (Int) -> Void
|
||||||
|
) {
|
||||||
|
calculateSingleSpinePageCount(
|
||||||
|
parser: parser,
|
||||||
|
spineIndex: spineIndex,
|
||||||
|
hostingView: hostingView,
|
||||||
|
presentation: RDEPUBPresentationStyle(
|
||||||
|
viewportSize: viewportSize,
|
||||||
|
contentInsets: padding,
|
||||||
|
fontSize: fontSize,
|
||||||
|
lineHeightMultiple: lineHeightMultiple
|
||||||
|
),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func injectPaginationCSS(
|
||||||
|
into html: String,
|
||||||
|
viewportSize: CGSize,
|
||||||
|
padding: UIEdgeInsets,
|
||||||
|
fontSize: CGFloat,
|
||||||
|
lineHeightMultiple: CGFloat,
|
||||||
|
themeBackgroundColor: String? = nil,
|
||||||
|
themeTextColor: String? = nil
|
||||||
|
) -> String {
|
||||||
|
RDEPUBStyleSheetBuilder.injectPaginationCSS(
|
||||||
|
into: html,
|
||||||
|
presentation: RDEPUBPresentationStyle(
|
||||||
|
viewportSize: viewportSize,
|
||||||
|
contentInsets: padding,
|
||||||
|
fontSize: fontSize,
|
||||||
|
lineHeightMultiple: lineHeightMultiple,
|
||||||
|
themeBackgroundColor: themeBackgroundColor,
|
||||||
|
themeTextColor: themeTextColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func measureNextSpineItem() {
|
||||||
|
guard let parser else {
|
||||||
|
finishIfNeeded()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard currentMeasurementOffset < measurementIndices.count else {
|
||||||
|
finishIfNeeded()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentSpineIndex = measurementIndices[currentMeasurementOffset]
|
||||||
|
|
||||||
|
let item = parser.spine[currentSpineIndex]
|
||||||
|
guard isRenderablePage(item: item),
|
||||||
|
let opfDirectoryURL = parser.opfDirectoryURL else {
|
||||||
|
pageCounts[currentSpineIndex] = 1
|
||||||
|
currentMeasurementOffset += 1
|
||||||
|
measureNextSpineItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileURL = opfDirectoryURL.appendingPathComponent(item.href)
|
||||||
|
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||||
|
pageCounts[currentSpineIndex] = 1
|
||||||
|
currentMeasurementOffset += 1
|
||||||
|
measureNextSpineItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingMeasurementValue = 1
|
||||||
|
measurementPass = 0
|
||||||
|
let readAccessURL = parser.extractionRootURL ?? opfDirectoryURL
|
||||||
|
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "measure-load", url: fileURL)
|
||||||
|
webView.loadFileURL(fileURL, allowingReadAccessTo: readAccessURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentSpineIndexForMeasurement() -> Int? {
|
||||||
|
measurementIndices[safe: currentMeasurementOffset]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishIfNeeded() {
|
||||||
|
if let singlePageCountCompletion {
|
||||||
|
let measuredSpineIndex = measurementIndices.first ?? 0
|
||||||
|
singlePageCountCompletion(max(1, pageCounts[safe: measuredSpineIndex] ?? 1))
|
||||||
|
self.singlePageCountCompletion = nil
|
||||||
|
cleanupMeasurementState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let completion else { return }
|
||||||
|
completion(pageCounts)
|
||||||
|
self.completion = nil
|
||||||
|
cleanupMeasurementState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cleanupMeasurementState() {
|
||||||
|
parser = nil
|
||||||
|
hostingView = nil
|
||||||
|
completion = nil
|
||||||
|
singlePageCountCompletion = nil
|
||||||
|
measurementIndices = []
|
||||||
|
currentMeasurementOffset = 0
|
||||||
|
pendingMeasurementValue = 1
|
||||||
|
measurementPass = 0
|
||||||
|
activeSessionID = UUID()
|
||||||
|
webView.stopLoading()
|
||||||
|
webView.navigationDelegate = nil
|
||||||
|
webView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isRenderablePage(item: RDEPUBSpineItem) -> Bool {
|
||||||
|
let mediaType = item.mediaType.lowercased()
|
||||||
|
return mediaType.contains("html") || mediaType.contains("xhtml") || mediaType.contains("xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var measurementPassValues: [Int] = []
|
||||||
|
|
||||||
|
private func scheduleMeasurementPass() {
|
||||||
|
let sessionID = activeSessionID
|
||||||
|
let delays: [TimeInterval] = [0.0, 0.08, 0.18]
|
||||||
|
guard measurementPass < delays.count else {
|
||||||
|
guard sessionID == activeSessionID,
|
||||||
|
let currentSpineIndex = currentSpineIndexForMeasurement() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let finalValue = max(1, pendingMeasurementValue)
|
||||||
|
|
||||||
|
if measurementPassValues.count >= 2 {
|
||||||
|
let minVal = measurementPassValues.min() ?? 1
|
||||||
|
let maxVal = measurementPassValues.max() ?? 1
|
||||||
|
if minVal > 0 && Double(maxVal - minVal) / Double(minVal) > 0.2 {
|
||||||
|
RDEPUBWebViewDebug.log(debugScope, message: "measurement instability: passes=\(measurementPassValues) spine=\(currentSpineIndex)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageCounts[currentSpineIndex] = finalValue
|
||||||
|
currentMeasurementOffset += 1
|
||||||
|
measurementPassValues = []
|
||||||
|
measureNextSpineItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay = delays[measurementPass]
|
||||||
|
measurementPass += 1
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
|
guard let self, self.activeSessionID == sessionID else { return }
|
||||||
|
self.measureCurrentDocument(sessionID: sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func measureCurrentDocument(sessionID: UUID) {
|
||||||
|
let script = RDEPUBStyleSheetBuilder.measurementScript(for: presentation)
|
||||||
|
RDEPUBWebViewDebug.logJavaScript(debugScope, webView: webView, action: "measure", details: "session=\(sessionID) pass=\(measurementPass)")
|
||||||
|
|
||||||
|
webView.evaluateJavaScript(script) { [weak self] value, _ in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.activeSessionID == sessionID,
|
||||||
|
self.currentSpineIndexForMeasurement() != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let number = value as? NSNumber {
|
||||||
|
self.pendingMeasurementValue = max(self.pendingMeasurementValue, number.intValue)
|
||||||
|
self.measurementPassValues.append(number.intValue)
|
||||||
|
} else if let intValue = value as? Int {
|
||||||
|
self.pendingMeasurementValue = max(self.pendingMeasurementValue, intValue)
|
||||||
|
self.measurementPassValues.append(intValue)
|
||||||
|
}
|
||||||
|
RDEPUBWebViewDebug.log(self.debugScope, message: "measurement value=\(self.pendingMeasurementValue) session=\(sessionID)")
|
||||||
|
self.scheduleMeasurementPass()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RDEPUBPaginator: WKNavigationDelegate {
|
||||||
|
|
||||||
|
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||||
|
guard let url = navigationAction.request.url else {
|
||||||
|
decisionHandler(.cancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let scheme = url.scheme?.lowercased() ?? ""
|
||||||
|
if scheme == "file" || scheme == RDEPUBResourceURLSchemeHandler.scheme {
|
||||||
|
decisionHandler(.allow)
|
||||||
|
} else {
|
||||||
|
RDEPUBWebViewDebug.log(debugScope, message: "blocked navigation to non-local scheme: \(scheme)")
|
||||||
|
decisionHandler(.cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||||
|
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didStart", url: webView.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||||
|
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFinish", url: webView.url)
|
||||||
|
guard currentSpineIndexForMeasurement() != nil else { return }
|
||||||
|
scheduleMeasurementPass()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||||
|
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFail", url: webView.url, error: error)
|
||||||
|
let currentSpineIndex = measurementIndices[safe: currentMeasurementOffset] ?? 0
|
||||||
|
pageCounts[currentSpineIndex] = 1
|
||||||
|
currentMeasurementOffset += 1
|
||||||
|
measureNextSpineItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||||
|
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFailProvisional", url: webView.url, error: error)
|
||||||
|
let currentSpineIndex = measurementIndices[safe: currentMeasurementOffset] ?? 0
|
||||||
|
pageCounts[currentSpineIndex] = 1
|
||||||
|
currentMeasurementOffset += 1
|
||||||
|
measureNextSpineItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||||
|
RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "processTerminated", url: webView.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Array {
|
||||||
|
subscript(safe index: Int) -> Element? {
|
||||||
|
indices.contains(index) ? self[index] : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -111,11 +111,18 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
|
|||||||
updatedItem.title = manifestTitleByID[item.id]
|
updatedItem.title = manifestTitleByID[item.id]
|
||||||
return updatedItem
|
return updatedItem
|
||||||
}
|
}
|
||||||
|
// 部分历史 EPUB 的 OPF manifest 含重复 id。`uniqueKeysWithValues` 会在
|
||||||
|
// 此处触发运行时断言,导致整本书无法打开;spine 按 OPF 顺序解析,故保留
|
||||||
|
// 第一个声明即可维持其原始引用语义。
|
||||||
|
let manifestByID = manifest.reduce(into: [String: RDEPUBManifestItem]()) { result, item in
|
||||||
|
guard !item.id.isEmpty, result[item.id] == nil else { return }
|
||||||
|
result[item.id] = item
|
||||||
|
}
|
||||||
|
|
||||||
return OPFPackageDocument(
|
return OPFPackageDocument(
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
manifest: manifest,
|
manifest: manifest,
|
||||||
manifestByID: Dictionary(uniqueKeysWithValues: manifest.map { ($0.id, $0) }),
|
manifestByID: manifestByID,
|
||||||
spineReferences: spineReferences,
|
spineReferences: spineReferences,
|
||||||
ncxItem: manifest.first(where: { $0.id == ncxID || $0.isNCX }),
|
ncxItem: manifest.first(where: { $0.id == ncxID || $0.isNCX }),
|
||||||
navigationItem: manifest.first(where: { $0.isNavigationDocument })
|
navigationItem: manifest.first(where: { $0.isNavigationDocument })
|
||||||
@ -174,7 +174,8 @@ public final class RDEPUBParser {
|
|||||||
let packageDocument = delegate.packageDocument()
|
let packageDocument = delegate.packageDocument()
|
||||||
self.opfURL = opfURL
|
self.opfURL = opfURL
|
||||||
self.metadata = packageDocument.metadata
|
self.metadata = packageDocument.metadata
|
||||||
self.manifest = Dictionary(uniqueKeysWithValues: packageDocument.manifest.map { ($0.id, $0) })
|
// 与 packageDocument 使用同一份已去重索引,避免重复 manifest id 再次触发断言。
|
||||||
|
self.manifest = packageDocument.manifestByID
|
||||||
self.spine = try buildSpine(from: packageDocument, opfURL: opfURL)
|
self.spine = try buildSpine(from: packageDocument, opfURL: opfURL)
|
||||||
self.tableOfContents = parseTOC(from: packageDocument, opfURL: opfURL)
|
self.tableOfContents = parseTOC(from: packageDocument, opfURL: opfURL)
|
||||||
}
|
}
|
||||||
@ -206,4 +207,4 @@ private extension URL {
|
|||||||
}
|
}
|
||||||
return totalSize
|
return totalSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
extension RDEPUBWebView {
|
||||||
|
|
||||||
|
func configureWebViewIfNeeded(publication: RDEPUBPublication) {
|
||||||
|
let parser = publication.parser
|
||||||
|
let publicationKey = parser.opfURL?.path ?? parser.extractionRootURL?.path ?? UUID().uuidString
|
||||||
|
if publicationKey == configuredPublicationKey, webView != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teardownWebView()
|
||||||
|
|
||||||
|
let userContentController = WKUserContentController()
|
||||||
|
let messageHandlerProxy = RDEPUBWeakScriptMessageHandler(target: self)
|
||||||
|
RDEPUBJavaScriptBridge.messageNames.forEach {
|
||||||
|
userContentController.add(messageHandlerProxy, name: $0)
|
||||||
|
}
|
||||||
|
userContentController.addUserScript(
|
||||||
|
WKUserScript(
|
||||||
|
source: RDEPUBJavaScriptBridge.userScript,
|
||||||
|
injectionTime: .atDocumentEnd,
|
||||||
|
forMainFrameOnly: true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
configuration.websiteDataStore = .nonPersistent()
|
||||||
|
configuration.userContentController = userContentController
|
||||||
|
let schemeHandler = RDEPUBResourceURLSchemeHandler(parser: parser)
|
||||||
|
configuration.setURLSchemeHandler(schemeHandler, forURLScheme: RDEPUBResourceURLSchemeHandler.scheme)
|
||||||
|
|
||||||
|
let webView = RDEPUBAnnotationWebView(frame: bounds, configuration: configuration)
|
||||||
|
webView.onSelectionAction = { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
self.handleSelectionMenuAction(action)
|
||||||
|
}
|
||||||
|
// M-15: Use UIEditMenuInteraction on iOS 16+ to replace deprecated UIMenuController
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
let interaction = UIEditMenuInteraction(delegate: webView)
|
||||||
|
webView.addInteraction(interaction)
|
||||||
|
webView._editMenuInteraction = interaction
|
||||||
|
} else {
|
||||||
|
UIMenuController.shared.menuItems = [
|
||||||
|
UIMenuItem(title: "拷贝", action: #selector(RDEPUBAnnotationWebView.rd_copy(_:))),
|
||||||
|
UIMenuItem(title: "高亮", action: #selector(RDEPUBAnnotationWebView.rd_highlight(_:))),
|
||||||
|
UIMenuItem(title: "批注", action: #selector(RDEPUBAnnotationWebView.rd_annotate(_:)))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
webView.navigationDelegate = self
|
||||||
|
// 安全区自适应会把 Web 布局视口缩小(横屏左右各 59pt、底部 21pt),
|
||||||
|
// 固定布局页面随之按缩小后的视口适配,产生大边距/中缝。
|
||||||
|
// 阅读区域的安全区避让统一由 fixedContentInset / 排版 CSS 负责。
|
||||||
|
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
webView.scrollView.isScrollEnabled = false
|
||||||
|
webView.scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
webView.scrollView.showsVerticalScrollIndicator = false
|
||||||
|
webView.scrollView.bounces = false
|
||||||
|
webView.scrollView.alwaysBounceHorizontal = false
|
||||||
|
webView.scrollView.alwaysBounceVertical = false
|
||||||
|
webView.scrollView.clipsToBounds = true
|
||||||
|
webView.scrollView.panGestureRecognizer.isEnabled = false
|
||||||
|
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
|
||||||
|
webView.isOpaque = false
|
||||||
|
webView.backgroundColor = .clear
|
||||||
|
webView.clipsToBounds = true
|
||||||
|
if #available(iOS 16.4, *) {
|
||||||
|
webView.isInspectable = RDEPUBWebViewDebug.isInspectableEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubview(webView)
|
||||||
|
webView.frame = bounds
|
||||||
|
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
|
||||||
|
self.schemeHandler = schemeHandler
|
||||||
|
self.webView = webView
|
||||||
|
self.configuredPublicationKey = publicationKey
|
||||||
|
RDEPUBWebViewDebug.log(debugScope, message: "configured webView=\(RDEPUBWebViewDebug.webViewID(webView)) publicationKey=\(publicationKey)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction) {
|
||||||
|
delegate?.epubWebView(self, didRequestSelectionAction: action)
|
||||||
|
clearWebSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearWebSelection() {
|
||||||
|
webView?.evaluateJavaScript("window.getSelection && window.getSelection().removeAllRanges();")
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardownWebView() {
|
||||||
|
cancelFixedLayoutReadyFallback()
|
||||||
|
if let webView {
|
||||||
|
RDEPUBWebViewDebug.log(debugScope, message: "teardown webView=\(RDEPUBWebViewDebug.webViewID(webView))")
|
||||||
|
}
|
||||||
|
// M-15: Clean up UIEditMenuInteraction on iOS 16+
|
||||||
|
if #available(iOS 16.0, *), let interaction = (webView as? RDEPUBAnnotationWebView)?._editMenuInteraction as? UIEditMenuInteraction {
|
||||||
|
webView?.removeInteraction(interaction)
|
||||||
|
}
|
||||||
|
webView?.navigationDelegate = nil
|
||||||
|
if let userContentController = webView?.configuration.userContentController {
|
||||||
|
RDEPUBJavaScriptBridge.messageNames.forEach {
|
||||||
|
userContentController.removeScriptMessageHandler(forName: $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(webView as? RDEPUBAnnotationWebView)?.onSelectionAction = nil
|
||||||
|
webView?.removeFromSuperview()
|
||||||
|
webView = nil
|
||||||
|
schemeHandler = nil
|
||||||
|
configuredPublicationKey = nil
|
||||||
|
currentLoadSignature = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M-08: WKUserContentController 强持有注册的 script message handler。
|
||||||
|
/// 直接注册 RDEPUBWebView 自身会形成保留环
|
||||||
|
/// (webView → userContentController → RDEPUBWebView → webView),
|
||||||
|
/// 被丢弃的页面 WebView 将永远无法释放。本代理弱持有真实 handler,
|
||||||
|
/// 保留环从一开始就不成立,RDEPUBWebView 可随最后一个持有者正常 deinit。
|
||||||
|
private final class RDEPUBWeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||||
|
|
||||||
|
private weak var target: WKScriptMessageHandler?
|
||||||
|
|
||||||
|
init(target: WKScriptMessageHandler) {
|
||||||
|
self.target = target
|
||||||
|
}
|
||||||
|
|
||||||
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
target?.userContentController(userContentController, didReceive: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
243
Sources/RDEpubReaderView/EPUBCore/RDEPUBWebView.swift
Normal file
243
Sources/RDEpubReaderView/EPUBCore/RDEPUBWebView.swift
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
public protocol RDEPUBWebViewDelegate: AnyObject {
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int)
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int)
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction)
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int)
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didActivateExternalLink url: URL)
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String)
|
||||||
|
|
||||||
|
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView)
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didTapImageWithSource src: String, sourceRect: CGRect?)
|
||||||
|
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension RDEPUBWebViewDelegate {
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) {}
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didTapImageWithSource src: String, sourceRect: CGRect?) {}
|
||||||
|
func epubWebView(_ webView: RDEPUBWebView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class RDEPUBAnnotationWebView: WKWebView {
|
||||||
|
|
||||||
|
var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)?
|
||||||
|
|
||||||
|
// M-15: Backing store for UIEditMenuInteraction (iOS 16+).
|
||||||
|
// Stored as Any? to avoid @available on stored property restriction.
|
||||||
|
var _editMenuInteraction: Any?
|
||||||
|
|
||||||
|
override var canBecomeFirstResponder: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||||
|
switch action {
|
||||||
|
case #selector(rd_copy(_:)),
|
||||||
|
#selector(rd_highlight(_:)),
|
||||||
|
#selector(rd_annotate(_:)):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func rd_copy(_ sender: Any?) {
|
||||||
|
onSelectionAction?(.copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func rd_highlight(_ sender: Any?) {
|
||||||
|
onSelectionAction?(.highlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func rd_annotate(_ sender: Any?) {
|
||||||
|
onSelectionAction?(.annotate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIEditMenuInteractionDelegate (iOS 16+)
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
extension RDEPUBAnnotationWebView: UIEditMenuInteractionDelegate {
|
||||||
|
func editMenuInteraction(
|
||||||
|
_ interaction: UIEditMenuInteraction,
|
||||||
|
menuFor configuration: UIEditMenuConfiguration
|
||||||
|
) -> UIMenu {
|
||||||
|
UIMenu(children: [
|
||||||
|
UICommand(title: "拷贝", action: #selector(rd_copy(_:))),
|
||||||
|
UICommand(title: "高亮", action: #selector(rd_highlight(_:))),
|
||||||
|
UICommand(title: "批注", action: #selector(rd_annotate(_:)))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func editMenuInteraction(
|
||||||
|
_ interaction: UIEditMenuInteraction,
|
||||||
|
targetRectFor configuration: UIEditMenuConfiguration
|
||||||
|
) -> CGRect {
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class RDEPUBWebView: UIView {
|
||||||
|
|
||||||
|
public weak var delegate: RDEPUBWebViewDelegate?
|
||||||
|
|
||||||
|
public var onRendered: (() -> Void)?
|
||||||
|
|
||||||
|
var onDecorationsResolved: (([RDEPUBTextOverlayDecoration]) -> Void)?
|
||||||
|
|
||||||
|
var publication: RDEPUBPublication?
|
||||||
|
|
||||||
|
var currentRenderRequest: RDEPUBRenderRequest?
|
||||||
|
|
||||||
|
var webView: WKWebView?
|
||||||
|
|
||||||
|
var schemeHandler: RDEPUBResourceURLSchemeHandler?
|
||||||
|
|
||||||
|
var configuredPublicationKey: String?
|
||||||
|
|
||||||
|
var currentLoadSignature: String?
|
||||||
|
|
||||||
|
var currentSpineIndex = 0
|
||||||
|
|
||||||
|
var currentHref = ""
|
||||||
|
|
||||||
|
var currentPageIndex = 0
|
||||||
|
|
||||||
|
var currentTotalPagesInChapter = 1
|
||||||
|
|
||||||
|
var viewportSize: CGSize = .zero
|
||||||
|
|
||||||
|
var currentPadding: UIEdgeInsets = .zero
|
||||||
|
|
||||||
|
var currentFontSize: CGFloat = 16
|
||||||
|
|
||||||
|
var currentLineHeightMultiple: CGFloat = 1.5
|
||||||
|
|
||||||
|
var currentThemeBackgroundColor: String?
|
||||||
|
|
||||||
|
var currentThemeTextColor: String?
|
||||||
|
|
||||||
|
var targetLocation: RDEPUBLocation?
|
||||||
|
|
||||||
|
var pendingHighlights: [RDEPUBHighlight] = []
|
||||||
|
|
||||||
|
var fixedSpread: EPUBFixedSpread?
|
||||||
|
|
||||||
|
var isFixedLayout = false
|
||||||
|
|
||||||
|
var pendingProgressionRequest = false
|
||||||
|
|
||||||
|
var fixedLayoutReadyWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
var didRenderCurrentRequest = false
|
||||||
|
|
||||||
|
let debugScope = "ReaderWebView"
|
||||||
|
|
||||||
|
public override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// M-08: script message handler 经 RDEPUBWeakScriptMessageHandler 弱代理注册
|
||||||
|
// (见 configureWebViewIfNeeded),webView → userContentController → self
|
||||||
|
// 的保留环不再成立,deinit 会随最后一个持有者释放而正常触发。
|
||||||
|
// reset() 仍可用于在丢弃路径上提前拆除 WKWebView(不必等 dealloc)。
|
||||||
|
deinit {
|
||||||
|
teardownWebView()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
webView?.frame = bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
public func reset() {
|
||||||
|
delegate = nil
|
||||||
|
onRendered = nil
|
||||||
|
onDecorationsResolved = nil
|
||||||
|
publication = nil
|
||||||
|
currentRenderRequest = nil
|
||||||
|
currentSpineIndex = 0
|
||||||
|
currentHref = ""
|
||||||
|
currentPageIndex = 0
|
||||||
|
currentTotalPagesInChapter = 1
|
||||||
|
viewportSize = .zero
|
||||||
|
currentPadding = .zero
|
||||||
|
currentFontSize = 16
|
||||||
|
currentLineHeightMultiple = 1.5
|
||||||
|
currentThemeBackgroundColor = nil
|
||||||
|
currentThemeTextColor = nil
|
||||||
|
targetLocation = nil
|
||||||
|
pendingHighlights = []
|
||||||
|
fixedSpread = nil
|
||||||
|
isFixedLayout = false
|
||||||
|
pendingProgressionRequest = false
|
||||||
|
didRenderCurrentRequest = false
|
||||||
|
teardownWebView()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func load(publication: RDEPUBPublication, request: RDEPUBRenderRequest) {
|
||||||
|
configureWebViewIfNeeded(publication: publication)
|
||||||
|
guard let webView else { return }
|
||||||
|
|
||||||
|
self.publication = publication
|
||||||
|
self.currentRenderRequest = request
|
||||||
|
self.currentSpineIndex = request.primarySpineIndex
|
||||||
|
self.currentHref = request.primaryHref
|
||||||
|
self.didRenderCurrentRequest = false
|
||||||
|
cancelFixedLayoutReadyFallback()
|
||||||
|
|
||||||
|
let publicationKey = publication.parser.opfURL?.path ?? publication.parser.extractionRootURL?.path ?? ""
|
||||||
|
let loadSignature = pageLoadSignature(publicationKey: publicationKey, request: request)
|
||||||
|
RDEPUBWebViewDebug.log(
|
||||||
|
debugScope,
|
||||||
|
message: "load request=\(request.isFixedLayout ? "fixed" : "reflowable") signature=\(loadSignature) webView=\(RDEPUBWebViewDebug.webViewID(webView))"
|
||||||
|
)
|
||||||
|
|
||||||
|
switch request {
|
||||||
|
case .reflowable(let reflowableRequest):
|
||||||
|
handleReflowableLoad(publication: publication, request: reflowableRequest, loadSignature: loadSignature, in: webView)
|
||||||
|
case .fixed(let fixedRequest):
|
||||||
|
handleFixedLayoutLoad(publication: publication, request: fixedRequest, loadSignature: loadSignature, in: webView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageLoadSignature(
|
||||||
|
publicationKey: String,
|
||||||
|
request: RDEPUBRenderRequest
|
||||||
|
) -> String {
|
||||||
|
switch request {
|
||||||
|
case .reflowable(let request):
|
||||||
|
return reflowableLoadSignature(publicationKey: publicationKey, request: request)
|
||||||
|
case .fixed(let request):
|
||||||
|
return fixedLayoutLoadSignature(publicationKey: publicationKey, request: request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rendered() {
|
||||||
|
guard !didRenderCurrentRequest else { return }
|
||||||
|
didRenderCurrentRequest = true
|
||||||
|
cancelFixedLayoutReadyFallback()
|
||||||
|
RDEPUBWebViewDebug.log(debugScope, message: "rendered spine=\(currentSpineIndex) href=\(currentHref) fixed=\(isFixedLayout)")
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.delegate?.epubWebViewDidFinishRendering(self)
|
||||||
|
self.onRendered?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user