Compare commits

...

4 Commits

Author SHA1 Message Date
shen
8ccb7157b2 feat: improve epub reader controls and annotations 2026-07-12 12:23:10 +08:00
shen
063a493b18 fix epub reader playback and trial navigation 2026-07-11 08:16:56 +08:00
shenlei
d7fcda345d 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
2026-07-10 19:44:53 +09:00
shenlei
d5a7755702 feat: 支持加密 EPUB、试读墙、工具栏定制与朝向锁定
- 新增 RDEPUBResourceDataProvider 解密钩子协议 + 访问登记表,收敛章节 HTML/
  图片/内联 CSS/脚注/封面/图片查看器/正文取图等读取点,scheme handler 对加密书
  禁用流式分支;新增 RDEPUBDecryptingImageAttachment 解密 DTCoreText 图片附件;
  RDEPUBReaderDependencies.live(resourceDataProvider:) 便捷注入。明文书零影响。
- 试读墙:configuration.trialPolicy + delegate epubReaderTrialWallView/
  DidReachTrialWall,UI 由宿主提供(RDEPUBReaderController+Trial)。
- 工具栏定制:RDEPUBReaderTop/BottomToolViewProtocol 协议 + dependencies 工厂注入,
  内置栏已 conform,configureTopToolView 改协议类型。
- 朝向锁定:RDEPUBMetadata.orientation(OPF rendition:orientation 主,
  iBooks display-options 兜底)+ RDEPUBReaderController+Orientation 重写支持朝向、
  打开后主动转向。

注:RDEPUBReaderController+LocationResolution / PaginationCoordinator 为本次改动前
即存在的工作区修改,一并纳入。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:25:17 +09:00
270 changed files with 5302 additions and 3421 deletions

18
.gitignore vendored
View File

@ -6,3 +6,21 @@ xcuserdata/
.artifacts/
.deriveddata/
.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/

View File

@ -1,7 +1,7 @@
# ReadViewSDK 代码审查报告
> 审查日期2026-06-26
> 审查范围Sources/RDReaderView 全部源码
> 审查范围Sources/RDEpubReaderView 全部源码
> 审查方法:逐文件阅读 + 交叉验证 + 线程模型分析
---
@ -148,4 +148,4 @@ func currentTextPageSize() -> CGSize {
| 🟡 P2 | P2-2 | javaScriptStringLiteral 实现依赖隐式假设 | 可维护性 |
| 🔵 P3 | P3-1 | 后台线程同步 hop 主线程获取布局信息 | 线程模型 |
**整体评价**: SDK 架构设计良好模块分层清晰引用管理unowned/weak使用正确分页取消机制有 `paginationToken` + `cancellationController` 兜底。主要值得修复的是三个性能类 P1 问题(主线程阻塞搜索、资源加载阻塞、缓存无清理),两个 P2 鲁棒性/可维护性问题,以及一个 P3 线程模型优化建议。
**整体评价**: SDK 架构设计良好模块分层清晰引用管理unowned/weak使用正确分页取消机制有 `paginationToken` + `cancellationController` 兜底。主要值得修复的是三个性能类 P1 问题(主线程阻塞搜索、资源加载阻塞、缓存无清理),两个 P2 鲁棒性/可维护性问题,以及一个 P3 线程模型优化建议。

View File

@ -8,7 +8,7 @@
## 1. RDEPUBReaderController — 入口控制器
**文件**`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift` 及扩展
**文件**`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift` 及扩展
### 1.1 初始化
@ -180,7 +180,7 @@ func nativeTextSemanticSummary() -> String?
## 2. RDEPUBReaderDelegate — 委托协议
**文件**`Sources/RDReaderView/EPUBUI/RDEPUBReaderDelegate.swift`
**文件**`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderDelegate.swift`
所有方法均有默认空实现,可按需实现。
@ -232,7 +232,7 @@ public protocol RDEPUBReaderDelegate: AnyObject {
## 3. RDEPUBReaderPersistence — 持久化协议
**文件**`Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
**文件**`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
```swift
public protocol RDEPUBReaderPersistence: AnyObject {
@ -264,7 +264,7 @@ public protocol RDEPUBReaderPersistence: AnyObject {
## 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 |
| `numberOfColumns` | `Int` | `1` | 每页列数1 或 2 |
| `columnGap` | `CGFloat` | `20` | 列间距 |
| `displayType` | `RDReaderView.DisplayType` | `.pageCurl` | 翻页模式 |
| `displayType` | `RDEpubReaderView.DisplayType` | `.pageCurl` | 翻页模式 |
| `landscapeDualPageEnabled` | `Bool` | `true` | 横屏双页 |
| `showsTableOfContents` | `Bool` | `true` | 显示目录 |
| `allowsHighlights` | `Bool` | `true` | 允许高亮 |
@ -301,50 +301,50 @@ public protocol RDEPUBReaderPersistence: AnyObject {
## 5. 翻页容器协议
**文件**`Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift`
**文件**`Sources/RDEpubReaderView/ReaderView/RDEpubReaderViewProtocols.swift`
### 5.1 RDReaderPageProvider推荐
### 5.1 RDEpubReaderPageProvider推荐
```swift
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
@objc public protocol RDEpubReaderPageProvider: NSObjectProtocol {
/// 总页数
func numberOfPages(in readerView: RDReaderView) -> Int
func numberOfPages(in readerView: RDEpubReaderView) -> Int
/// 返回指定页的视图
/// - Parameters:
/// - index: 页码索引0-based
/// - 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
@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
public protocol RDReaderPageNavigating: AnyObject {
public protocol RDEpubReaderPageNavigating: AnyObject {
/// 当前页码
var currentPage: Int { get }
@ -357,19 +357,19 @@ public protocol RDReaderPageNavigating: AnyObject {
}
```
### 5.4 RDReaderDataSource遗留
### 5.4 RDEpubReaderDataSource遗留
```swift
@objc public protocol RDReaderDataSource: NSObjectProtocol {
func pageCountOfReaderView(readerView: RDReaderView) -> Int
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
@objc public protocol RDEpubReaderDataSource: NSObjectProtocol {
func pageCountOfReaderView(readerView: RDEpubReaderView) -> Int
func pageContentView(readerView: RDEpubReaderView, pageNum: Int, containerView: UIView?) -> UIView
func pageIdentifier(readerView: RDEpubReaderView, pageNum: Int) -> String?
@objc optional func topToolView(readerView: RDEpubReaderView) -> UIView?
@objc optional func bottomToolView(readerView: RDEpubReaderView) -> UIView?
}
```
通过 `RDReaderLegacyDataSourceAdapter` 自动适配到 `RDReaderPageProvider`。
通过 `RDEpubReaderLegacyDataSourceAdapter` 自动适配到 `RDEpubReaderPageProvider`。
---

View File

@ -22,7 +22,7 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK支持文本重排Reflowable
│ ReadViewDemo (Demo App) │
│ ViewController · LaunchAutomationPlan · UITests │
└──────────────────────────────┬──────────────────────────────┘
│ imports RDReaderView
│ imports RDEpubReaderView
┌──────────────────────────────▼──────────────────────────────┐
│ EPUBUI 层 │
│ RDEPUBReaderController · Coordinators · Settings · TextPage │
@ -30,8 +30,8 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK支持文本重排Reflowable
└──────────────────────────────┬──────────────────────────────┘
┌──────────────────────────────▼──────────────────────────────┐
│ RDReaderView 层 │
│ RDReaderView · FlowLayout · PreloadController │
│ RDEpubReaderView 层 │
│ RDEpubReaderView · FlowLayout · PreloadController │
│ SpreadResolver · TapRegionHandler · PagingController │
└──────────────────────────────┬──────────────────────────────┘
@ -54,7 +54,7 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK支持文本重排Reflowable
|---|---|---|
| **EPUBCore** | EPUB 文件解析、资源管理、WebView 渲染、JS 桥接 | `RDEPUBParser`, `RDEPUBPublication`, `RDEPUBWebView` |
| **EPUBTextRendering** | HTML→NSAttributedString 转换、排版、分页、全书构建 | `RDEPUBTextBookBuilder`, `RDEPUBCoreTextPageFrameFactory` |
| **RDReaderView** | 通用翻页容器、手势识别、页面预加载、双页布局 | `RDReaderView`, `RDReaderFlowLayout`, `RDReaderPreloadController` |
| **RDEpubReaderView** | 通用翻页容器、手势识别、页面预加载、双页布局 | `RDEpubReaderView`, `RDEpubReaderFlowLayout`, `RDEpubReaderPreloadController` |
| **EPUBUI** | 阅读器控制器、协调器模式、设置面板、按需加载、磁盘缓存 | `RDEPUBReaderController`, `RDEPUBReaderPaginationCoordinator` |
---
@ -310,10 +310,10 @@ RDEPUBWebView (UIView)
---
## 7. 翻页容器架构RDReaderView
## 7. 翻页容器架构RDEpubReaderView
```
RDReaderView (UIView)
RDEpubReaderView (UIView)
├─ 三种翻页模式:
│ ├─ .pageCurl → UIPageViewController (翻页动画)
@ -321,14 +321,14 @@ RDReaderView (UIView)
│ └─ .verticalScroll → UICollectionView (垂直滚动)
├─ 组合对象:
│ ├─ RDReaderPagingController // 翻页状态管理、请求队列
│ ├─ RDReaderPreloadController // 页面预加载、缓存管理
│ ├─ RDReaderSpreadResolver // 双页展开计算
│ └─ RDReaderTapRegionHandler // 点击区域分类(左/中/右)
│ ├─ RDEpubReaderPagingController // 翻页状态管理、请求队列
│ ├─ RDEpubReaderPreloadController // 页面预加载、缓存管理
│ ├─ RDEpubReaderSpreadResolver // 双页展开计算
│ └─ RDEpubReaderTapRegionHandler // 点击区域分类(左/中/右)
├─ 数据源协议:
│ ├─ RDReaderPageProvider (新) // 格式无关,优先级高
│ └─ RDReaderDataSource (旧) // 遗留兼容,通过 Adapter 适配
│ ├─ RDEpubReaderPageProvider (新) // 格式无关,优先级高
│ └─ RDEpubReaderDataSource (旧) // 遗留兼容,通过 Adapter 适配
└─ 手势流:
点击 → TapRegionHandler → 分类(左/中/右)
@ -351,7 +351,7 @@ struct RDEPUBReaderConfiguration {
var numberOfColumns: Int // 1 或 2
var columnGap: CGFloat // 默认 20
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 metadataParsingConcurrency: Int // 默认 CPU 核心数
var chapterWindowRadius: Int // 内存缓存窗口半径
@ -374,7 +374,7 @@ struct RDEPUBReaderConfiguration {
## 9. 目录结构
```
Sources/RDReaderView/
Sources/RDEpubReaderView/
├── EPUBCore/ # EPUB 解析与 WebView 渲染
│ ├── Models/ # 数据模型
│ │ ├── RDEPUBAnnotationModels.swift
@ -428,19 +428,19 @@ Sources/RDReaderView/
│ ├── RDEPUBDTCoreTextRenderer.swift # DTCoreText 渲染器实现
│ ├── RDEPUBChapterData.swift # 章节查询门面
│ ├── RDEPUBTextIndexTable.swift # 全书索引表
│ └── RDPlainTextBookBuilder.swift # 纯文本 (.txt) 构建器
│ └── RDEpubPlainTextBookBuilder.swift # 纯文本 (.txt) 构建器
├── ReaderView/ # 通用翻页容器
│ ├── Paging/ # 翻页子系统
│ │ ├── RDReaderPagingController.swift
│ │ ├── RDReaderPreloadController.swift
│ │ ├── RDReaderSpreadResolver.swift
│ │ └── RDReaderTapRegionHandler.swift
│ ├── RDReaderView.swift # 主容器视图
│ ├── RDReaderView+*.swift # 扩展CollectionView/PageCurl/ToolView/ContentAccess
│ ├── RDReaderFlowLayout.swift # 自定义 CollectionView 布局
│ ├── RDReaderContentCell.swift # 滚动模式 Cell
│ └── RDReaderPageChildViewController.swift // 翻页模式子 VC
│ │ ├── RDEpubReaderPagingController.swift
│ │ ├── RDEpubReaderPreloadController.swift
│ │ ├── RDEpubReaderSpreadResolver.swift
│ │ └── RDEpubReaderTapRegionHandler.swift
│ ├── RDEpubReaderView.swift # 主容器视图
│ ├── RDEpubReaderView+*.swift # 扩展CollectionView/PageCurl/ToolView/ContentAccess
│ ├── RDEpubReaderFlowLayout.swift # 自定义 CollectionView 布局
│ ├── RDEpubReaderContentCell.swift # 滚动模式 Cell
│ └── RDEpubReaderPageChildViewController.swift // 翻页模式子 VC
└── EPUBUI/ # 阅读器 UI
├── ReaderController/ # 控制器与协调器
@ -494,7 +494,7 @@ Sources/RDReaderView/
| **Builder** | `RDEPUBBookPageMap.Builder` 增量构建页码映射 |
| **Strategy** | `RDEPUBTextRenderer` 协议,可替换渲染器实现 |
| **Pipeline** | `RDEPUBTextTypesetterPipeline` 排版管线8 个逻辑阶段,封装为 5-6 个顶层调用) |
| **Adapter** | `RDReaderLegacyDataSourceAdapter` 适配旧数据源协议 |
| **Adapter** | `RDEpubReaderLegacyDataSourceAdapter` 适配旧数据源协议 |
| **三级缓存** | 内存 → 磁盘摘要 → 全书分页,逐级降级 |
| **Token 取消** | `paginationToken` 确保过期异步任务不干扰新任务 |
| **Frozen Parameters** | 后台任务冻结 `renderSignature`,避免运行中参数漂移 |

View File

@ -1,6 +1,6 @@
# ReadViewSDK 代码审查报告
> 审查范围:`Sources/RDReaderView/` 全部 184 个 Swift 文件(约 32,300 行)
> 审查范围:`Sources/RDEpubReaderView/` 全部 184 个 Swift 文件(约 32,300 行)
> 审查维度并发安全、内存管理、API 正确性、Swift/iOS 平台特定、架构与依赖
> 审查日期2026-06-26
@ -134,7 +134,7 @@ let stillChanged = abs(currentSize.width - self.lastTextPaginationPageSize!.widt
### H-10 · 依赖版本未锁定
`RDReaderView.podspec` 中 DTCoreText、SnapKit、SSAlertSwift 无版本约束:
`RDEpubReaderView.podspec` 中 DTCoreText、SnapKit、SSAlertSwift 无版本约束:
```ruby
s.dependency 'ZIPFoundation', '~> 0.9' # ✅ 已锁定
@ -143,7 +143,7 @@ s.dependency 'SnapKit' # ❌ 无约束
s.dependency 'SSAlertSwift' # ❌ 无约束
```
**位置:** `RDReaderView.podspec:17-20`
**位置:** `RDEpubReaderView.podspec:17-20`
---

View File

@ -298,14 +298,14 @@ return builder.build()
**触发条件:** `landscapeDualPageEnabled && isLandscape && !verticalScroll`
**页面配对逻辑RDReaderSpreadResolver**
**页面配对逻辑RDEpubReaderSpreadResolver**
- 有封面页:封面独占一屏,后续页面两两配对
- 配对规则:`(coverIndex, nil)`, `(1, 2)`, `(3, 4)`, ...
- 无封面页:标准偶奇配对
- 配对规则:`(0, 1)`, `(2, 3)`, `(4, 5)`, ...
**封面感知布局RDReaderFlowLayout**
**封面感知布局RDEpubReaderFlowLayout**
```
封面页: [====全屏宽度====]
@ -314,7 +314,7 @@ return builder.build()
### 5.3 页面预加载
`RDReaderPreloadController` 管理两个缓存:
`RDEpubReaderPreloadController` 管理两个缓存:
- `preloadedPageViews` - 预渲染的页面视图
- `pageCurlCachedViews` - 当前显示的页面视图
@ -580,7 +580,7 @@ protocol RDEPUBReaderPersistence {
## 9. 纯文本 (.txt) 支持
`RDPlainTextBookBuilder` 复用 EPUB 渲染管线处理 .txt 文件:
`RDEpubPlainTextBookBuilder` 复用 EPUB 渲染管线处理 .txt 文件:
1. **解码:** 依次尝试 UTF-8 → GB18030 → GBK
2. **分章:** 正则匹配 `^(第[零一二三四五六七八九十百千万\d]+[章节回卷].*)$`

View File

@ -34,7 +34,7 @@ epubcfi(/6/4!ch01.xhtml/4/2/1:3)
顶层 CFI 模型,对应一个完整的 `epubcfi(...)` 字符串。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFI.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFI.swift`
```swift
public struct RDEPUBCFI: Codable, Equatable, Hashable {
@ -66,7 +66,7 @@ public enum RDEPUBCFISideBias: String, Codable {
路径模型,由一组 `RDEPUBCFIStep` 组成,描述从根节点到目标节点的遍历序列。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift`
```swift
public struct RDEPUBCFIPath: Codable, Equatable, Hashable {
@ -95,7 +95,7 @@ public struct RDEPUBCFIStep: Codable, Equatable, Hashable {
范围模型,表示文档中的一个连续区域,由父级 CFI 和起止 CFI 组成。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRange.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIRange.swift`
```swift
public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
@ -116,7 +116,7 @@ public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
将 CFI 字符串解析为结构化模型。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift`
```swift
public enum RDEPUBCFIParser {
@ -154,7 +154,7 @@ print(cfi.textAssertion?.exact) // Optional("目标文本")
将 CFI 模型序列化为标准字符串。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift`
```swift
public enum RDEPUBCFISerializer {
@ -192,7 +192,7 @@ let serialized = RDEPUBCFISerializer.serialize(cfi)
将 CFI 解析为可直接用于资源定位的结果。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift`
```swift
public struct RDEPUBCFIResolverResult: Equatable {
@ -228,7 +228,7 @@ print(result.chapterOffset) // Optional(80)
从已知的章节信息生成 CFI。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift`
```swift
public enum RDEPUBCFIGenerator {
@ -274,7 +274,7 @@ public enum RDEPUBCFIGenerator {
从 HTML 源码中提取带有 `id` 属性的元素路径映射。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift`
```swift
public enum RDEPUBCFIDOMPathBuilder {
@ -296,7 +296,7 @@ public enum RDEPUBCFIDOMPathBuilder {
当 CFI 精确定位失败时(如 DOM 结构变更),恢复引擎通过多级降级策略尝试找到最佳匹配位置。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift`
### 5.1 恢复置信度
@ -361,7 +361,7 @@ let result = RDEPUBCFIRecoveryEngine.recover(
文本断言用于在 CFI 定位后验证所指位置的文本内容是否符合预期,增强定位鲁棒性。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift`
```swift
public struct RDEPUBCFITextAssertion: Codable, Equatable, Hashable {
@ -393,7 +393,7 @@ print(assertion?.suffix) // Optional("后续内容")
提供宽松解析接口,兼容非标准 CFI 格式。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFICompatibility.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFICompatibility.swift`
```swift
public enum RDEPUBCFICompatibility {
@ -418,7 +418,7 @@ public enum RDEPUBCFICompatibility {
CFI 映射是章节级别的索引结构,将 CFI 路径映射到章节文本偏移量,是容错恢复引擎的核心数据源。
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift`
### 8.1 RDEPUBCFIMap
@ -506,7 +506,7 @@ public struct RDEPUBCFITokenAnchor: Codable, Equatable {
## 9. 错误类型RDEPUBCFIError
**文件**`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIError.swift`
**文件**`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIError.swift`
```swift
public enum RDEPUBCFIError: Error, Equatable {

View File

@ -16,8 +16,8 @@
- 渐进补全:后台逐步补全所有章节的页码信息
**关键文件**
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/` 目录
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift`
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/ChapterRuntime/` 目录
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift`
---

View File

@ -105,15 +105,15 @@
## Evidence仓库路径
- Build flags`Podfile`
- Pod metadata/toolchain`RDReaderView.podspec`
- Archive extraction`Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift`
- Resource path validationpost-extraction`Sources/RDReaderView/EPUBCore/RDEPUBParser+Resources.swift`
- Scheme handler reads full file bytes`Sources/RDReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift`
- WebView bridge + message handling`Sources/RDReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift`
- External link opening`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
- UserDefaults persistence implementation`Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
- Hidden paginator web view`Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift`
- Debug logging`Sources/RDReaderView/EPUBCore/RDEPUBWebViewDebug.swift`
- Pod metadata/toolchain`RDEpubReaderView.podspec`
- Archive extraction`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser+Archive.swift`
- Resource path validationpost-extraction`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser+Resources.swift`
- Scheme handler reads full file bytes`Sources/RDEpubReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift`
- WebView bridge + message handling`Sources/RDEpubReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift`
- External link opening`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift`
- UserDefaults persistence implementation`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
- Hidden paginator web view`Sources/RDEpubReaderView/EPUBCore/RDEPUBPaginator.swift`
- Debug logging`Sources/RDEpubReaderView/EPUBCore/RDEPUBWebViewDebug.swift`
- Vendored dependencies`Pods/`、`ReadViewDemo/Pods/`
---

View File

@ -6,43 +6,43 @@
## 语言与工程约束
- **主要语言**SwiftPodspec 声明 `s.swift_versions = ["5.10"]`,见 `RDReaderView.podspec`
- **最低系统版本**Podspec `iOS 15.0``RDReaderView.podspec`),示例工程 Podfile/构建设置里常见为 `iOS 15.6``Podfile`
- **主要语言**SwiftPodspec 声明 `s.swift_versions = ["5.10"]`,见 `RDEpubReaderView.podspec`
- **最低系统版本**Podspec `iOS 15.0``RDEpubReaderView.podspec`),示例工程 Podfile/构建设置里常见为 `iOS 15.6``Podfile`
- **依赖管理**CocoaPods`Podfile`、`ReadViewDemo/Podfile`、`Podfile.lock`
## 命名约定
**文件/类型命名Swift**
- 以类型名为文件名的单文件组织较常见:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
- 以类型名为文件名的单文件组织较常见:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`
- 大量使用前缀区分模块域:
- `RD...`:阅读器 UI/控制器相关(如 `Sources/RDReaderView/RDReaderView.swift`、`Sources/RDReaderView/RDURLReaderController.swift`
- `RDEPUB...`EPUB Core/UI/渲染相关(如 `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`
- Extension 文件使用 `+` 命名:`Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift`
- `RD...`:阅读器 UI/控制器相关(如 `Sources/RDEpubReaderView/RDEpubReaderView.swift`、`Sources/RDEpubReaderView/RDEpubURLReaderController.swift`
- `RDEPUB...`EPUB Core/UI/渲染相关(如 `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`
- Extension 文件使用 `+` 命名:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift`
**变量/函数命名:**
- 基本遵循 Swift lowerCamelCase`parse(epubURL:)``Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
- 常量多用 `static let``kRDEPUBHighlightAttributeName``Sources/RDReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift`
- 基本遵循 Swift lowerCamelCase`parse(epubURL:)``Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.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 相关文件常用空行分隔属性/初始化/布局段落
- `// 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`
- API 暴露处使用 `public`、`public final class`、`public enum/struct`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`
- “对外只读、内部可写”常用 `public internal(set)`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
- API 暴露处使用 `public`、`public final class`、`public enum/struct`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`
- “对外只读、内部可写”常用 `public internal(set)`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`
## 导入与依赖使用
**import**
- UIKit/UI 文件:`import UIKit`(大量文件)
- Core/模型文件:`import Foundation`(如 `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`
- Core/模型文件:`import Foundation`(如 `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`
- 三方依赖按需引入:
- `SnapKit`:布局
- `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`
- 未检测到统一日志框架(未发现专用 logging package/config出现时以系统 API/局部输出为主(需按具体文件核对)。
## 注释与文档
- **项目规则(强约束)**:代码标识符保持英文,但**代码注释/文档/提交信息使用中文**(见 `CONTEXT.md`)。
- 历史文件常带 Xcode 头部注释块(示例:`Sources/RDReaderView/ReaderView/RDReaderView.swift`)。
- 公共 API 处存在少量三斜线文档注释(示例:`Sources/RDReaderView/RDReaderView.swift` 的中文说明)。
- 历史文件常带 Xcode 头部注释块(示例:`Sources/RDEpubReaderView/ReaderView/RDEpubReaderView.swift`)。
- 公共 API 处存在少量三斜线文档注释(示例:`Sources/RDEpubReaderView/RDEpubReaderView.swift` 的中文说明)。
## Lint / Formatter / 静态检查
@ -77,9 +77,9 @@
- `CONTEXT.md`
- `Podfile`
- `RDReaderView.podspec`
- `RDEpubReaderView.podspec`
- `ReadViewDemo/ReadViewDemo/ViewController.swift`
- `Sources/RDReaderView/ReaderView/RDReaderView.swift`
- `Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
- `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
- `Sources/RDEpubReaderView/ReaderView/RDEpubReaderView.swift`
- `Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`
- `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`
- `Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift`

View File

@ -471,7 +471,7 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
## 9. 纯文本构建器
**文件:** `RDPlainTextBookBuilder.swift`
**文件:** `RDEpubPlainTextBookBuilder.swift`
从纯文本(.txt文件构建 `RDEPUBTextBook`,用于支持 TXT 格式阅读。

View File

@ -48,7 +48,7 @@ public final class RDEPUBReaderController: UIViewController {
let epubURL: URL // EPUB 文件路径
let persistence: RDEPUBReaderPersistence? // 持久化代理
let dependencies: RDEPUBReaderDependencies // 依赖注入
let readerView = RDReaderView() // 翻页容器
let readerView = RDEpubReaderView() // 翻页容器
}
```
@ -57,7 +57,7 @@ public final class RDEPUBReaderController: UIViewController {
| 文件 | 职责 |
|------|------|
| `RDEPUBReaderController+PublicAPI.swift` | 公开 API跳转、搜索、标注、书签 |
| `RDEPUBReaderController+DataSource.swift` | `RDReaderPageProvider` 实现 |
| `RDEPUBReaderController+DataSource.swift` | `RDEpubReaderPageProvider` 实现 |
| `RDEPUBReaderController+ContentDelegates.swift` | WebView/TextContentView 代理路由 |
| `RDEPUBReaderController+LocationResolution.swift` | 页码/位置解析、阅读状态同步 |
| `RDEPUBReaderController+ExternalLinks.swift` | 外部链接处理(白名单、确认弹窗) |
@ -120,7 +120,7 @@ public protocol RDEPUBReaderPersistence: AnyObject {
```swift
final class RDEPUBReaderContext {
weak var controller: RDEPUBReaderController?
weak var readerView: RDReaderView?
weak var readerView: RDEpubReaderView?
var dependencies: RDEPUBReaderDependencies
var runtime: RDEPUBReaderRuntime?
@ -155,7 +155,7 @@ public struct RDEPUBReaderDependencies {
public var makeParser: () -> RDEPUBParser
public var makePaginator: () -> RDEPUBPaginator
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 static var live: RDEPUBReaderDependencies // 默认实现
@ -659,9 +659,9 @@ struct RDEPUBReaderUIState {
页图协调器(按需分页与全量分页的结果合并)。
### 10.6 RDURLReaderController
### 10.6 RDEpubURLReaderController
**文件:** `RDURLReaderController.swift`
**文件:** `RDEpubURLReaderController.swift`
URL 阅读器控制器(用于打开单个 URL
@ -715,12 +715,12 @@ RDEPUBReaderRuntime.applyParsedPublication()
├── RDEPUBBookPageMap 生成
└── RDReaderView.transitionToPage() → 显示页面
└── RDEpubReaderView.transitionToPage() → 显示页面
用户翻页
RDReaderView.currentPage 变化
RDEpubReaderView.currentPage 变化
├── RDEPUBReaderLocationCoordinator.recordPageChangeIfNeeded()
│ └── persistence.saveLocation()

View File

@ -14,9 +14,9 @@
生成点:
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494``buildPagesFromRanges`
- `Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294`
- `Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift:78`
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494``buildPagesFromRanges`
- `Sources/RDEpubReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294`
- `Sources/RDEpubReaderView/EPUBTextRendering/RDEpubPlainTextBookBuilder.swift:78`
现存全部消费点(已盘点确认,仅 3 处):
@ -27,7 +27,7 @@
**任务**
- [x] 将 `RDEPUBTextPage.content` 改为按需构造:已改为基于 `chapterContent` + `contentRange` 的计算属性(带范围钳制),公开读取 API 不变
- [x] 三个构造点(`RDEPUBChapterLoader` / `RDEPUBTextBookBuilder` / `RDPlainTextBookBuilder`)不再生成每页子串;消费点经计算属性透明按需构造
- [x] 三个构造点(`RDEPUBChapterLoader` / `RDEPUBTextBookBuilder` / `RDEpubPlainTextBookBuilder`)不再生成每页子串;消费点经计算属性透明按需构造
- [x] `Equatable` 确认:`content` 为派生值,由 `contentRange` + `chapterContent` 判等覆盖,语义不变
**API 兼容性注意**
@ -47,7 +47,7 @@
### P0-3 保持现有缓存限制,防止错误复用
- [x] `shouldAvoidReaderPageCaching``RDEPUBTextContentView.swift:319`)保持现状,已补注释说明放开的前置条件是 P1-1 落地
- [x] 本阶段未改 `RDReaderPreloadController` 行为
- [x] 本阶段未改 `RDEpubReaderPreloadController` 行为
---
@ -101,7 +101,7 @@
前置P1-1P1-3 全部落地后,页面视图不再持有整章副本。
- [ ] 调整 `shouldAvoidReaderPageCaching` 判定,允许轻量化后的文本页进入 `RDReaderPreloadController` 缓存
- [ ] 调整 `shouldAvoidReaderPageCaching` 判定,允许轻量化后的文本页进入 `RDEpubReaderPreloadController` 缓存
- [ ] 对比放开前后的翻页流畅度与峰值内存,数据不佳则回退此项
---

View File

@ -6,54 +6,54 @@
## 1. 模块概述
`ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDReaderPageProvider` 协议提供页面内容视图ReaderView 负责容器管理和翻页调度。
`ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDEpubReaderPageProvider` 协议提供页面内容视图ReaderView 负责容器管理和翻页调度。
**文件清单14 个 Swift 文件):**
| 文件 | 核心类型 | 职责 |
|------|----------|------|
| `RDReaderView.swift` | `RDReaderView` | 主容器视图,协调所有子组件 |
| `RDReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 |
| `RDReaderFlowLayout.swift` | `RDReaderFlowLayout` | UICollectionView 自定义布局 |
| `RDReaderGestureController.swift` | `RDReaderGestureController` | 手势控制器(预留) |
| `RDReaderContentCell.swift` | `RDReaderContentCell` | CollectionView 内容 Cell |
| `RDReaderPageChildViewController.swift` | `RDReaderPageChildViewController` | PageCurl 模式子 VC |
| `RDReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 |
| `RDReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 |
| `RDReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 |
| `RDReaderView+ToolView.swift` | Extension | 工具栏安装与动画 |
| `Paging/RDReaderPagingController.swift` | `RDReaderPagingController` | 翻页状态机 |
| `Paging/RDReaderPreloadController.swift` | `RDReaderPreloadController` | 页面预加载与缓存 |
| `Paging/RDReaderSpreadResolver.swift` | `RDReaderSpreadResolver` | 双页展开计算 |
| `Paging/RDReaderTapRegionHandler.swift` | `RDReaderTapRegionHandler` | 点击区域判定 |
| `RDEpubReaderView.swift` | `RDEpubReaderView` | 主容器视图,协调所有子组件 |
| `RDEpubReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 |
| `RDEpubReaderFlowLayout.swift` | `RDEpubReaderFlowLayout` | UICollectionView 自定义布局 |
| `RDEpubReaderGestureController.swift` | `RDEpubReaderGestureController` | 手势控制器(预留) |
| `RDEpubReaderContentCell.swift` | `RDEpubReaderContentCell` | CollectionView 内容 Cell |
| `RDEpubReaderPageChildViewController.swift` | `RDEpubReaderPageChildViewController` | PageCurl 模式子 VC |
| `RDEpubReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 |
| `RDEpubReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 |
| `RDEpubReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 |
| `RDEpubReaderView+ToolView.swift` | Extension | 工具栏安装与动画 |
| `Paging/RDEpubReaderPagingController.swift` | `RDEpubReaderPagingController` | 翻页状态机 |
| `Paging/RDEpubReaderPreloadController.swift` | `RDEpubReaderPreloadController` | 页面预加载与缓存 |
| `Paging/RDEpubReaderSpreadResolver.swift` | `RDEpubReaderSpreadResolver` | 双页展开计算 |
| `Paging/RDEpubReaderTapRegionHandler.swift` | `RDEpubReaderTapRegionHandler` | 点击区域判定 |
---
## 2. 协议定义RDReaderViewProtocols.swift
## 2. 协议定义RDEpubReaderViewProtocols.swift
### 2.1 RDReaderDataSource旧版数据源已废弃
### 2.1 RDEpubReaderDataSource旧版数据源已废弃
```swift
@objc public protocol RDReaderDataSource: NSObjectProtocol {
func pageCountOfReaderView(readerView: RDReaderView) -> Int
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
@objc public protocol RDEpubReaderDataSource: NSObjectProtocol {
func pageCountOfReaderView(readerView: RDEpubReaderView) -> Int
func pageContentView(readerView: RDEpubReaderView, pageNum: Int, containerView: UIView?) -> UIView
func pageIdentifier(readerView: RDEpubReaderView, pageNum: Int) -> String?
@objc optional func topToolView(readerView: RDEpubReaderView) -> UIView?
@objc optional func bottomToolView(readerView: RDEpubReaderView) -> UIView?
}
```
> 向后兼容保留,新代码应使用 `RDReaderPageProvider`。
> 向后兼容保留,新代码应使用 `RDEpubReaderPageProvider`。
### 2.2 RDReaderPageProvider推荐数据源
### 2.2 RDEpubReaderPageProvider推荐数据源
```swift
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
func numberOfPages(in readerView: RDReaderView) -> Int
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
@objc public protocol RDEpubReaderPageProvider: NSObjectProtocol {
func numberOfPages(in readerView: RDEpubReaderView) -> Int
func readerView(_ readerView: RDEpubReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
@objc optional func pageIdentifier(in readerView: RDEpubReaderView, index: Int) -> String?
@objc optional func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView?
@objc optional func readerViewBottomChrome(_ readerView: RDEpubReaderView) -> UIView?
}
```
@ -65,31 +65,31 @@
| `readerViewTopChrome(_:)` | 返回顶部工具栏视图 |
| `readerViewBottomChrome(_:)` | 返回底部工具栏视图 |
### 2.3 RDReaderDelegate
### 2.3 RDEpubReaderDelegate
```swift
@objc public protocol RDReaderDelegate: NSObjectProtocol {
func pageNum(readerView: RDReaderView, pageNum: Int)
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
@objc public protocol RDEpubReaderDelegate: NSObjectProtocol {
func pageNum(readerView: RDEpubReaderView, pageNum: Int)
@objc optional func readerViewOrientationWillChange(readerView: RDEpubReaderView, isLandscape: Bool)
}
```
### 2.4 RDReaderPageNavigating
### 2.4 RDEpubReaderPageNavigating
```swift
public protocol RDReaderPageNavigating: AnyObject {
public protocol RDEpubReaderPageNavigating: AnyObject {
var currentPage: Int { get }
func reloadPages()
func transition(to page: Int, animated: Bool)
}
```
`RDReaderView` 遵循此协议,提供统一的页面导航接口。
`RDEpubReaderView` 遵循此协议,提供统一的页面导航接口。
### 2.5 枚举类型
```swift
extension RDReaderView {
extension RDEpubReaderView {
public enum DisplayType {
case pageCurl // 仿真翻页UIPageViewController
case horizontalScroll // 水平滑动UICollectionView
@ -105,13 +105,13 @@ extension RDReaderView {
---
## 3. 核心类RDReaderView
## 3. 核心类RDEpubReaderView
**文件:** `RDReaderView.swift`
**文件:** `RDEpubReaderView.swift`
`RDReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
`RDEpubReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
- **PageCurl 模式**:使用 `UIPageViewController` 实现仿真翻页
- **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDReaderFlowLayout` 实现滑动翻页
- **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDEpubReaderFlowLayout` 实现滑动翻页
### 3.1 关键属性
@ -124,16 +124,16 @@ extension RDReaderView {
| `coverPageIndex` | `Int?` | 封面页索引(独占一屏) |
| `pagesPerScreen` | `Int` | 每屏页数(横屏双页时为 2 |
| `preloadRadius` | `Int` | 预加载半径(默认 1 |
| `dataSource` | `RDReaderDataSource?` | 旧版数据源 |
| `pageProvider` | `RDReaderPageProvider?` | 推荐数据源 |
| `delegate` | `RDReaderDelegate?` | 事件代理 |
| `dataSource` | `RDEpubReaderDataSource?` | 旧版数据源 |
| `pageProvider` | `RDEpubReaderPageProvider?` | 推荐数据源 |
| `delegate` | `RDEpubReaderDelegate?` | 事件代理 |
| `toolViewAnimationDuration` | `TimeInterval` | 工具栏动画时长0.3s |
### 3.2 关键方法
```swift
/// 切换显示模式pageCurl / horizontalScroll / verticalScroll
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType)
public func switchReaderDisplayType(_ displayType: RDEpubReaderView.DisplayType)
/// 跳转到指定页
public func transitionToPage(pageNum: Int, animated: Bool = false)
@ -154,22 +154,22 @@ public func isFullScreenPage(_ pageNum: Int) -> Bool
|------|------|------|
| `pageViewController` | `UIPageViewController` | PageCurl 翻页引擎 |
| `collectionView` | `UICollectionView` | Scroll 翻页引擎 |
| `layout` | `RDReaderFlowLayout` | CollectionView 自定义布局 |
| `spreadResolver` | `RDReaderSpreadResolver` | 双页配对计算 |
| `tapRegionHandler` | `RDReaderTapRegionHandler` | 点击区域判定 |
| `preloadController` | `RDReaderPreloadController` | 页面预加载与缓存 |
| `pagingController` | `RDReaderPagingController` | 翻页状态管理 |
| `layout` | `RDEpubReaderFlowLayout` | CollectionView 自定义布局 |
| `spreadResolver` | `RDEpubReaderSpreadResolver` | 双页配对计算 |
| `tapRegionHandler` | `RDEpubReaderTapRegionHandler` | 点击区域判定 |
| `preloadController` | `RDEpubReaderPreloadController` | 页面预加载与缓存 |
| `pagingController` | `RDEpubReaderPagingController` | 翻页状态管理 |
---
## 4. 翻页状态机RDReaderPagingController
## 4. 翻页状态机RDEpubReaderPagingController
**文件:** `Paging/RDReaderPagingController.swift`
**文件:** `Paging/RDEpubReaderPagingController.swift`
管理 PageCurl 模式下的翻页请求队列,防止动画冲突。
```swift
struct RDReaderPagingController {
struct RDEpubReaderPagingController {
struct PageTransitionRequest: Equatable {
let pageNum: Int
let animated: Bool
@ -180,7 +180,7 @@ struct RDReaderPagingController {
var didBuildUI: Bool // UI 是否已构建
/// 判断是否应排队请求PageCurl 模式下动画中返回 true
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDEpubReaderView.DisplayType) -> Bool
/// 完成翻页动画,返回待处理的请求
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
struct Environment {
let displayType: RDReaderView.DisplayType
let displayType: RDEpubReaderView.DisplayType
let isLandscape: Bool
let pagesPerScreen: Int
let boundsSize: CGSize
let landscapeDualPageEnabled: Bool
let coverPageIndex: Int?
let totalPages: Int
let spreadResolver: RDReaderSpreadResolver
let spreadResolver: RDEpubReaderSpreadResolver
}
```
---
## 6. 双页展开计算RDReaderSpreadResolver
## 6. 双页展开计算RDEpubReaderSpreadResolver
**文件:** `Paging/RDReaderSpreadResolver.swift`
**文件:** `Paging/RDEpubReaderSpreadResolver.swift`
纯函数式结构体,负责双页模式下的页面配对和导航计算。
```swift
struct RDReaderSpreadResolver {
struct RDEpubReaderSpreadResolver {
/// 判断是否为全屏页(封面页独占一屏)
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
struct RDReaderTapRegionHandler {
func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDReaderView.TapEvent
struct RDEpubReaderTapRegionHandler {
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` 子类,支持水平滚动和垂直滚动两种模式。
@ -294,7 +294,7 @@ struct RDReaderTapRegionHandler {
| 属性 | 类型 | 说明 |
|------|------|------|
| `displayType` | `RDReaderView.DisplayType` | 布局模式 |
| `displayType` | `RDEpubReaderView.DisplayType` | 布局模式 |
| `isLandscapeDualPage` | `Bool` | 是否横屏双页 |
| `coverPageIndex` | `Int?` | 封面页索引 |
| `pagesPerScreen` | `Int` | 每屏页数 |
@ -302,12 +302,12 @@ struct RDReaderTapRegionHandler {
### 8.2 协议
```swift
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
public protocol RDEpubReaderFlowLayoutDataSoure: NSObjectProtocol {
func heigtOfVerticalScrollPage(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) -> CGFloat?
}
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
@objc public protocol RDEpubReaderFlowLayoutDelegate: NSObjectProtocol {
func pageNum(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int)
}
```
@ -324,28 +324,28 @@ func currentContentOffset(count: Int) -> CGPoint
---
## 9. 内容 CellRDReaderContentCell
## 9. 内容 CellRDEpubReaderContentCell
**文件:** `RDReaderContentCell.swift`
**文件:** `RDEpubReaderContentCell.swift`
`UICollectionViewCell` 子类,用于 Scroll 模式下承载页面内容视图。
```swift
class RDReaderContentCell: UICollectionViewCell {
class RDEpubReaderContentCell: UICollectionViewCell {
var containerView: UIView? // 设置时自动添加到 contentView移除旧视图
}
```
---
## 10. PageCurl 子控制器RDReaderPageChildViewController
## 10. PageCurl 子控制器RDEpubReaderPageChildViewController
**文件:** `RDReaderPageChildViewController.swift`
**文件:** `RDEpubReaderPageChildViewController.swift`
`UIViewController` 子类,作为 `UIPageViewController` 的页面 VC。
```swift
class RDReaderPageChildViewController: UIViewController {
class RDEpubReaderPageChildViewController: UIViewController {
var contentView: UIView? // 内容视图,设置时自动安装到容器
var pageNum: Int // 对应页码
@ -357,7 +357,7 @@ class RDReaderPageChildViewController: UIViewController {
## 11. Extension 汇总
### 11.1 RDReaderView+PageCurl
### 11.1 RDEpubReaderView+PageCurl
实现 `UIPageViewControllerDataSource``UIPageViewControllerDelegate`
@ -367,18 +367,18 @@ class RDReaderPageChildViewController: UIViewController {
- `pageViewController(_:willTransitionTo:)` — 即将翻页时预加载
**特殊页码:**
- `RDReaderView.blankPageNum``Int.max`)— 双页模式下的空白页
- `RDReaderView.blankEndPageNum``Int.max - 1`)— 末尾空白页
- `RDEpubReaderView.blankPageNum``Int.max`)— 双页模式下的空白页
- `RDEpubReaderView.blankEndPageNum``Int.max - 1`)— 末尾空白页
### 11.2 RDReaderView+CollectionView
### 11.2 RDEpubReaderView+CollectionView
实现 `UICollectionViewDataSource`、`RDReaderFlowLayoutDelegate`、`RDReaderFlowLayoutDataSoure`
实现 `UICollectionViewDataSource`、`RDEpubReaderFlowLayoutDelegate`、`RDEpubReaderFlowLayoutDataSoure`
- `collectionView(_:cellForItemAt:)` — 复用预加载视图或创建新 Cell
- `collectionView(_:numberOfItemsInSection:)` — 返回总页数
- `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
```
### 11.4 RDReaderView+ToolView
### 11.4 RDEpubReaderView+ToolView
管理顶部/底部工具栏的安装、显示/隐藏动画:
@ -420,11 +420,11 @@ func updateToolViewHeightConstraintsIfNeeded()
| 模式 | 应用 |
|------|------|
| **策略模式** | `DisplayType` 切换 PageCurl / Scroll 两种翻页策略 |
| **适配器模式** | `RDReaderLegacyDataSourceAdapter` 将旧 `RDReaderDataSource` 适配为 `RDReaderPageProvider` |
| **命令队列** | `RDReaderPagingController` 管理翻页请求队列 |
| **缓存签名** | `RDReaderPreloadController.CacheSignature` 检测环境变化自动失效 |
| **适配器模式** | `RDEpubReaderLegacyDataSourceAdapter` 将旧 `RDEpubReaderDataSource` 适配为 `RDEpubReaderPageProvider` |
| **命令队列** | `RDEpubReaderPagingController` 管理翻页请求队列 |
| **缓存签名** | `RDEpubReaderPreloadController.CacheSignature` 检测环境变化自动失效 |
| **关注点分离** | Extension 将不同功能拆分到独立文件 |
| **纯函数** | `RDReaderSpreadResolver` 和 `RDReaderTapRegionHandler` 无状态计算 |
| **纯函数** | `RDEpubReaderSpreadResolver` 和 `RDEpubReaderTapRegionHandler` 无状态计算 |
---
@ -434,7 +434,7 @@ func updateToolViewHeightConstraintsIfNeeded()
用户点击屏幕
RDReaderTapRegionHandler.resolveTapEvent()
RDEpubReaderTapRegionHandler.resolveTapEvent()
├── .left → goPreviousPage() ──→ spreadResolver.nextPage(forward: false)
├── .right → goNextPage() ──→ spreadResolver.nextPage(forward: true)

View File

@ -116,7 +116,7 @@ xcodebuild test \
建议同时记录以下观测项:
- `prepareOnDemandChapter` 主线程 wall clock
- `RDReaderPreloadController` 预加载命中率
- `RDEpubReaderPreloadController` 预加载命中率
- `bookPageMap` partial extension / full replacement 次数
- 页面静态底图缓存命中率
- CFI 延迟构建完成次数与耗时

View File

@ -16,7 +16,7 @@
**输出**`RDEPUBTypesettingOutput`(渲染请求、诊断信息、兼容性报告)
**关键文件**`Sources/RDReaderView/EPUBTextRendering/Typesetter/`
**关键文件**`Sources/RDEpubReaderView/EPUBTextRendering/Typesetter/`
---

View File

@ -31,12 +31,12 @@ graph TB
IndexTable[RDEPUBTextIndexTable]
end
subgraph RDReaderView
ReaderView[RDReaderView]
FlowLayout[RDReaderFlowLayout]
Preload[RDReaderPreloadController]
Spread[RDReaderSpreadResolver]
TapRegion[RDReaderTapRegionHandler]
subgraph RDEpubReaderView
ReaderView[RDEpubReaderView]
FlowLayout[RDEpubReaderFlowLayout]
Preload[RDEpubReaderPreloadController]
Spread[RDEpubReaderSpreadResolver]
TapRegion[RDEpubReaderTapRegionHandler]
end
subgraph EPUBUI
@ -297,11 +297,11 @@ classDiagram
```mermaid
classDiagram
class RDReaderView {
class RDEpubReaderView {
+currentPage: Int
+currentDisplayType: DisplayType
+pageProvider: RDReaderPageProvider?
+delegate: RDReaderDelegate?
+pageProvider: RDEpubReaderPageProvider?
+delegate: RDEpubReaderDelegate?
+landscapeDualPageEnabled: Bool
+pageDirection: PageDirection
+coverPageIndex: Int?
@ -318,14 +318,14 @@ classDiagram
verticalScroll
}
class RDReaderPageProvider {
class RDEpubReaderPageProvider {
<<protocol>>
+numberOfPages(in:) Int
+readerView(_:viewForPageAt:reusableView:) UIView
+pageIdentifier(in:index:) String?
}
class RDReaderFlowLayout {
class RDEpubReaderFlowLayout {
+displayType: DisplayType
+isLandscapeDualPage: Bool
+coverPageIndex: Int?
@ -333,7 +333,7 @@ classDiagram
+currentPage: Int
}
class RDReaderPreloadController {
class RDEpubReaderPreloadController {
+radius: Int
+pageViewForDisplay(pageNum:environment:contentViewProvider:) UIView?
+takePreloadedView(for:) UIView?
@ -341,13 +341,13 @@ classDiagram
+invalidate(environment:)
}
class RDReaderSpreadResolver {
class RDEpubReaderSpreadResolver {
+isFullScreenPage(...) Bool
+dualPagePair(for:totalPages:coverPageIndex:) (Int, Int?)
+nextPage(from:totalPages:pagesPerScreen:coverPageIndex:forward:) Int?
}
class RDReaderTapRegionHandler {
class RDEpubReaderTapRegionHandler {
+resolveTapEvent(point:viewFrame:isToolViewVisible:) TapEvent
}
@ -359,7 +359,7 @@ classDiagram
right
}
class RDReaderPagingController {
class RDEpubReaderPagingController {
+isTransitioning: Bool
+didBuildUI: Bool
+pendingTransitionRequest: PageTransitionRequest?
@ -367,25 +367,25 @@ classDiagram
+finishPageCurlTransition() PageTransitionRequest?
}
class RDReaderContentCell {
class RDEpubReaderContentCell {
+containerView: UIView?
}
class RDReaderPageChildViewController {
class RDEpubReaderPageChildViewController {
+contentView: UIView?
+pageNum: Int
}
RDReaderView --> DisplayType : uses
RDReaderView --> RDReaderPageProvider : delegates
RDReaderView --> RDReaderFlowLayout : owns
RDReaderView --> RDReaderPreloadController : owns
RDReaderView --> RDReaderSpreadResolver : owns
RDReaderView --> RDReaderTapRegionHandler : owns
RDReaderView --> RDReaderPagingController : owns
RDReaderView --> RDReaderContentCell : creates
RDReaderView --> RDReaderPageChildViewController : creates
RDReaderTapRegionHandler --> TapEvent : produces
RDEpubReaderView --> DisplayType : uses
RDEpubReaderView --> RDEpubReaderPageProvider : delegates
RDEpubReaderView --> RDEpubReaderFlowLayout : owns
RDEpubReaderView --> RDEpubReaderPreloadController : owns
RDEpubReaderView --> RDEpubReaderSpreadResolver : owns
RDEpubReaderView --> RDEpubReaderTapRegionHandler : owns
RDEpubReaderView --> RDEpubReaderPagingController : owns
RDEpubReaderView --> RDEpubReaderContentCell : creates
RDEpubReaderView --> RDEpubReaderPageChildViewController : creates
RDEpubReaderTapRegionHandler --> TapEvent : produces
```
---
@ -403,7 +403,7 @@ classDiagram
+highlights: [RDEPUBHighlight]
+bookmarks: [RDEPUBBookmark]
+tableOfContents: [EPUBTableOfContentsItem]
+readerView: RDReaderView
+readerView: RDEpubReaderView
+readerContext: RDEPUBReaderContext
+runtime: RDEPUBReaderRuntime?
}

View File

@ -47,7 +47,7 @@
## 项目信息
- **模块总数:** 4 个EPUBCore、EPUBTextRendering、RDReaderView、EPUBUI
- **模块总数:** 4 个EPUBCore、EPUBTextRendering、RDEpubReaderView、EPUBUI
- **Swift 文件数:** 142 个SDK Sources
- **测试用例数:** 23 个测试类,约 99 个测试方法UI 测试)
- **最低 iOS 版本:** 15.6

View File

@ -5,7 +5,7 @@ workspace 'ReadViewSDK.xcworkspace'
project 'ReadViewSDK.xcodeproj'
target 'ReadViewSDK' do
pod 'RDReaderView', :path => '..'
pod 'RDEpubReaderView', :path => '..'
end
post_install do |installer|

19
RDEpubReaderView.podspec Normal file
View 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

View File

@ -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

View File

@ -3,14 +3,14 @@ platform :ios, '15.6'
target 'ReadViewDemo' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'RDReaderView', :path => '..'
pod 'RDEpubReaderView', :path => '..'
end
post_install do |installer|
apply_settings = lambda do |config|
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
# Pods project (Pods.xcodeproj)

View File

@ -16,13 +16,13 @@ PODS:
- DTFoundation/Core
- DTFoundation/UIKit (1.7.19):
- DTFoundation/Core
- RDReaderView (0.0.1):
- RDEpubReaderView (0.0.1):
- DTCoreText (~> 1.6)
- ZIPFoundation (~> 0.9)
- ZIPFoundation (0.9.20)
DEPENDENCIES:
- RDReaderView (from `..`)
- RDEpubReaderView (from `..`)
SPEC REPOS:
trunk:
@ -31,15 +31,15 @@ SPEC REPOS:
- ZIPFoundation
EXTERNAL SOURCES:
RDReaderView:
RDEpubReaderView:
:path: ".."
SPEC CHECKSUMS:
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
RDReaderView: 2e0eeeff4bcfe8bbc09642344d326440af280ed5
RDEpubReaderView: 5ff99b803f5a46e5ff4ebef3024dd217284c69ed
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
PODFILE CHECKSUM: adda84af3e8577b2a99d5b2cecf381511352f0bd
COCOAPODS: 1.16.2

View 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": "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.git",
"authors": {
"shenlei": "shenlei@touchread.com"
},
"source": {
"git": "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.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"
}

View File

@ -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"
}

View File

@ -16,13 +16,13 @@ PODS:
- DTFoundation/Core
- DTFoundation/UIKit (1.7.19):
- DTFoundation/Core
- RDReaderView (0.0.1):
- RDEpubReaderView (0.0.1):
- DTCoreText (~> 1.6)
- ZIPFoundation (~> 0.9)
- ZIPFoundation (0.9.20)
DEPENDENCIES:
- RDReaderView (from `..`)
- RDEpubReaderView (from `..`)
SPEC REPOS:
trunk:
@ -31,15 +31,15 @@ SPEC REPOS:
- ZIPFoundation
EXTERNAL SOURCES:
RDReaderView:
RDEpubReaderView:
:path: ".."
SPEC CHECKSUMS:
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
RDReaderView: 2e0eeeff4bcfe8bbc09642344d326440af280ed5
RDEpubReaderView: 5ff99b803f5a46e5ff4ebef3024dd217284c69ed
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
PODFILE CHECKSUM: adda84af3e8577b2a99d5b2cecf381511352f0bd
COCOAPODS: 1.16.2

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh
${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.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

View File

@ -1,4 +1,4 @@
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.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

View File

@ -1,5 +1,5 @@
${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh
${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.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

View File

@ -1,4 +1,4 @@
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.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

View File

@ -178,13 +178,13 @@ code_sign_if_enabled() {
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.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"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.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"
fi
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then

View File

@ -1,12 +1,12 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
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
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'
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_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_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}/RDEpubReaderView" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

View File

@ -1,12 +1,12 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
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
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'
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_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_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}/RDEpubReaderView" "-F${PODS_CONFIGURATION_BUILD_DIR}/ZIPFoundation"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

View File

@ -0,0 +1,5 @@
#import <Foundation/Foundation.h>
@interface PodsDummy_RDEpubReaderView : NSObject
@end
@implementation PodsDummy_RDEpubReaderView
@end

View File

@ -11,6 +11,6 @@
#endif
FOUNDATION_EXPORT double RDReaderViewVersionNumber;
FOUNDATION_EXPORT const unsigned char RDReaderViewVersionString[];
FOUNDATION_EXPORT double RDEpubReaderViewVersionNumber;
FOUNDATION_EXPORT const unsigned char RDEpubReaderViewVersionString[];

View File

@ -1,5 +1,5 @@
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"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift

View File

@ -0,0 +1,6 @@
framework module RDEpubReaderView {
umbrella header "RDEpubReaderView-umbrella.h"
export *
module * { export * }
}

View File

@ -1,5 +1,5 @@
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"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift

View File

@ -1,5 +0,0 @@
#import <Foundation/Foundation.h>
@interface PodsDummy_RDReaderView : NSObject
@end
@implementation PodsDummy_RDReaderView
@end

View File

@ -1,6 +0,0 @@
framework module RDReaderView {
umbrella header "RDReaderView-umbrella.h"
export *
module * { export * }
}

View File

@ -18,9 +18,10 @@
1A2B3C4D00000011AABBCC01 /* DemoReaderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000012AABBCC01 /* DemoReaderState.swift */; };
1A2B3C4D00000014AABBCC01 /* FanrenParseTimeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */; };
1A2B3C4D00000015AABBCC01 /* AsyncChapterLoadingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.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 */; };
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */; };
1A2B3C4D00000021AABBCC01 /* MemoryFootprintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */; };
3BC5C96D7A0ACF35F2192CC7 /* XCUIApplication+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B4C44287820D68ED6570F8 /* XCUIApplication+Launch.swift */; };
4509ED928F228F43888E063D /* ReaderToolbarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADF0A18AD034B74A482A4C1 /* ReaderToolbarTests.swift */; };
52A6A0C9941A4B5F9B88C6B0 /* ReaderAnnotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BD15E5D8F04E7E9A239E14 /* ReaderAnnotationTests.swift */; };
@ -54,6 +55,8 @@
1A2B3C4D00000012AABBCC01 /* DemoReaderState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DemoReaderState.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>"; };
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>"; };
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>"; };
@ -65,7 +68,6 @@
9EDF066BA12974E6CFBE519F /* ReaderOpenCloseTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderOpenCloseTests.swift; sourceTree = "<group>"; };
BADF0A18AD034B74A482A4C1 /* ReaderToolbarTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderToolbarTests.swift; sourceTree = "<group>"; };
CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SearchTests.swift; sourceTree = "<group>"; };
1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MemoryFootprintTests.swift; sourceTree = "<group>"; };
DE070C1D2FBF0CC900ED065F /* ReadViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReadViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DisplayTypeTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -196,6 +198,7 @@
CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */,
1A2B3C4D00000020AABBCC01 /* MemoryFootprintTests.swift */,
1A2B3C4D00000016AABBCC01 /* AsyncChapterLoadingTests.swift */,
1A2B3C4D00000031AABBCC01 /* FixedLayoutRotationTests.swift */,
);
path = ReaderUITests;
sourceTree = "<group>";
@ -362,6 +365,7 @@
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */,
1A2B3C4D00000021AABBCC01 /* MemoryFootprintTests.swift in Sources */,
1A2B3C4D00000015AABBCC01 /* AsyncChapterLoadingTests.swift in Sources */,
1A2B3C4D00000030AABBCC01 /* FixedLayoutRotationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -394,7 +398,7 @@
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MG4Z7FU83W;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.shen.ReadViewDemoUITests;
PRODUCT_MODULE_NAME = ReadViewDemoUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -419,7 +423,7 @@
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MG4Z7FU83W;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.shen.ReadViewDemoUITests;
PRODUCT_MODULE_NAME = ReadViewDemoUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -452,7 +456,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -491,7 +495,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -563,7 +567,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@ -620,7 +624,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;

View File

@ -1,5 +1,5 @@
import UIKit
import RDReaderView
import RDEpubReaderView
final class ViewController: UIViewController {
private struct DemoBook: Equatable {
@ -10,10 +10,10 @@ final class ViewController: UIViewController {
private struct LaunchAutomationPlan {
let bookTitleQuery: String
let displayType: RDReaderView.DisplayType?
let displayType: RDEpubReaderView.DisplayType?
let pageNumber: Int?
let resetsReaderState: Bool
let displaySequence: [RDReaderView.DisplayType]
let displaySequence: [RDEpubReaderView.DisplayType]
let stepDelay: TimeInterval
let windowSize: Int?
let concurrency: Int?
@ -23,7 +23,7 @@ final class ViewController: UIViewController {
let enablesVerboseWebLogs: Bool
let searchKeyword: String?
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDReaderView.DisplayType? {
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDEpubReaderView.DisplayType? {
switch rawValue.lowercased() {
case "pagecurl", "curl":
return .pageCurl
@ -213,8 +213,8 @@ final class ViewController: UIViewController {
_ book: DemoBook,
configuration: RDEPUBReaderConfiguration = .default,
automationPlan: LaunchAutomationPlan? = nil
) -> RDURLReaderController {
let controller = RDURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
) -> RDEpubURLReaderController {
let controller = RDEpubURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
controller.title = book.title
if let navigationController {

View File

@ -2,7 +2,7 @@
这是 ReadViewSDK 的 Phase 5 纯文本验证样本。
它用于覆盖 RDURLReaderController 与 RDPlainTextBookBuilder 的 TXT 路径,
它用于覆盖 RDEpubURLReaderController 与 RDEpubPlainTextBookBuilder 的 TXT 路径,
确认纯文本分页、打开主流程,以及后续 reader 壳层回归不会失效。
为了让 UI 自动化可以稳定验证翻页、长按选区、书签和目录行为,

View File

@ -54,7 +54,6 @@ enum IDs {
static let settingsFontValue = "epub.reader.settings.font.value"
static let settingsFontChoice = "epub.reader.settings.font.choice"
static let settingsLineHeight = "epub.reader.settings.lineHeight"
static let settingsColumns = "epub.reader.settings.columns"
static let settingsDisplayType = "epub.reader.settings.displayType"
static let settingsDone = "epub.reader.settings.done"
static func settingsTheme(_ index: Int) -> String { "epub.reader.settings.theme.\(index)" }

View File

@ -28,6 +28,7 @@ struct DemoReaderState {
var isOpened: Bool { fields["reader"] == "opened" }
var page: Int? { fields["page"].flatMap(Int.init) }
var pagesPerScreen: Int? { fields["pagesPerScreen"].flatMap(Int.init) }
var display: String? { fields["display"] }
var toolbar: String? { fields["toolbar"] }
var highlights: Int? { fields["highlights"].flatMap(Int.init) }

View File

@ -394,6 +394,10 @@ final class AsyncChapterLoadingTests: XCTestCase {
for _ in 0..<10 {
paging.swipeLeft()
RunLoop.current.run(until: Date().addingTimeInterval(0.4))
XCTAssertTrue(
app.activityIndicators[IDs.readerLoadingSpinner].waitForNonExistence(timeout: 10),
"横屏翻页后的页面不应一直停留在 loading"
)
}
}

View File

@ -0,0 +1,63 @@
import XCTest
/// pre-paginated
/// 使 WKWebView
/// script message handler WebView
/// CA commit abortCA::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))
let landscapeState = app.waitForDemoReaderState(
timeout: 5,
description: "固定版式横屏保持单页"
) { $0.pagesPerScreen == 1 }
XCTAssertEqual(landscapeState.pagesPerScreen, 1, "交互型固定版式书横屏不应启用双页")
XCUIDevice.shared.orientation = .portrait
RunLoop.current.run(until: Date().addingTimeInterval(2))
}
XCTAssertEqual(app.state, .runningForeground, "反复旋转后 app 已退出(疑似崩溃)")
app.waitForReader()
}
func testFixedLayoutCachedPageCanReturnAfterPageTurn() {
app.launchAndOpenSampleBook(bookTitleQuery: "熊爷爷")
app.waitForReader()
let content = app.otherElements[IDs.readerContent]
XCTAssertTrue(content.waitForExistence(timeout: 5), "固定版式阅读区域未出现")
let initialState = app.waitForDemoReaderState(timeout: 5, description: "固定版式初始页") {
$0.page != nil
}
let initialPage = initialState.page ?? 0
content.swipeLeft()
let nextState = app.waitForDemoReaderState(timeout: 8, description: "固定版式向后翻页") { state in
guard let page = state.page else { return false }
return page != initialPage
}
content.swipeRight()
let returnedState = app.waitForDemoReaderState(timeout: 8, description: "固定版式返回缓存页") {
$0.page == initialPage
}
XCTAssertNotEqual(nextState.page, returnedState.page, "固定版式返回缓存页后页码未恢复")
XCTAssertTrue(content.exists, "返回缓存页后阅读区域不应消失")
}
}

View File

@ -19,9 +19,9 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
else if app.buttons["高亮"].waitForExistence(timeout: 2) { app.buttons["高亮"].tap() }
else if app.buttons["划线"].waitForExistence(timeout: 2) { app.buttons["划线"].tap() }
app.waitForDemoReaderState(timeout: 5, description: "highlights exists") { $0.highlights != nil }
@ -42,7 +42,7 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
@ -63,7 +63,7 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
@ -102,7 +102,7 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
@ -112,15 +112,14 @@ final class HighlightsManagementTests: XCTestCase {
XCTAssertTrue(highlightsTable.waitForExistence(timeout: 5))
XCTAssertTrue(waitForCellCount(in: highlightsTable, minimum: 1, timeout: 5))
highlightsTable.cells.element(boundBy: 0).tap()
let editButton = app.navigationBars.buttons["编辑"].firstMatch
XCTAssertTrue(editButton.waitForExistence(timeout: 3), "标注列表应提供编辑入口")
editButton.tap()
let deleteButton = app.buttons["删除标注"].firstMatch
let deleteHighlightButton = app.buttons["删除高亮"].firstMatch
if deleteButton.waitForExistence(timeout: 3) {
deleteButton.tap()
} else if deleteHighlightButton.waitForExistence(timeout: 1) {
deleteHighlightButton.tap()
}
let deleteControl = highlightsTable.buttons["删除"].firstMatch
XCTAssertTrue(deleteControl.waitForExistence(timeout: 3), "编辑状态应显示删除控件")
deleteControl.tap()
app.buttons["删除"].lastMatch.tap()
XCTAssertTrue(app.staticTexts[IDs.readerHighlightsEmptyLabel].waitForExistence(timeout: 5), "删除后应显示空状态")
}

View File

@ -136,8 +136,8 @@ final class LocationPersistenceTests: XCTestCase {
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.55, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["高亮"].waitForExistence(timeout: 3))
app.menuItems["高亮"].tap()
XCTAssertTrue(app.menuItems["划线"].waitForExistence(timeout: 3))
app.menuItems["划线"].tap()
app.waitForDemoReaderState(timeout: 8, description: "highlights=1") { $0.highlights == 1 }
app.showReaderChromeIfNeeded()
@ -167,7 +167,7 @@ final class LocationPersistenceTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightItem = app.menuItems["高亮"]
let highlightItem = app.menuItems["划线"]
XCTAssertTrue(highlightItem.waitForExistence(timeout: 3), "应出现高亮菜单")
highlightItem.tap()
app.waitForDemoReaderState(timeout: 8, description: "highlights=1") { $0.highlights == 1 }

View File

@ -22,7 +22,7 @@ final class ReaderAnnotationTests: XCTestCase {
app.waitForDemoReaderState(timeout: 5, description: "selection=1") { $0.selection == 1 }
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
let highlightButton = app.buttons[IDs.readerSelectionHighlight]
if highlightMenuItem.waitForExistence(timeout: 3) {
highlightMenuItem.tap()
@ -65,7 +65,7 @@ final class ReaderAnnotationTests: XCTestCase {
app.waitForDemoReaderState(timeout: 5, description: "selection=1") { $0.selection == 1 }
let annotateMenuItem = app.menuItems[""]
let annotateMenuItem = app.menuItems[""]
XCTAssertTrue(annotateMenuItem.waitForExistence(timeout: 3), "选中文本后未出现批注菜单")
annotateMenuItem.tap()

View File

@ -29,7 +29,7 @@ final class SelectionAnnotateTests: XCTestCase {
Thread.sleep(forTimeInterval: 0.5)
//
let annotateItem = app.menuItems[""]
let annotateItem = app.menuItems[""]
guard annotateItem.waitForExistence(timeout: 3) else {
throw XCTSkip("批注菜单项不存在")
}
@ -56,7 +56,7 @@ final class SelectionAnnotateTests: XCTestCase {
Thread.sleep(forTimeInterval: 0.5)
let annotateItem = app.menuItems[""]
let annotateItem = app.menuItems[""]
guard annotateItem.waitForExistence(timeout: 3) else {
throw XCTSkip("批注菜单项不存在")
}
@ -101,7 +101,7 @@ final class SelectionAnnotateTests: XCTestCase {
start.press(forDuration: 0.8, thenDragTo: end)
Thread.sleep(forTimeInterval: 0.5)
let highlightItem = app.menuItems["高亮"]
let highlightItem = app.menuItems["划线"]
guard highlightItem.waitForExistence(timeout: 3) else {
throw XCTSkip("高亮菜单项不存在")
}

View File

@ -18,7 +18,7 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["拷贝"].waitForExistence(timeout: 3), "选区菜单应包含拷贝选项")
XCTAssertTrue(app.menuItems["复制"].waitForExistence(timeout: 3), "选区菜单应包含拷贝选项")
}
func testSelectionMenuShowsAnnotateOption() throws {
@ -32,7 +32,7 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems[""].waitForExistence(timeout: 3), "选区菜单应包含批注选项")
XCTAssertTrue(app.menuItems[""].waitForExistence(timeout: 3), "选区菜单应包含批注选项")
}
func testTapBlankAreaClearsSelection() throws {
@ -46,14 +46,14 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["拷贝"].waitForExistence(timeout: 3), "选区菜单应出现")
XCTAssertTrue(app.menuItems["复制"].waitForExistence(timeout: 3), "选区菜单应出现")
let contentArea = app.otherElements[IDs.readerContentView].firstMatch
if contentArea.waitForExistence(timeout: 3) {
contentArea.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.1)).tap()
}
let menuGone = !app.menuItems["拷贝"].waitForExistence(timeout: 2)
let menuGone = !app.menuItems["复制"].waitForExistence(timeout: 2)
XCTAssertTrue(menuGone, "点击空白区域后选区菜单应消失")
}
@ -68,7 +68,7 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let copyMenuItem = app.menuItems["拷贝"]
let copyMenuItem = app.menuItems["复制"]
if copyMenuItem.waitForExistence(timeout: 3) { copyMenuItem.tap() }
app.waitForDemoReaderState(timeout: 5, description: "reader=opened") { $0.isOpened }

View File

@ -44,24 +44,6 @@ final class SettingsEffectTests: XCTestCase {
XCTAssertTrue(contentView.waitForExistence(timeout: 5), "主题切换后内容区应存在")
}
func testColumnCountChangeUpdatesLayout() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5))
app.scrollViews[IDs.settingsScroll].swipeUp()
let columnsControl = app.segmentedControls[IDs.settingsColumns]
if columnsControl.waitForExistence(timeout: 3) && columnsControl.buttons.count > 1 {
columnsControl.buttons.element(boundBy: 1).tap()
}
app.buttons[IDs.settingsDone].tap()
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
}
func testLineHeightChangeUpdatesContent() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)

View File

@ -36,18 +36,6 @@ final class SettingsExtendedTests: XCTestCase {
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
}
func testColumnCountControl() {
openSettings()
let columns = app.segmentedControls[IDs.settingsColumns]
XCTAssertTrue(columns.waitForExistence(timeout: 3), "分栏控件未出现")
columns.buttons["单栏"].tap()
columns.buttons["双栏"].tap()
app.buttons[IDs.settingsDone].tap()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
}
func testDisplayTypeFromSettings() {
openSettings()
let displayType = app.segmentedControls[IDs.settingsDisplayType]
@ -73,7 +61,6 @@ final class SettingsExtendedTests: XCTestCase {
XCTAssertTrue(app.sliders[IDs.settingsBrightness].waitForExistence(timeout: 3))
XCTAssertTrue(app.segmentedControls[IDs.settingsLineHeight].waitForExistence(timeout: 3))
XCTAssertTrue(app.segmentedControls[IDs.settingsColumns].waitForExistence(timeout: 3))
XCTAssertTrue(app.segmentedControls[IDs.settingsDisplayType].waitForExistence(timeout: 3))
}
}

View File

@ -1,5 +1,5 @@
import UIKit
import RDReaderView
import RDEpubReaderView
final class ViewController: UIViewController {
private struct DemoBook: Equatable {
@ -10,12 +10,12 @@ final class ViewController: UIViewController {
private struct LaunchAutomationPlan {
let bookTitleQuery: String
let displayType: RDReaderView.DisplayType?
let displayType: RDEpubReaderView.DisplayType?
let pageNumber: Int?
let displaySequence: [RDReaderView.DisplayType]
let displaySequence: [RDEpubReaderView.DisplayType]
let stepDelay: TimeInterval
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDReaderView.DisplayType? {
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDEpubReaderView.DisplayType? {
switch rawValue.lowercased() {
case "pagecurl", "curl":
return .pageCurl
@ -328,7 +328,7 @@ final class ViewController: UIViewController {
) -> (report: BookValidationReport, diagnostics: [String]) {
let fileExtension = book.fileURL.pathExtension.lowercased()
if fileExtension == "txt" {
let builder = RDPlainTextBookBuilder()
let builder = RDEpubPlainTextBookBuilder()
do {
let textBook = try builder.build(textFileURL: book.fileURL, pageSize: pageSize, style: style)
let passed = !textBook.pages.isEmpty && !textBook.chapters.isEmpty
@ -646,8 +646,8 @@ final class ViewController: UIViewController {
_ book: DemoBook,
configuration: RDEPUBReaderConfiguration = .default,
automationPlan: LaunchAutomationPlan? = nil
) -> RDURLReaderController {
let controller = RDURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
) -> RDEpubURLReaderController {
let controller = RDEpubURLReaderController(bookURL: book.fileURL, epubConfiguration: configuration)
controller.title = book.title
if let navigationController {

View File

@ -1,9 +1,9 @@
import Foundation
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 {
@ -110,6 +110,10 @@ public struct RDEPUBHighlight: Codable, Equatable {
public var note: String?
/// True when this record was created directly as an annotation instead of
/// adding a note to an existing underline.
public var isAnnotationOnly: Bool
public var createdAt: Date
public init(
@ -118,9 +122,10 @@ public struct RDEPUBHighlight: Codable, Equatable {
location: RDEPUBLocation,
text: String,
rangeInfo: String? = nil,
style: RDEPUBHighlightStyle = .highlight,
style: RDEPUBHighlightStyle = .underline,
color: String = "#F8E16C",
note: String? = nil,
isAnnotationOnly: Bool = false,
createdAt: Date = Date()
) {
self.id = id
@ -131,6 +136,7 @@ public struct RDEPUBHighlight: Codable, Equatable {
self.style = style
self.color = color
self.note = note?.nilIfEmpty
self.isAnnotationOnly = isAnnotationOnly
self.createdAt = createdAt
}
@ -152,6 +158,7 @@ public struct RDEPUBHighlight: Codable, Equatable {
case style
case color
case note
case isAnnotationOnly
case createdAt
}
@ -164,12 +171,13 @@ public struct RDEPUBHighlight: Codable, Equatable {
rangeInfo = try container.decodeIfPresent(String.self, forKey: .rangeInfo)?.nilIfEmpty
if let rawStyle = try container.decodeIfPresent(String.self, forKey: .style),
let decodedStyle = RDEPUBHighlightStyle(rawValue: rawStyle) {
style = decodedStyle
style = decodedStyle == .highlight ? .underline : decodedStyle
} else {
style = .highlight
style = .underline
}
color = try container.decodeIfPresent(String.self, forKey: .color) ?? "#F8E16C"
note = try container.decodeIfPresent(String.self, forKey: .note)?.nilIfEmpty
isAnnotationOnly = try container.decodeIfPresent(Bool.self, forKey: .isAnnotationOnly) ?? false
createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date()
}
@ -183,6 +191,7 @@ public struct RDEPUBHighlight: Codable, Equatable {
try container.encode(style, forKey: .style)
try container.encode(color, forKey: .color)
try container.encodeIfPresent(note, forKey: .note)
try container.encode(isAnnotationOnly, forKey: .isAnnotationOnly)
try container.encode(createdAt, forKey: .createdAt)
}

View File

@ -59,13 +59,22 @@ public final class RDEPUBNoteResolver {
guard let fragment,
!fragment.isEmpty,
let fileURL = resourceResolver.fileURL(forRelativePath: href),
let html = try? String(contentsOf: fileURL, encoding: .utf8) else {
let html = chapterHTML(at: fileURL) else {
return nil
}
return elementHTML(withID: fragment, in: html)
}
/// HTML provider
private func chapterHTML(at fileURL: URL) -> String? {
if let provided = resourceResolver.providedResourceData(at: fileURL) {
return String(data: provided, encoding: .utf8)
?? String(data: provided, encoding: .utf16)
}
return try? String(contentsOf: fileURL, encoding: .utf8)
}
private func elementHTML(withID id: String, in html: String) -> String? {
let escapedID = NSRegularExpression.escapedPattern(for: id)
let pattern = #"<([A-Za-z][A-Za-z0-9:_-]*)(?=[^>]*(?:id|xml:id)\s*=\s*(['"])"# + escapedID + #"\2)[^>]*>"#

View File

@ -60,9 +60,11 @@ enum RDEPUBAssetRepository {
private static var resolvedBundle: Bundle? = {
let hostBundles = [Bundle(for: RDEPUBAssetBundleToken.self), Bundle.main] + Bundle.allFrameworks + Bundle.allBundles
for hostBundle in hostBundles {
if let url = hostBundle.url(forResource: "RDReaderViewAssets", withExtension: "bundle"),
let bundle = Bundle(url: url) {
return bundle
for bundleName in ["RDEpubReaderViewAssets", "RDReaderViewAssets"] {
if let url = hostBundle.url(forResource: bundleName, withExtension: "bundle"),
let bundle = Bundle(url: url) {
return bundle
}
}
}
return nil

View File

@ -24,6 +24,13 @@ public enum RDEPUBPageSpread: String, Codable {
case center
}
/// EPUB rendition:orientation / iBooks display-options
public enum RDEPUBOrientation: String, Codable {
case portrait
case landscape
case auto
}
public struct RDEPUBMetadata: Codable, Equatable {
public var identifier: String?
@ -42,6 +49,10 @@ public struct RDEPUBMetadata: Codable, Equatable {
public var readingProgression: RDEPUBReadingProgression
/// nil =
/// OPF rendition:orientation > META-INF/com.apple.ibooks.display-options.xml
public var orientation: RDEPUBOrientation?
public init(
identifier: String? = nil,
title: String = "",
@ -50,7 +61,8 @@ public struct RDEPUBMetadata: Codable, Equatable {
version: String? = nil,
layout: RDEPUBLayout = .reflowable,
spread: String? = nil,
readingProgression: RDEPUBReadingProgression = .auto
readingProgression: RDEPUBReadingProgression = .auto,
orientation: RDEPUBOrientation? = nil
) {
self.identifier = identifier
self.title = title
@ -60,6 +72,7 @@ public struct RDEPUBMetadata: Codable, Equatable {
self.layout = layout
self.spread = spread
self.readingProgression = readingProgression
self.orientation = orientation
}
}

View File

@ -56,4 +56,4 @@ public struct RDEPUBNavigatorLayoutContext: Equatable {
insets.right = horizontalInsets
return insets
}
}
}

View File

@ -3,7 +3,7 @@ import UIKit
import WebKit
public final class RDEPUBPaginator: NSObject {
private var parser: RDEPUBParser?
private weak var hostingView: UIView?

View File

@ -151,3 +151,48 @@ private final class ContainerXMLParserDelegate: NSObject, XMLParserDelegate {
rootFilePath = attributeDict["full-path"]?.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
/// com.apple.ibooks.display-options.xml orientation-lock
final class IBooksDisplayOptionsParserDelegate: NSObject, XMLParserDelegate {
private(set) var orientationLock: String?
private var isCapturingOrientationOption = false
private var buffer = ""
func parser(
_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String] = [:]
) {
let name = XMLName.localName(from: qName ?? elementName)
guard name == "option" else { return }
let optionName = (attributeDict["name"] ?? "").lowercased()
if optionName == "orientation-lock" {
isCapturingOrientationOption = true
buffer = ""
}
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
guard isCapturingOrientationOption else { return }
buffer += string
}
func parser(
_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?
) {
let name = XMLName.localName(from: qName ?? elementName)
guard name == "option", isCapturingOrientationOption else { return }
isCapturingOrientationOption = false
let value = buffer.trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty, orientationLock == nil {
orientationLock = value
}
}
}

View File

@ -111,11 +111,18 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
updatedItem.title = manifestTitleByID[item.id]
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(
metadata: metadata,
manifest: manifest,
manifestByID: Dictionary(uniqueKeysWithValues: manifest.map { ($0.id, $0) }),
manifestByID: manifestByID,
spineReferences: spineReferences,
ncxItem: manifest.first(where: { $0.id == ncxID || $0.isNCX }),
navigationItem: manifest.first(where: { $0.isNavigationDocument })
@ -180,6 +187,11 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
property == "rendition:spread" {
metadata.spread = attributeDict["content"]
}
if let property = currentMetaProperty?.lowercased(),
property == "rendition:orientation",
let value = attributeDict["content"] {
metadata.orientation = orientation(from: value)
}
if let property = currentMetaProperty?.lowercased(),
property == "title",
let refinesID = currentMetaRefinesID,
@ -270,6 +282,8 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
metadata.layout = layout(from: normalizedValue)
case "rendition:spread":
metadata.spread = normalizedValue
case "rendition:orientation":
metadata.orientation = orientation(from: normalizedValue)
case "dcterms:identifier", "identifier":
if metadata.identifier == nil {
metadata.identifier = normalizedValue
@ -315,6 +329,19 @@ final class OPFPackageParserDelegate: NSObject, XMLParserDelegate {
private func layout(from rawValue: String) -> RDEPUBLayout {
rawValue.lowercased().contains("pre-paginated") ? .fixed : .reflowable
}
private func orientation(from rawValue: String) -> RDEPUBOrientation? {
switch rawValue.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) {
case "portrait":
return .portrait
case "landscape":
return .landscape
case "auto":
return .auto
default:
return nil
}
}
}
enum XMLSection {

View File

@ -62,7 +62,8 @@ extension RDEPUBParser {
}
for item in coverCandidates {
if let fileURL = fileURL(forRelativePath: item.href),
let image = UIImage(contentsOfFile: fileURL.path) {
let data = resourceData(at: fileURL),
let image = UIImage(data: data) {
return image
}
}
@ -80,6 +81,10 @@ extension RDEPUBParser {
guard let fileURL = fileURL(forRelativePath: relativePath) else {
return nil
}
if let provided = providedResourceData(at: fileURL) {
return String(data: provided, encoding: .utf8)
?? String(data: provided, encoding: .utf16)
}
return try? String(contentsOf: fileURL)
}

View File

@ -19,6 +19,28 @@ public final class RDEPUBParser {
opfURL?.deletingLastPathComponent()
}
/// nil
/// `parse(epubURL:)` `RDEPUBReaderDependencies.live(resourceDataProvider:)`
public var resourceDataProvider: RDEPUBResourceDataProvider? {
didSet {
guard resourceDataProvider != nil else { return }
RDEPUBResourceAccessRegistry.register(self)
}
}
/// provider provider provider nil
public func providedResourceData(at fileURL: URL) -> Data? {
resourceDataProvider?.resourceData(at: fileURL)
}
/// provider
public func resourceData(at fileURL: URL) -> Data? {
if let provided = providedResourceData(at: fileURL) {
return provided
}
return try? Data(contentsOf: fileURL)
}
public init() {}
// MARK: - Cache Management
@ -101,6 +123,37 @@ public final class RDEPUBParser {
}
try parseOPF(at: packageURL)
// OPF 退 iBooks display-options
if metadata.orientation == nil {
let displayOptionsURL = extractionURL
.appendingPathComponent("META-INF/com.apple.ibooks.display-options.xml")
if let orientation = parseIBooksOrientation(at: displayOptionsURL) {
metadata.orientation = orientation
}
}
}
/// `com.apple.ibooks.display-options.xml` orientation-lock
/// platform[@name] / option[@name="orientation-lock"]
private func parseIBooksOrientation(at url: URL) -> RDEPUBOrientation? {
guard FileManager.default.fileExists(atPath: url.path),
let parser = XMLParser(contentsOf: url) else {
return nil
}
let delegate = IBooksDisplayOptionsParserDelegate()
parser.delegate = delegate
guard parser.parse() else { return nil }
switch delegate.orientationLock?.lowercased() {
case "portrait-orientation", "portrait":
return .portrait
case "landscape-orientation", "landscape":
return .landscape
case "none":
return .auto
default:
return nil
}
}
public func parseOPF(at opfURL: URL) throws {
@ -121,7 +174,8 @@ public final class RDEPUBParser {
let packageDocument = delegate.packageDocument()
self.opfURL = opfURL
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.tableOfContents = parseTOC(from: packageDocument, opfURL: opfURL)
}
@ -153,4 +207,4 @@ private extension URL {
}
return totalSize
}
}
}

View File

@ -36,6 +36,13 @@ public final class RDEPUBPublication {
parser.readingProfile()
}
/// Scripted fixed-layout publications own a complete interactive scene per
/// spine item. Combining scenes in landscape can start multiple timelines
/// and media tracks, so these publications remain single-page.
public lazy var requiresSinglePagePresentation: Bool = {
layout == .fixed && parser.hasInteractiveContent()
}()
public var readingProgression: RDEPUBReadingProgression {
metadata.readingProgression
}
@ -68,4 +75,4 @@ public final class RDEPUBPublication {
spreadEnabled: fixedLayoutSpreadEnabled(for: preferences, viewportSize: viewportSize)
)
}
}
}

View File

@ -0,0 +1,63 @@
import Foundation
/// EPUB
///
/// 宿 App EPUB
/// SDK HTML CSS provider
/// provider nil SDK 退
///
///
/// 宿SDK
///
/// / 线
/// I/O
public protocol RDEPUBResourceDataProvider: AnyObject {
///
/// - Parameter fileURL:
/// - Returns: nil SDK
func resourceData(at fileURL: URL) -> Data?
}
/// 访
///
/// DTCoreText `<img>` parser
/// provider parser
/// `RDEPUBDecryptingImageAttachment` provider
enum RDEPUBResourceAccessRegistry {
private struct WeakParserBox {
weak var parser: RDEPUBParser?
}
private static let lock = NSLock()
private static var boxes: [WeakParserBox] = []
/// provider parser
static func register(_ parser: RDEPUBParser) {
lock.lock()
defer { lock.unlock() }
boxes.removeAll { $0.parser == nil || $0.parser === parser }
boxes.append(WeakParserBox(parser: parser))
#if canImport(DTCoreText)
RDEPUBDecryptingImageAttachment.registerTagClassIfNeeded()
#endif
}
/// provider
/// parser
static func providedResourceData(at fileURL: URL) -> Data? {
let targetPath = fileURL.resolvingSymlinksInPath().standardizedFileURL.path
lock.lock()
let parsers = boxes.compactMap(\.parser)
lock.unlock()
for parser in parsers {
guard let rootURL = parser.extractionRootURL else { continue }
let rootPath = rootURL.resolvingSymlinksInPath().standardizedFileURL.path
guard targetPath.hasPrefix(rootPath + "/") else { continue }
return parser.providedResourceData(at: fileURL)
}
return nil
}
}

View File

@ -25,6 +25,21 @@ public final class RDEPUBResourceResolver {
parser.fileURL(forResourceURL: resourceURL)
}
///
public var hasResourceDataProvider: Bool {
parser.resourceDataProvider != nil
}
/// provider provider provider nil
public func providedResourceData(at fileURL: URL) -> Data? {
parser.providedResourceData(at: fileURL)
}
/// provider
public func resourceData(at fileURL: URL) -> Data? {
parser.resourceData(at: fileURL)
}
public func normalizedHref(_ href: String, relativeToSpineIndex spineIndex: Int? = nil) -> String? {
guard let opfDirectoryURL else {
return href.components(separatedBy: "#").first

View File

@ -87,7 +87,9 @@ public final class RDEPUBResourceURLSchemeHandler: NSObject, WKURLSchemeHandler
RDEPUBWebViewDebug.logSchemeTask("ResourceScheme", requestURL: requestURL, fileURL: fileURL, event: "resolved")
let fileSize = resourceSize(for: fileURL)
if let fileSize, fileSize > 524_288 {
// provider
// provider
if let fileSize, fileSize > 524_288, parser.resourceDataProvider == nil {
respondWithStreaming(fileURL: fileURL, requestURL: requestURL, taskID: taskID, urlSchemeTask: urlSchemeTask, fileSize: fileSize)
} else {
respondWithInMemoryData(fileURL: fileURL, requestURL: requestURL, taskID: taskID, urlSchemeTask: urlSchemeTask)
@ -133,7 +135,13 @@ public final class RDEPUBResourceURLSchemeHandler: NSObject, WKURLSchemeHandler
return
}
do {
let data = try Data(contentsOf: fileURL)
// provider provider
let data: Data
if let provided = self.parser?.providedResourceData(at: fileURL) {
data = provided
} else {
data = try Data(contentsOf: fileURL)
}
guard self.isTaskActive(taskID) else {
DispatchQueue.main.async {
urlSchemeTask.didFailWithError(NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled))

View File

@ -11,6 +11,10 @@ public enum RDEPUBSafeArea {
/// paddings, not approximations of the safe area itself.
public static let minimumVerticalTextMargin: CGFloat = 20
/// Compact top/bottom padding used by a landscape two-page spread. This
/// applies only beyond the actual safe area, which is always preserved.
public static let landscapeVerticalTextMargin: CGFloat = 12
public static let minimumHorizontalTextMargin: CGFloat = 16
public static func keyWindow() -> UIWindow? {

Some files were not shown because too many files have changed in this diff Show More