Compare commits
3 Commits
ea21c6a831
...
948004eed1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
948004eed1 | ||
|
|
1efb9d172f | ||
|
|
44202357c0 |
@ -21,70 +21,143 @@ RDReaderView 是一个 iOS 阅读器组件库(CocoaPods),提供开箱即
|
|||||||
│
|
│
|
||||||
┌───────────────────────────▼─────────────────────────────┐
|
┌───────────────────────────▼─────────────────────────────┐
|
||||||
│ EPUBUI 层(library 级读者 UI) │
|
│ EPUBUI 层(library 级读者 UI) │
|
||||||
│ RDEPUBReaderController(开箱即用入口,~1995 行) │
|
│ │
|
||||||
│ RDEPUBReaderConfiguration / Theme / Settings / Persistence│
|
│ 主控制器 │
|
||||||
│ TopToolView / BottomToolView / ToolView 基类 │
|
│ RDEPUBReaderController(开箱即用入口) │
|
||||||
│ ChapterList / Highlights / Bookmarks / Settings 面板 │
|
│ +ContentDelegates / +DataSource / +PublicAPI │
|
||||||
│ RDEPUBTextContentView / RDEPUBWebContentView │
|
│ +RenderSupport / +RuntimeBridge / +TableOfContents │
|
||||||
│ RDEPUBPageInteractionController / SelectionOverlayView │
|
│ RDURLReaderController(URL 阅读入口) │
|
||||||
│ RDEPUBPageLayoutSnapshot / RDURLReaderController │
|
│ │
|
||||||
|
│ ReaderController/(协调器) │
|
||||||
|
│ RDEPUBReaderRuntime(中央运行时协调器) │
|
||||||
|
│ RDEPUBReaderContext(上下文状态容器) │
|
||||||
|
│ RDEPUBReaderDependencies(依赖注入) │
|
||||||
|
│ RDEPUBReaderLoadCoordinator(EPUB 加载) │
|
||||||
|
│ RDEPUBReaderPaginationCoordinator(分页协调) │
|
||||||
|
│ RDEPUBReaderLocationCoordinator(位置持久化) │
|
||||||
|
│ RDEPUBReaderAnnotationCoordinator(标注管理) │
|
||||||
|
│ RDEPUBReaderSearchCoordinator(搜索) │
|
||||||
|
│ RDEPUBReaderChromeCoordinator(工具栏) │
|
||||||
|
│ RDEPUBReaderAssemblyCoordinator(UI 组装) │
|
||||||
|
│ RDEPUBReaderViewportMonitor(视口变化监听) │
|
||||||
|
│ │
|
||||||
|
│ Settings/(配置与主题) │
|
||||||
|
│ RDEPUBReaderConfiguration / RDEPUBReaderSettings │
|
||||||
|
│ RDEPUBReaderSettingsViewController / RDEPUBReaderTheme │
|
||||||
|
│ │
|
||||||
|
│ TextPage/(文本页面交互) │
|
||||||
|
│ RDEPUBTextContentView / RDEPUBTextPageRenderView │
|
||||||
|
│ RDEPUBSelectableTextView / RDEPUBTextSelectionController│
|
||||||
|
│ RDEPUBSelectionOverlayView / RDEPUBTextAnnotationOverlay│
|
||||||
|
│ RDEPUBPageInteractionController / RDEPUBPageLayoutSnapshot│
|
||||||
|
│ RDEPUBTextPageDecorationView │
|
||||||
|
│ │
|
||||||
|
│ 工具栏与面板 │
|
||||||
|
│ RDEPUBReaderTopToolView / RDEPUBReaderBottomToolView │
|
||||||
|
│ RDEPUBReaderToolView(基类) │
|
||||||
|
│ RDEPUBReaderChapterListController(目录面板) │
|
||||||
|
│ RDEPUBReaderHighlightsViewController(高亮管理) │
|
||||||
|
│ RDEPUBReaderPersistence(位置持久化) │
|
||||||
|
│ RDEPUBReaderDelegate / RDEPUBReaderTableOfContentsItem│
|
||||||
|
│ RDEPUBWebContentView / RDEPUBWebDecorationOverlayView │
|
||||||
|
│ RDEPUBViewportTypes / UIColor+RDEPUBHex │
|
||||||
└───────────────────────────┬─────────────────────────────┘
|
└───────────────────────────┬─────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌───────────────────────────▼─────────────────────────────┐
|
┌───────────────────────────▼─────────────────────────────┐
|
||||||
│ 翻页容器层(RDReaderView) │
|
│ 翻页容器层(RDReaderView) │
|
||||||
|
│ │
|
||||||
│ RDReaderView(UIView,统一翻页外壳) │
|
│ RDReaderView(UIView,统一翻页外壳) │
|
||||||
│ 4 种翻页模式:pageCurl / horizontalScroll / │
|
│ 3 种翻页模式:pageCurl / horizontalScroll / │
|
||||||
│ verticalScroll / horizontalCoverScroll │
|
│ verticalScroll │
|
||||||
|
│ RDReaderViewProtocols(DataSource / Delegate / DisplayType)│
|
||||||
|
│ +CollectionView / +ContentAccess / +PageCurl / +ToolView │
|
||||||
│ RDReaderFlowLayout / RDReaderContentCell │
|
│ RDReaderFlowLayout / RDReaderContentCell │
|
||||||
│ RDReaderPageChildViewController(pageCurl 页包装) │
|
│ RDReaderPageChildViewController(pageCurl 页包装) │
|
||||||
|
│ RDReaderGestureController │
|
||||||
|
│ │
|
||||||
|
│ Paging/(翻页控制) │
|
||||||
|
│ RDReaderPagingController(转场与排队) │
|
||||||
|
│ RDReaderPreloadController(预加载与缓存) │
|
||||||
|
│ RDReaderSpreadResolver(双页配对) │
|
||||||
|
│ RDReaderTapRegionHandler(手势分区) │
|
||||||
└───────────────────────────┬─────────────────────────────┘
|
└───────────────────────────┬─────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌───────────────────────────▼─────────────────────────────┐
|
┌───────────────────────────▼─────────────────────────────┐
|
||||||
│ EPUBCore 层(EPUB 引擎) │
|
│ EPUBCore 层(EPUB 引擎) │
|
||||||
│ │
|
│ │
|
||||||
│ Publication 层 │
|
│ 解析与模型 │
|
||||||
│ RDEPUBParser(+Archive / +Package / +TOC / │
|
│ RDEPUBParser(+Archive / +Package / +TOC / │
|
||||||
│ +ReadingProfile / +Resources) │
|
│ +ReadingProfile / +Resources) │
|
||||||
│ RDEPUBPublication(出版物聚合对象) │
|
│ RDEPUBPublication(出版物聚合对象) │
|
||||||
│ RDEPUBModels(metadata / manifest / spine 模型) │
|
│ RDEPUBModels(metadata / manifest / spine 模型) │
|
||||||
│ RDEPUBReadingModels(location / viewport / highlight) │
|
│ Models/ │
|
||||||
|
│ RDEPUBReadingLocationModels(location 模型) │
|
||||||
|
│ RDEPUBPaginationModels(分页模型) │
|
||||||
|
│ RDEPUBAnnotationModels(标注模型) │
|
||||||
│ RDEPUBTextAnchor / RDEPUBTextRangeAnchor(文本锚点) │
|
│ RDEPUBTextAnchor / RDEPUBTextRangeAnchor(文本锚点) │
|
||||||
│ RDEPUBRenderRequest(渲染请求模型) │
|
│ RDEPUBRenderRequest(渲染请求模型) │
|
||||||
│ │
|
│ │
|
||||||
│ Services 层 │
|
│ 服务层 │
|
||||||
│ RDEPUBResourceResolver(资源 URL 统一入口) │
|
│ RDEPUBResourceResolver(资源 URL 统一入口) │
|
||||||
|
│ RDEPUBResourceURLSchemeHandler(ss-reader:// 协议) │
|
||||||
│ RDEPUBPreferences(展示参数聚合) │
|
│ RDEPUBPreferences(展示参数聚合) │
|
||||||
│ RDEPUBPaginator(离屏分页服务) │
|
│ RDEPUBPaginator(离屏分页服务) │
|
||||||
|
│ RDEPUBStyleSheetBuilder / RDEPUBJavaScriptBridge │
|
||||||
|
│ RDEPUBFixedLayoutTemplate / RDEPUBAssetRepository │
|
||||||
│ │
|
│ │
|
||||||
│ Navigator 层 │
|
│ 会话与导航 │
|
||||||
│ RDEPUBReadingSession(状态机 + 会话协调) │
|
│ RDEPUBReadingSession(状态机 + 会话协调) │
|
||||||
│ RDEPUBNavigatorState(状态枚举) │
|
│ RDEPUBNavigatorState(状态枚举) │
|
||||||
│ RDEPUBNavigatorLayoutContext │
|
│ RDEPUBNavigatorLayoutContext │
|
||||||
│ │
|
│ │
|
||||||
│ Resource View 层 │
|
│ WebView 渲染 │
|
||||||
│ RDEPUBWebView(+Configuration / +Reflowable / │
|
│ RDEPUBWebView(+Configuration / +Reflowable / │
|
||||||
│ +FixedLayout / +JavaScriptBridge / │
|
│ +FixedLayout / +JavaScriptBridge / │
|
||||||
│ +Search) │
|
│ +Search) │
|
||||||
│ RDEPUBResourceURLSchemeHandler(ss-reader:// 协议) │
|
|
||||||
│ RDEPUBStyleSheetBuilder / RDEPUBJavaScriptBridge │
|
|
||||||
│ RDEPUBFixedLayoutTemplate / RDEPUBAssetRepository │
|
|
||||||
│ RDEPUBWebViewDebug(调试日志工具) │
|
│ RDEPUBWebViewDebug(调试日志工具) │
|
||||||
│ │
|
│ │
|
||||||
│ Search 层 │
|
│ 搜索 │
|
||||||
│ RDEPUBSearchEngine(协议)/ RDEPUBHTMLSearchEngine │
|
│ RDEPUBSearchEngine(协议)/ RDEPUBHTMLSearchEngine │
|
||||||
│ RDEPUBSearchModels(SearchMatch/Result/State/Presentation)│
|
│ RDEPUBSearchModels(SearchMatch/Result/State/Presentation)│
|
||||||
└───────────────────────────┬─────────────────────────────┘
|
└───────────────────────────┬─────────────────────────────┘
|
||||||
|
│
|
||||||
┌───────────────────────────▼─────────────────────────────┐
|
┌───────────────────────────▼─────────────────────────────┐
|
||||||
│ EPUBTextRendering 层(文本 EPUB 渲染) │
|
│ EPUBTextRendering 层(文本 EPUB 渲染) │
|
||||||
|
│ │
|
||||||
|
│ 渲染 │
|
||||||
│ RDEPUBTextRenderer(协议) │
|
│ RDEPUBTextRenderer(协议) │
|
||||||
│ RDEPUBDTCoreTextRenderer(DTCoreText 实现) │
|
│ RDEPUBDTCoreTextRenderer(DTCoreText 实现) │
|
||||||
│ RDEPUBTextRendererSupport / RDEPUBTextPaginationSupport │
|
│ RDPlainTextBookBuilder(纯文本书籍构建) │
|
||||||
│ RDEPUBTextBookBuilder / RDPlainTextBookBuilder │
|
│ RDEPUBTextPositionConverter(位置转换器) │
|
||||||
|
│ RDEPUBTextSearchEngine(文本搜索引擎) │
|
||||||
|
│ RDEPUBTextIndexTable / RDEPUBChapterData │
|
||||||
|
│ │
|
||||||
|
│ BuildPipeline/(构建管线) │
|
||||||
|
│ RDEPUBTextBookBuilder(分页书籍构建器) │
|
||||||
|
│ RDEPUBTextBookCache / RDEPUBTextBookModels │
|
||||||
|
│ RDEPUBTextBuildPipelineInterfaces(管线协议) │
|
||||||
|
│ RDEPUBPaginationCacheCoordinator(缓存协调) │
|
||||||
|
│ RDEPUBChapterTailNormalizer(章尾规范化) │
|
||||||
|
│ RDEPUBBuildDiagnosticsReporter(诊断报告) │
|
||||||
|
│ RDEPUBTextPerformanceSampler(性能采样) │
|
||||||
|
│ │
|
||||||
|
│ Pagination/(分页引擎) │
|
||||||
│ RDEPUBTextLayouter / RDEPUBTextLayoutFrame │
|
│ RDEPUBTextLayouter / RDEPUBTextLayoutFrame │
|
||||||
│ RDEPUBTextBookCache / RDEPUBChapterData │
|
│ RDEPUBChapterPageCounter / RDEPUBCoreTextPageFrameFactory│
|
||||||
│ RDEPUBTextIndexTable / RDEPUBTextPerformanceSampler │
|
│ RDEPUBPageBreakPolicy(断页策略) │
|
||||||
│ RDEPUBTextSearchEngine │
|
│ RDEPUBTextPaginationInterfaces(分页协议) │
|
||||||
|
│ RDEPUBTextPaginationSupport(分页支持) │
|
||||||
|
│ │
|
||||||
|
│ Typesetter/(排版管线) │
|
||||||
|
│ RDEPUBTypesettingPipeline(排版管线编排) │
|
||||||
|
│ RDEPUBHTMLNormalizer(HTML 规范化) │
|
||||||
|
│ RDEPUBStyleSheetComposer(CSS 组合) │
|
||||||
|
│ RDEPUBFontNormalizer(字体规范化) │
|
||||||
|
│ RDEPUBAttachmentNormalizer(附件规范化) │
|
||||||
|
│ RDEPUBFragmentMarkerInjector(Fragment 标记注入) │
|
||||||
|
│ RDEPUBSemanticMarkerInjector(语义标记注入) │
|
||||||
|
│ RDEPUBRenderDiagnosticsCollector(渲染诊断) │
|
||||||
|
│ RDEPUBTextRendererSupport(渲染辅助工具) │
|
||||||
└──────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -92,14 +165,13 @@ RDReaderView 是一个 iOS 阅读器组件库(CocoaPods),提供开箱即
|
|||||||
|
|
||||||
## 3. 翻页容器层(RDReaderView)
|
## 3. 翻页容器层(RDReaderView)
|
||||||
|
|
||||||
### 3.1 四种翻页模式
|
### 3.1 三种翻页模式
|
||||||
|
|
||||||
| 模式 | 实现方式 | 特点 |
|
| 模式 | 实现方式 | 特点 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| `pageCurl` | UIPageViewController | 原生翻书效果,手势由系统提供 |
|
| `pageCurl` | UIPageViewController | 原生翻书效果,手势由系统提供 |
|
||||||
| `horizontalScroll` | UICollectionView + RDReaderFlowLayout | 每屏显示 2 项,水平分页滚动 |
|
| `horizontalScroll` | UICollectionView + RDReaderFlowLayout | 每屏显示 2 项,水平分页滚动 |
|
||||||
| `verticalScroll` | UICollectionView + RDReaderFlowLayout | 全宽项目,垂直连续滚动 |
|
| `verticalScroll` | UICollectionView + RDReaderFlowLayout | 全宽项目,垂直连续滚动 |
|
||||||
| `horizontalCoverScroll` | UICollectionView + RDReaderFlowLayout | 覆盖滚动效果,Z 轴动画 |
|
|
||||||
|
|
||||||
### 3.2 数据源协议
|
### 3.2 数据源协议
|
||||||
|
|
||||||
|
|||||||
@ -249,7 +249,7 @@ RDEPUBReaderSettings (Codable)
|
|||||||
└── themePreset: RDEPUBReaderThemePreset?
|
└── themePreset: RDEPUBReaderThemePreset?
|
||||||
```
|
```
|
||||||
|
|
||||||
`RDEPUBReaderDisplayMode`:可序列化的翻页模式枚举(pageCurl / horizontalScroll / verticalScroll / horizontalCoverScroll),与 `RDReaderView.DisplayType` 相互转换。
|
`RDEPUBReaderDisplayMode`:可序列化的翻页模式枚举(pageCurl / horizontalScroll / verticalScroll),与 `RDReaderView.DisplayType` 相互转换。注意:历史版本遗留的 `horizontalCoverScroll` 会自动映射为 `horizontalScroll`。
|
||||||
|
|
||||||
`RDEPUBReaderThemePreset`:可序列化的主题预设枚举(light / yellow / green / pink / blue / dark),与 `RDEPUBReaderTheme` 相互转换。
|
`RDEPUBReaderThemePreset`:可序列化的主题预设枚举(light / yellow / green / pink / blue / dark),与 `RDEPUBReaderTheme` 相互转换。
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
## 1. 范围与目标
|
## 1. 范围与目标
|
||||||
|
|
||||||
- 代码范围:`Sources/RDReaderView/ReaderView/`(5 个 Swift 文件)
|
- 代码范围:`Sources/RDReaderView/ReaderView/`(5 个 Swift 文件)
|
||||||
- 目标:说明分页阅读器容器如何管理四种显示模式、DataSource/Delegate 协议、翻页交互、工具栏动画、双屏适配和 RTL 支持。
|
- 目标:说明分页阅读器容器如何管理三种显示模式、DataSource/Delegate 协议、翻页交互、工具栏动画、双屏适配和 RTL 支持。
|
||||||
- 主链路关键词:`RDReaderDataSource -> reloadData -> DisplayType 切换 -> 翻页/滚动 -> RDReaderDelegate.pageNum -> 工具栏显隐`。
|
- 主链路关键词:`RDReaderDataSource -> reloadData -> DisplayType 切换 -> 翻页/滚动 -> RDReaderDelegate.pageNum -> 工具栏显隐`。
|
||||||
|
|
||||||
## 2. 关键对象职责
|
## 2. 关键对象职责
|
||||||
@ -13,7 +13,7 @@
|
|||||||
- 文件:`Sources/RDReaderView/ReaderView/RDReaderView.swift`(~1219 行)
|
- 文件:`Sources/RDReaderView/ReaderView/RDReaderView.swift`(~1219 行)
|
||||||
- 入口方法:`reloadData()`
|
- 入口方法:`reloadData()`
|
||||||
- 职责:
|
- 职责:
|
||||||
- 管理四种显示模式的视图层级切换
|
- 管理三种显示模式的视图层级切换
|
||||||
- 持有 `UIPageViewController`(pageCurl 模式)或 `UICollectionView`(滚动模式)
|
- 持有 `UIPageViewController`(pageCurl 模式)或 `UICollectionView`(滚动模式)
|
||||||
- 处理点击手势(左/中/右三区域)
|
- 处理点击手势(左/中/右三区域)
|
||||||
- 管理工具栏(topToolView / bottomToolView)的显示/隐藏动画
|
- 管理工具栏(topToolView / bottomToolView)的显示/隐藏动画
|
||||||
@ -25,10 +25,9 @@
|
|||||||
|
|
||||||
- 文件:`Sources/RDReaderView/ReaderView/RDReaderFlowLayout.swift`(~375 行)
|
- 文件:`Sources/RDReaderView/ReaderView/RDReaderFlowLayout.swift`(~375 行)
|
||||||
- 职责:
|
- 职责:
|
||||||
- 继承 `UICollectionViewFlowLayout`,为三种滚动模式提供布局计算
|
- 继承 `UICollectionViewFlowLayout`,为两种滚动模式提供布局计算
|
||||||
- 水平滚动:全屏宽 item,pagingEnabled
|
- 水平滚动:全屏宽 item,pagingEnabled
|
||||||
- 垂直滚动:可变高度 item,累加计算
|
- 垂直滚动:可变高度 item,累加计算
|
||||||
- 水平覆盖滚动:zIndex 分层 + 阴影效果模拟深度
|
|
||||||
- 封面感知帧计算:封面页全屏宽,后续页面两两配对半屏宽
|
- 封面感知帧计算:封面页全屏宽,后续页面两两配对半屏宽
|
||||||
|
|
||||||
### 2.3 内容 Cell `RDReaderContentCell`
|
### 2.3 内容 Cell `RDReaderContentCell`
|
||||||
@ -80,7 +79,7 @@ func pageNum(readerView: RDReaderView, pageNum: Int)
|
|||||||
3. `reloadData` 内部调用 `switchReaderDisplayType(currentDisplayType)` 重建视图层级。
|
3. `reloadData` 内部调用 `switchReaderDisplayType(currentDisplayType)` 重建视图层级。
|
||||||
4. 同时从 `dataSource` 获取 `topToolView` 和 `bottomToolView` 并添加到视图层级。
|
4. 同时从 `dataSource` 获取 `topToolView` 和 `bottomToolView` 并添加到视图层级。
|
||||||
|
|
||||||
### 3.3 四种显示模式切换
|
### 3.3 三种显示模式切换
|
||||||
|
|
||||||
**pageCurl 模式**:
|
**pageCurl 模式**:
|
||||||
- 创建 `UIPageViewController`(transitionStyle: .pageCurl)
|
- 创建 `UIPageViewController`(transitionStyle: .pageCurl)
|
||||||
@ -99,13 +98,6 @@ func pageNum(readerView: RDReaderView, pageNum: Int)
|
|||||||
- 页面高度可变,通过 `RDReaderFlowLayoutDataSoure.heigtOfVerticalScrollPage` 查询
|
- 页面高度可变,通过 `RDReaderFlowLayoutDataSoure.heigtOfVerticalScrollPage` 查询
|
||||||
- collectionViewContentSize 为所有页面高度之和
|
- collectionViewContentSize 为所有页面高度之和
|
||||||
|
|
||||||
**horizontalCoverScroll 模式**:
|
|
||||||
- 水平分页,但带封面滑动动画
|
|
||||||
- `layoutAttributesForElements` 中:
|
|
||||||
- 仅计算当前页附近的窄窗口内的 item attributes
|
|
||||||
- 当前页之前的 item zIndex = -1,当前及之后 zIndex = 1
|
|
||||||
- 顶层页面边缘添加阴影效果
|
|
||||||
|
|
||||||
### 3.4 翻页交互
|
### 3.4 翻页交互
|
||||||
|
|
||||||
**点击手势**(`tapAction(tap:)`):
|
**点击手势**(`tapAction(tap:)`):
|
||||||
@ -170,8 +162,7 @@ func pageNum(readerView: RDReaderView, pageNum: Int)
|
|||||||
RDReaderView.DisplayType
|
RDReaderView.DisplayType
|
||||||
├── .pageCurl // UIPageViewController 翻页效果
|
├── .pageCurl // UIPageViewController 翻页效果
|
||||||
├── .horizontalScroll // UICollectionView 水平滚动
|
├── .horizontalScroll // UICollectionView 水平滚动
|
||||||
├── .verticalScroll // UICollectionView 垂直滚动
|
└── .verticalScroll // UICollectionView 垂直滚动
|
||||||
└── .horizontalCoverScroll // UICollectionView 水平覆盖动画
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 翻页方向
|
### 5.2 翻页方向
|
||||||
|
|||||||
415
Doc/UI自动化测试工作文档.md
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
# ReadViewSDK UI 自动化测试可执行方案
|
||||||
|
|
||||||
|
## 1. 目标与边界
|
||||||
|
|
||||||
|
本文档描述当前架构下可落地的 XCUITest 方案。目标不是一次性覆盖所有 SDK API,而是先为 Demo App 的核心阅读链路建立稳定回归网:
|
||||||
|
|
||||||
|
- 书库页面能展示样本书。
|
||||||
|
- 样本书能打开阅读器。
|
||||||
|
- 点击阅读区域中部能显示顶部和底部工具栏。
|
||||||
|
- 顶部返回按钮能关闭阅读器并回到书库。
|
||||||
|
- 设置面板能打开、操作、关闭。
|
||||||
|
- 三种翻页模式能通过启动参数切换并保持阅读器可用。
|
||||||
|
|
||||||
|
XCUITest 运行在独立进程,不能直接调用 `RDEPUBReaderController` 的 Swift 公共 API。因此需要通过 UI 元素、launch arguments、Demo 测试状态标签或截图附件验证结果。SDK 公共 API 可以由单元测试或 Demo 自动化入口间接驱动,不应写成 XCUITest 的直接依赖。
|
||||||
|
|
||||||
|
## 2. 当前可用基础
|
||||||
|
|
||||||
|
### 2.1 已有 accessibilityIdentifier
|
||||||
|
|
||||||
|
| 标识符 | 当前文件 | 用途 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| `epub.reader.back` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift` | 顶部返回按钮 |
|
||||||
|
| `epub.reader.bookmark` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift` | 顶部书签按钮 |
|
||||||
|
| `epub.reader.title` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift` | 顶部标题 |
|
||||||
|
| `epub.reader.toc` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift` | 目录按钮 |
|
||||||
|
| `epub.reader.bookmarks` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift` | 书签列表按钮 |
|
||||||
|
| `epub.reader.highlights` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift` | 高亮列表按钮 |
|
||||||
|
| `epub.reader.add-highlight` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift` | 新建高亮按钮 |
|
||||||
|
| `epub.reader.settings` | `Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift` | 设置按钮 |
|
||||||
|
| `demo.root` | `ReadViewDemo/ReadViewDemo/ViewController.swift` | Demo 根视图 |
|
||||||
|
| `demo.status` | `ReadViewDemo/ReadViewDemo/ViewController.swift` | Demo 状态标签 |
|
||||||
|
| `demo.books.table` | `ReadViewDemo/ReadViewDemo/ViewController.swift` | 书库表格 |
|
||||||
|
| `demo.books.empty` | `ReadViewDemo/ReadViewDemo/ViewController.swift` | 空书库提示 |
|
||||||
|
| `demo.book.{n}` | `ReadViewDemo/ReadViewDemo/ViewController.swift` | 书库第 n 本书 |
|
||||||
|
| `demo.reader.host` | `ReadViewDemo/ReadViewDemo/ViewController.swift` | modal 场景下的阅读器宿主 |
|
||||||
|
|
||||||
|
### 2.2 已有 Demo 启动参数
|
||||||
|
|
||||||
|
`ReadViewDemo/ReadViewDemo/ViewController.swift` 已有 `LaunchAutomationPlan`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--demo-book-title <关键词>
|
||||||
|
--demo-display-type <pagecurl|scroll|vertical>
|
||||||
|
--demo-page <页码>
|
||||||
|
--demo-display-sequence <pagecurl,scroll,vertical>
|
||||||
|
--demo-step-delay <秒>
|
||||||
|
```
|
||||||
|
|
||||||
|
首批测试优先使用 `--demo-book-title 回归验证样本`。该样本目前存在于 `ReadViewDemo/ReadViewDemo/book/回归验证样本.txt`,适合作为稳定自动化入口。
|
||||||
|
|
||||||
|
## 3. 当前架构下的落点
|
||||||
|
|
||||||
|
不要把测试辅助职责重新塞回 `RDEPUBReaderController.swift`。identifier 和测试状态应按真实 UI/职责归属放置:
|
||||||
|
|
||||||
|
| 能力 | 落点 | 原因 |
|
||||||
|
|------|------|------|
|
||||||
|
| 顶部工具栏容器 | `RDEPUBReaderTopToolView.swift` | 工具栏自身创建和维护顶部按钮 |
|
||||||
|
| 底部工具栏容器 | `RDEPUBReaderBottomToolView.swift` | 工具栏自身创建和维护底部按钮 |
|
||||||
|
| 阅读点击区域 | `RDReaderView.swift` | 点击中区、翻页手势都由 ReaderView 处理 |
|
||||||
|
| 滚动翻页容器 | `RDReaderView.swift` 的 `collectionView` | 横滑/竖滑模式使用 collection view |
|
||||||
|
| 单页内容 cell | `RDReaderContentCell.swift` 或当前分页 cell 文件 | 验证 cell 存在,不验证文本排版细节 |
|
||||||
|
| 设置面板控件 | `EPUBUI/Settings/RDEPUBReaderSettingsViewController.swift` | 设置面板已从 EPUBUI 根目录迁移到 Settings |
|
||||||
|
| 目录列表 | `RDEPUBReaderChapterListController.swift` | 目录是独立 UITableViewController |
|
||||||
|
| 高亮/书签列表 | `RDEPUBReaderHighlightsViewController.swift` | 高亮和书签管理器在该文件中 |
|
||||||
|
| Demo 自动化状态 | `ViewController.swift` 或 `RDURLReaderController.swift` | XCUITest 通过文本/identifier 读取状态 |
|
||||||
|
|
||||||
|
## 4. 需要新增的最小标识符
|
||||||
|
|
||||||
|
### 4.1 P0 必需
|
||||||
|
|
||||||
|
| 标识符 | 建议文件 | 用途 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| `epub.reader.topToolbar` | `RDEPUBReaderTopToolView.swift` | 断言顶部工具栏显示/隐藏 |
|
||||||
|
| `epub.reader.bottomToolbar` | `RDEPUBReaderBottomToolView.swift` | 断言底部工具栏显示/隐藏 |
|
||||||
|
| `epub.reader.content` | `RDReaderView.swift` | 点击阅读区域中部、滑动翻页 |
|
||||||
|
| `epub.reader.paging` | `RDReaderView.swift` 的 `collectionView` | 横滑/竖滑容器存在性 |
|
||||||
|
| `epub.reader.settings.scroll` | `RDEPUBReaderSettingsViewController.swift` | 设置面板已打开 |
|
||||||
|
| `epub.reader.settings.brightness` | `RDEPUBReaderSettingsViewController.swift` | 亮度 slider |
|
||||||
|
| `epub.reader.settings.font.decrease` | `RDEPUBReaderSettingsViewController.swift` | 字号减 |
|
||||||
|
| `epub.reader.settings.font.increase` | `RDEPUBReaderSettingsViewController.swift` | 字号加 |
|
||||||
|
| `epub.reader.settings.font.value` | `RDEPUBReaderSettingsViewController.swift` | 字号值 |
|
||||||
|
| `epub.reader.settings.displayType` | `RDEPUBReaderSettingsViewController.swift` | 翻页模式分段控件 |
|
||||||
|
| `epub.reader.settings.done` | `RDEPUBReaderSettingsViewController.swift` | 完成按钮 |
|
||||||
|
| `demo.reader.state` | Demo 层 | 输出当前打开状态、页码、翻页模式 |
|
||||||
|
|
||||||
|
### 4.2 P1 后续补充
|
||||||
|
|
||||||
|
| 标识符 | 建议文件 | 用途 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| `epub.reader.toc.list` | `RDEPUBReaderChapterListController.swift` | 目录列表 |
|
||||||
|
| `epub.reader.toc.cell.{n}` | `RDEPUBReaderChapterListController.swift` | 目录项 |
|
||||||
|
| `epub.reader.bookmark.list` | `RDEPUBReaderHighlightsViewController.swift` 的书签控制器 | 书签列表 |
|
||||||
|
| `epub.reader.bookmark.cell.{n}` | `RDEPUBReaderHighlightsViewController.swift` 的书签控制器 | 书签项 |
|
||||||
|
| `epub.reader.highlight.list` | `RDEPUBReaderHighlightsViewController.swift` | 高亮列表 |
|
||||||
|
| `epub.reader.highlight.cell.{n}` | `RDEPUBReaderHighlightsViewController.swift` | 高亮项 |
|
||||||
|
| `epub.reader.settings.lineHeight` | `RDEPUBReaderSettingsViewController.swift` | 行距分段控件 |
|
||||||
|
| `epub.reader.settings.columns` | `RDEPUBReaderSettingsViewController.swift` | 栏数分段控件 |
|
||||||
|
| `epub.reader.settings.theme.{n}` | `RDEPUBReaderSettingsViewController.swift` | 主题按钮 |
|
||||||
|
|
||||||
|
`epub.reader.pageIndicator` 暂不列为必需项,因为当前没有稳定的页码指示器 UI。若需要断言页码,优先通过 `demo.reader.state` 暴露 `page=...`,或者给 `RDReaderView` 设置 `accessibilityValue`。
|
||||||
|
|
||||||
|
## 5. Demo 测试状态设计
|
||||||
|
|
||||||
|
建议增加一个仅用于自动化的状态标签:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
stateLabel.accessibilityIdentifier = "demo.reader.state"
|
||||||
|
stateLabel.isHidden = true
|
||||||
|
stateLabel.text = "reader=opened page=1 display=pagecurl toolbar=hidden"
|
||||||
|
```
|
||||||
|
|
||||||
|
状态来源可以在 Demo 层更新,不要求 SDK 为测试暴露内部对象:
|
||||||
|
|
||||||
|
- 打开阅读器后:`reader=opened`
|
||||||
|
- 返回书库后:`reader=closed`
|
||||||
|
- 跳页或翻页后:`page=<n>`
|
||||||
|
- 切换翻页模式后:`display=pagecurl|scroll|vertical`
|
||||||
|
- 工具栏显示后:`toolbar=visible`
|
||||||
|
|
||||||
|
这个标签能显著减少 XCUITest 对动画、布局和截图的猜测,是当前架构下最稳的可执行方案。
|
||||||
|
|
||||||
|
## 6. UI Test Target 创建方式
|
||||||
|
|
||||||
|
使用 workspace,不使用单独的 xcodeproj:
|
||||||
|
|
||||||
|
1. 打开 `ReadViewDemo/ReadViewDemo.xcworkspace`
|
||||||
|
2. File -> New -> Target -> iOS UI Testing Bundle
|
||||||
|
3. Product Name: `ReadViewDemoUITests`
|
||||||
|
4. Target to be Tested: `ReadViewDemo`
|
||||||
|
5. 新增测试目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ReadViewDemo/
|
||||||
|
ReadViewDemoUITests/
|
||||||
|
Helpers/
|
||||||
|
AccessibilityIdentifiers.swift
|
||||||
|
XCUIApplication+Launch.swift
|
||||||
|
XCUIElement+Wait.swift
|
||||||
|
ReaderUITests/
|
||||||
|
BookListTests.swift
|
||||||
|
ReaderOpenCloseTests.swift
|
||||||
|
ReaderToolbarTests.swift
|
||||||
|
SettingsPanelTests.swift
|
||||||
|
DisplayTypeTests.swift
|
||||||
|
SmokeScreenshotTests.swift
|
||||||
|
```
|
||||||
|
|
||||||
|
首批不要拆太多测试类,避免还没稳定就出现维护成本。
|
||||||
|
|
||||||
|
## 7. P0 测试用例
|
||||||
|
|
||||||
|
### 7.1 启动辅助
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
enum IDs {
|
||||||
|
static let demoStatus = "demo.status"
|
||||||
|
static let demoBooksTable = "demo.books.table"
|
||||||
|
static func demoBook(_ n: Int) -> String { "demo.book.\(n)" }
|
||||||
|
static let demoReaderState = "demo.reader.state"
|
||||||
|
|
||||||
|
static let readerBack = "epub.reader.back"
|
||||||
|
static let readerTitle = "epub.reader.title"
|
||||||
|
static let readerTopToolbar = "epub.reader.topToolbar"
|
||||||
|
static let readerBottomToolbar = "epub.reader.bottomToolbar"
|
||||||
|
static let readerContent = "epub.reader.content"
|
||||||
|
static let readerSettings = "epub.reader.settings"
|
||||||
|
|
||||||
|
static let settingsScroll = "epub.reader.settings.scroll"
|
||||||
|
static let settingsFontIncrease = "epub.reader.settings.font.increase"
|
||||||
|
static let settingsFontDecrease = "epub.reader.settings.font.decrease"
|
||||||
|
static let settingsFontValue = "epub.reader.settings.font.value"
|
||||||
|
static let settingsDone = "epub.reader.settings.done"
|
||||||
|
}
|
||||||
|
|
||||||
|
extension XCUIApplication {
|
||||||
|
func launchAndOpenSampleBook(
|
||||||
|
displayType: String? = nil,
|
||||||
|
pageNumber: Int? = nil
|
||||||
|
) {
|
||||||
|
var args = ["--demo-book-title", "回归验证样本"]
|
||||||
|
if let displayType {
|
||||||
|
args += ["--demo-display-type", displayType]
|
||||||
|
}
|
||||||
|
if let pageNumber {
|
||||||
|
args += ["--demo-page", "\(pageNumber)"]
|
||||||
|
}
|
||||||
|
launchArguments = args
|
||||||
|
launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func waitForReader(timeout: TimeInterval = 10) -> XCUIElement {
|
||||||
|
let backButton = buttons[IDs.readerBack]
|
||||||
|
XCTAssertTrue(backButton.waitForExistence(timeout: timeout))
|
||||||
|
return backButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 书库与打开关闭
|
||||||
|
|
||||||
|
```swift
|
||||||
|
final class ReaderOpenCloseTests: XCTestCase {
|
||||||
|
private let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBookListShowsSampleBooks() {
|
||||||
|
app.launch()
|
||||||
|
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5))
|
||||||
|
XCTAssertTrue(app.cells[IDs.demoBook(0)].waitForExistence(timeout: 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLaunchArgumentOpensReader() {
|
||||||
|
app.launchAndOpenSampleBook()
|
||||||
|
app.waitForReader()
|
||||||
|
XCTAssertTrue(app.staticTexts[IDs.readerTitle].exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBackButtonReturnsToBookList() {
|
||||||
|
app.launchAndOpenSampleBook()
|
||||||
|
app.waitForReader().tap()
|
||||||
|
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 工具栏显示
|
||||||
|
|
||||||
|
```swift
|
||||||
|
final class ReaderToolbarTests: XCTestCase {
|
||||||
|
private let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTapCenterShowsToolbars() {
|
||||||
|
app.launchAndOpenSampleBook()
|
||||||
|
app.waitForReader()
|
||||||
|
|
||||||
|
let content = app.otherElements[IDs.readerContent]
|
||||||
|
XCTAssertTrue(content.waitForExistence(timeout: 5))
|
||||||
|
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||||
|
|
||||||
|
XCTAssertTrue(app.otherElements[IDs.readerTopToolbar].waitForExistence(timeout: 3))
|
||||||
|
XCTAssertTrue(app.otherElements[IDs.readerBottomToolbar].waitForExistence(timeout: 3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 设置面板
|
||||||
|
|
||||||
|
```swift
|
||||||
|
final class SettingsPanelTests: XCTestCase {
|
||||||
|
private let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOpenChangeFontAndCloseSettings() {
|
||||||
|
app.launchAndOpenSampleBook()
|
||||||
|
app.waitForReader()
|
||||||
|
|
||||||
|
app.buttons[IDs.readerSettings].tap()
|
||||||
|
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
let fontValue = app.staticTexts[IDs.settingsFontValue]
|
||||||
|
XCTAssertTrue(fontValue.waitForExistence(timeout: 3))
|
||||||
|
let before = fontValue.label
|
||||||
|
|
||||||
|
app.buttons[IDs.settingsFontIncrease].tap()
|
||||||
|
XCTAssertNotEqual(before, fontValue.label)
|
||||||
|
|
||||||
|
app.buttons[IDs.settingsFontDecrease].tap()
|
||||||
|
app.buttons[IDs.settingsDone].tap()
|
||||||
|
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.5 翻页模式 Smoke Test
|
||||||
|
|
||||||
|
```swift
|
||||||
|
final class DisplayTypeTests: XCTestCase {
|
||||||
|
private let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOpenWithPageCurl() {
|
||||||
|
app.launchAndOpenSampleBook(displayType: "pagecurl")
|
||||||
|
app.waitForReader()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOpenWithHorizontalScroll() {
|
||||||
|
app.launchAndOpenSampleBook(displayType: "scroll")
|
||||||
|
app.waitForReader()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOpenWithVerticalScroll() {
|
||||||
|
app.launchAndOpenSampleBook(displayType: "vertical")
|
||||||
|
app.waitForReader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. P1 测试用例
|
||||||
|
|
||||||
|
P0 稳定后再加入:
|
||||||
|
|
||||||
|
- 目录打开、目录列表存在、点击第一项返回阅读器。
|
||||||
|
- 添加书签后按钮状态变化,打开书签列表存在对应 cell。
|
||||||
|
- 设置面板切换行距、栏数、主题后阅读器仍可用。
|
||||||
|
- 横滑/竖滑模式下拖动内容区域后 `demo.reader.state` 的 page 变化。
|
||||||
|
- 高亮列表打开和空态显示。
|
||||||
|
|
||||||
|
搜索和文本选择高亮不建议作为第一批 UI 自动化。它们依赖文本命中、长按选择、浮层菜单和异步渲染,flaky 风险更高。可以先用单元测试覆盖搜索引擎,用 P1/P2 再补 UI 冒烟。
|
||||||
|
|
||||||
|
## 9. CI 集成
|
||||||
|
|
||||||
|
### 9.1 手动命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild test \
|
||||||
|
-workspace ReadViewDemo/ReadViewDemo.xcworkspace \
|
||||||
|
-scheme ReadViewDemo \
|
||||||
|
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||||
|
-only-testing:ReadViewDemoUITests
|
||||||
|
```
|
||||||
|
|
||||||
|
如果本机没有 `iPhone 16`,先查看可用模拟器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun simctl list devices available
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: UI Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ui-tests:
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 40
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Pods
|
||||||
|
run: cd ReadViewDemo && pod install
|
||||||
|
|
||||||
|
- name: Run UI Tests
|
||||||
|
run: |
|
||||||
|
xcodebuild test \
|
||||||
|
-workspace ReadViewDemo/ReadViewDemo.xcworkspace \
|
||||||
|
-scheme ReadViewDemo \
|
||||||
|
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
|
||||||
|
-only-testing:ReadViewDemoUITests \
|
||||||
|
-resultBundlePath ui-test-results.xcresult
|
||||||
|
|
||||||
|
- name: Upload xcresult
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ui-test-results
|
||||||
|
path: ui-test-results.xcresult
|
||||||
|
```
|
||||||
|
|
||||||
|
CI 首批只跑 P0。P1 可以先本地或 nightly 跑,稳定后再进入 PR 必过。
|
||||||
|
|
||||||
|
## 10. 实施顺序
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 验收标准 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 1 | 补 P0 identifiers | XCUITest 能定位内容区、工具栏、设置控件 |
|
||||||
|
| 2 | 增加 `demo.reader.state` | 能通过 hidden label 读到 opened/page/display/toolbar |
|
||||||
|
| 3 | 创建 `ReadViewDemoUITests` | `xcodebuild test` 能发现测试 target |
|
||||||
|
| 4 | 实现 P0 用例 | 本地模拟器连续跑 3 次通过 |
|
||||||
|
| 5 | 接入 CI | PR 中 P0 UI Tests 可运行并上传 xcresult |
|
||||||
|
| 6 | 扩展 P1 | 目录、书签、主题、翻页断言逐步加入 |
|
||||||
|
|
||||||
|
## 11. 工时预估
|
||||||
|
|
||||||
|
| 工作 | 预估 |
|
||||||
|
|------|------|
|
||||||
|
| P0 identifiers + Demo 状态标签 | 2-3h |
|
||||||
|
| UI Test Target + helpers | 1-2h |
|
||||||
|
| P0 测试实现与稳定性调整 | 4-6h |
|
||||||
|
| CI 接入 | 1-2h |
|
||||||
|
| P1 首批扩展 | 4-8h |
|
||||||
|
|
||||||
|
首个可用版本建议按 1.5 到 2 天排期。后续每加入一组复杂交互,先本地观察稳定性,再进入 CI 必过集合。
|
||||||
|
|
||||||
|
## 12. 风险与约束
|
||||||
|
|
||||||
|
- UI 自动化不能依赖固定动画时间,优先使用 `waitForExistence` 和状态标签。
|
||||||
|
- 不要用截图像素对比作为 PR 必过项;截图附件适合作为人工排查材料。
|
||||||
|
- 样本书必须稳定存在,优先使用 `回归验证样本.txt`。
|
||||||
|
- 工具栏默认可能隐藏,测试应先点击阅读区域中部再断言底部按钮。
|
||||||
|
- 设置面板、目录面板是导航/弹出结构,测试应等待面板根控件,而不是立即点击内部控件。
|
||||||
|
- 若未来 Reader UI 迁移到真实 App,Demo-only 的 `demo.reader.state` 不应进入 SDK 公共 API。
|
||||||
@ -1,560 +0,0 @@
|
|||||||
# 读书 EPUB 阅读器实现架构分析
|
|
||||||
|
|
||||||
> 分析日期: 2026-05-18
|
|
||||||
> 基于读书 v10.0.3
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 核心结论:双渲染引擎架构
|
|
||||||
|
|
||||||
读书使用了 **两套渲染引擎**,根据内容类型选择不同的渲染路径:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ WRReaderViewController │
|
|
||||||
│ (阅读器主控制器, 管理翻页和状态) │
|
|
||||||
├─────────────────────────────────────────────────┤
|
|
||||||
│ WRPageViewController │
|
|
||||||
│ (基于 UIPageViewController) │
|
|
||||||
│ 支持 UIPageCurl(仿真翻页) + Scroll(滑动) │
|
|
||||||
├──────────────────────┬──────────────────────────┤
|
|
||||||
│ 路径A: 原生渲染 │ 路径B: WebView 渲染 │
|
|
||||||
│ (EPUB/书籍正文) │ (公众号/文集文章) │
|
|
||||||
│ │ │
|
|
||||||
│ WREpubTypesetter │ WKWebView + JS Bridge │
|
|
||||||
│ DTCoreText │ weread-highlighter.js │
|
|
||||||
│ CoreText 排版 │ rangy-*.js │
|
|
||||||
│ NSAttributedString │ MediaPlatform.js/css │
|
|
||||||
│ WRCoreTextLayouter │ Readability.js │
|
|
||||||
│ WRPageView (draw) │ WRMPPageView │
|
|
||||||
└──────────────────────┴──────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 路径A:EPUB 正文原生渲染 (核心路径)
|
|
||||||
|
|
||||||
这是 EPUB 阅读的**主要渲染方式**,完全用原生 CoreText 实现,不走 WebView。
|
|
||||||
|
|
||||||
### 1. EPUB 下载与解密流程
|
|
||||||
|
|
||||||
```
|
|
||||||
服务器 ZIP 包 (加密)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WRBookNetwork.loadTarForEpubBookId:chapter:isPreload:
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WRBookNetwork.handleUnzipWithBookId:zipPath:encryptKey:plainBookDirectory:
|
|
||||||
│ - 使用 encryptKey 解密
|
|
||||||
│ - 解压到 plainBookDirectory
|
|
||||||
│ - 处理解压错误 handleUnzipErrorWithPath:
|
|
||||||
▼
|
|
||||||
WRBookNetwork.processEncryptedBookFileAtPath:encryptKey:book:chapterUid:isFromReview:
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WREncryptedFileManager.decryptContentsOfFile:forBookId:isFileLost:
|
|
||||||
│ - keyForBookId: 获取每本书的密钥
|
|
||||||
│ - 解密 EPUB 章节文件
|
|
||||||
▼
|
|
||||||
WREncryptedFileManager.encryptFileForBookId:originalEncryptKey:atPath:toPath:
|
|
||||||
│ - 本地二次加密存储 (DRM 保护)
|
|
||||||
▼
|
|
||||||
本地缓存: epubImage 目录 + 解密后的 XHTML 文件
|
|
||||||
```
|
|
||||||
|
|
||||||
**密钥管理**:
|
|
||||||
- `WRPreloadBookManager.saveEncryptKey:forPath:bookId:` - 预加载章节密钥
|
|
||||||
- `WRPreloadBookManager.encryptKeyForPath:bookId:` - 读取密钥
|
|
||||||
- `WREncryptedFileManager.keyForBookId:` - 每本书独立密钥
|
|
||||||
|
|
||||||
### 2. EPUB 解析与排版流程
|
|
||||||
|
|
||||||
```
|
|
||||||
XHTML 章节文件
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WRBookNetwork.fileContentWithChapter:book:shouldRemoveHtmlTags:filterTranslateContent:
|
|
||||||
│ - 读取 XHTML 内容
|
|
||||||
│ - 可选去除 HTML 标签
|
|
||||||
│ - 过滤翻译内容
|
|
||||||
▼
|
|
||||||
WREpubTypesetter.attributeStringWithFilePath:priority:insertArticleToolAttachment:
|
|
||||||
insertBookChapterToolAttachment:insertRecommendView:book:chapter:
|
|
||||||
pageFlippingStyle:renderErrorReason:isStyleFileNotFound:options:
|
|
||||||
│
|
|
||||||
│ ┌─────────────────────────────────────────┐
|
|
||||||
│ │ DTHTMLAttributedStringBuilder │
|
|
||||||
│ │ (HTML -> NSAttributedString 转换器) │
|
|
||||||
│ │ │
|
|
||||||
│ │ 1. 解析 XHTML DOM 树 │
|
|
||||||
│ │ 2. 读取 EPUB 内嵌 CSS (replace.css) │
|
|
||||||
│ │ 3. 合并默认样式 (default.css) │
|
|
||||||
│ │ 4. 应用用户主题样式 (dark.css) │
|
|
||||||
│ │ 5. 转换为 NSAttributedString │
|
|
||||||
│ │ - 保留字体、颜色、行高、对齐等属性 │
|
|
||||||
│ │ - 处理图片 (NSTextAttachment) │
|
|
||||||
│ │ - 处理超链接 │
|
|
||||||
│ └─────────────────────────────────────────┘
|
|
||||||
▼
|
|
||||||
NSAttributedString (富文本)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WRCoreTextLayouter (CoreText 排版引擎)
|
|
||||||
│
|
|
||||||
│ ┌─────────────────────────────────────────┐
|
|
||||||
│ │ DTCoreText 框架 (自定义修改版) │
|
|
||||||
│ │ │
|
|
||||||
│ │ DTCoreTextLayouter │
|
|
||||||
│ │ └─ CTTypesetter │
|
|
||||||
│ │ └─ CTFramesetter │
|
|
||||||
│ │ └─ DTCoreTextLayoutFrame │
|
|
||||||
│ │ └─ CTFrame (每页) │
|
|
||||||
│ │ └─ DTCoreTextLayoutLine │
|
|
||||||
│ │ └─ CTLine (每行) │
|
|
||||||
│ │ └─ DTCoreTextGlyphRun │
|
|
||||||
│ │ └─ CTLine (字形) │
|
|
||||||
│ │ │
|
|
||||||
│ │ 特殊处理: │
|
|
||||||
│ │ - wr-vertical-center-style (图片居中) │
|
|
||||||
│ │ - weread-page-relate (分页控制) │
|
|
||||||
│ │ - avoidPageBreakInside (避免断页) │
|
|
||||||
│ │ - 繁简转换 (convertHansToHant) │
|
|
||||||
│ └─────────────────────────────────────────┘
|
|
||||||
▼
|
|
||||||
WRChapterData (章节数据模型)
|
|
||||||
│
|
|
||||||
│ - 包含排版后的 NSAttributedString
|
|
||||||
│ - 管理划线/高亮/书评等标注
|
|
||||||
│ - addHighlightInRange:key:itemId:color:
|
|
||||||
│ - addUnderLineToAttributedString:range:itemId:style:color:
|
|
||||||
│ - addReviewUnderlineInRange:itemId:type:
|
|
||||||
│ - generateOutlineContents (生成目录)
|
|
||||||
│ - freeTrialChapterCutOffStringLocaion (免费试读截断)
|
|
||||||
▼
|
|
||||||
分页计算: WRChapterPageCount
|
|
||||||
│
|
|
||||||
│ - rangeValueWithPageInfo: 计算每页的 NSRange
|
|
||||||
│ - rangeOfPage: 获取指定页的文本范围
|
|
||||||
▼
|
|
||||||
WRPageView (页面视图, UIView 子类)
|
|
||||||
│
|
|
||||||
│ - 继承 UIView
|
|
||||||
│ - drawRect: 中调用 CoreText 绘制
|
|
||||||
│ - WRCoreTextLayoutFrame.drawInContext:image:size:inRect:position:
|
|
||||||
│ - 直接用 CGContext 绘制文字和图片
|
|
||||||
│ - 不使用 UILabel/UITextView
|
|
||||||
▼
|
|
||||||
屏幕显示 (像素级精确渲染)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 翻页机制
|
|
||||||
|
|
||||||
```
|
|
||||||
WRPageViewController
|
|
||||||
│
|
|
||||||
│ 基于 UIPageViewController 封装
|
|
||||||
│
|
|
||||||
│ 初始化: initWithDelegate:withPageType:pageFlippingStyle:
|
|
||||||
│
|
|
||||||
│ pageFlippingStyle 支持:
|
|
||||||
│ ┌────────────────────────────────────┐
|
|
||||||
│ │ UIPageCurl - 仿真翻页 (纸张卷曲) │
|
|
||||||
│ │ Scroll - 左右滑动翻页 │
|
|
||||||
│ └────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
│ 核心方法:
|
|
||||||
│ - pageViewController:viewControllerBeforeViewController: (上一页)
|
|
||||||
│ - pageViewController:viewControllerAfterViewController: (下一页)
|
|
||||||
│ - pageViewController:spineLocationForInterfaceOrientation: (书脊位置)
|
|
||||||
│ - weread_setViewControllers:withCurlOfType:fromLocation:direction:
|
|
||||||
│ animated:notifyDelegate:completion: (自定义设置方法)
|
|
||||||
│
|
|
||||||
│ 故障修复:
|
|
||||||
│ - patchNavigationDirectionFault (导航方向修复)
|
|
||||||
│ - patchNoViewControllerManagingPageViewFault (页面管理修复)
|
|
||||||
│ - patchUIPageCurlFault (翻页动画修复)
|
|
||||||
│ - detectNavigationDirectionCrashWithPageViewController: (崩溃检测)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WRPageView (每个页面的渲染视图)
|
|
||||||
│
|
|
||||||
│ - 通过 WRChapterData 获取排版结果
|
|
||||||
│ - 通过 rangeOfPage: 获取当前页的文本范围
|
|
||||||
│ - 使用 CoreText 直接绘制到 CGContext
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 文本选择与标注
|
|
||||||
|
|
||||||
```
|
|
||||||
用户触摸/长按
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WRPageView 手势识别
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
文本位置计算 (CoreText hit test)
|
|
||||||
│ - CTLineGetStringIndexForPosition (坐标->字符索引)
|
|
||||||
│ - CTLineGetOffsetForStringIndex (字符索引->坐标)
|
|
||||||
▼
|
|
||||||
选择范围确定
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
弹出操作菜单 (UIMenuController)
|
|
||||||
│ - 划线/高亮
|
|
||||||
│ - 写想法/书评
|
|
||||||
│ - 复制
|
|
||||||
│ - 查询/翻译
|
|
||||||
│ - 分享
|
|
||||||
▼
|
|
||||||
WRChapterData 添加标注
|
|
||||||
│ - addHighlightInRange:key:itemId:color:
|
|
||||||
│ - addUnderLineToAttributedString:range:itemId:style:color:
|
|
||||||
│ - addReviewUnderlineInRange:itemId:type:
|
|
||||||
▼
|
|
||||||
保存到服务器
|
|
||||||
│ - WRBookNetwork.addReview:shareToWechat:...
|
|
||||||
│ - 同步书签: loadBookmarkListWithBookId:syncKey:callback:
|
|
||||||
▼
|
|
||||||
重新排版当前页 (recomposeCurrentPageViewWithSource:)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 路径B:WebView 渲染 (公众号/文集文章)
|
|
||||||
|
|
||||||
用于渲染**微信公众号文章、文集、书评**等富媒体内容。
|
|
||||||
|
|
||||||
```
|
|
||||||
HTML 内容 (来自服务器)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WRMPReadingManager.composeMPReviewHTMLString:withReview:
|
|
||||||
│ - 组装 HTML 模板
|
|
||||||
│ - 注入 CSS (MediaPlatform.css, MPExtra.css)
|
|
||||||
│ - 注入 JS (MediaPlatform.js, mpForArticle.js)
|
|
||||||
▼
|
|
||||||
WKWebView 加载
|
|
||||||
│
|
|
||||||
│ 注入脚本:
|
|
||||||
│ <script src="WeReadApi.js"></script>
|
|
||||||
│ <script src="rich_display.js"></script>
|
|
||||||
▼
|
|
||||||
weread-highlighter.js 初始化
|
|
||||||
│
|
|
||||||
│ 1. rangy.init() - 初始化 Rangy 选择库
|
|
||||||
│ 2. 创建 Highlighter (TextRange 模式)
|
|
||||||
│ 3. 注册 ClassApplier:
|
|
||||||
│ - "highlight" (高亮)
|
|
||||||
│ - "review" / "friend-review" (书评)
|
|
||||||
│ - "reference" (引用)
|
|
||||||
│ - "tts" (语音朗读标记)
|
|
||||||
│ 4. 监听 selectionchange 事件
|
|
||||||
│ 5. 通过 wereadBridge.execMPReaderMethod 通知原生
|
|
||||||
▼
|
|
||||||
JS Bridge 双向通信
|
|
||||||
│
|
|
||||||
│ 原生 -> JS:
|
|
||||||
│ - evaluateJavaScript: 调用 JS 方法
|
|
||||||
│ - WKUserScript 注入脚本
|
|
||||||
│
|
|
||||||
│ JS -> 原生:
|
|
||||||
│ - window.webkit.messageHandlers.XXX.postMessage()
|
|
||||||
│ - wereadBridge.execMPReaderMethod('MPReader', data)
|
|
||||||
▼
|
|
||||||
WRMPReadingViewModel (ViewModel 层)
|
|
||||||
│
|
|
||||||
│ - addHighlightWithStart:withEnd:withContent:callback:
|
|
||||||
│ - addReviewWithRange:content:reference:secretMode:withCallback:
|
|
||||||
│ - genJSInfosWithHighlights:refrencedHighlight:
|
|
||||||
│ - genJSInfosWithReviews:refrencedReview:
|
|
||||||
│ - readReviewsWithLoadCount:maxObj:
|
|
||||||
│ - setupTTSAudioList
|
|
||||||
▼
|
|
||||||
WRMPPageView (WebView 包装视图)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 关键源码路径 (从二进制中提取)
|
|
||||||
|
|
||||||
```
|
|
||||||
WeRead/Src/Modules/EpubParser/
|
|
||||||
├── WREpubTypesetter.m # EPUB 排版器
|
|
||||||
├── WREpubPositionConverter.m # 位置转换器
|
|
||||||
└── Utils/DTCoreTextFunctions.m # CoreText 工具函数
|
|
||||||
|
|
||||||
WeRead/Src/Modules/TypeSetter/
|
|
||||||
├── WRCoreTextLayouter.m # CoreText 排版器
|
|
||||||
├── WRCoreTextLayoutFrame.m # 排版帧 (管理页面)
|
|
||||||
└── DTCoreTextGlyphRun.m # 字形渲染
|
|
||||||
|
|
||||||
WeRead/Src/Modules/Reading/
|
|
||||||
├── Controller/
|
|
||||||
│ ├── WRReaderViewController.m # 阅读器主控制器
|
|
||||||
│ └── WRPageViewController.m # 翻页控制器
|
|
||||||
├── Model/
|
|
||||||
│ ├── WRChapterData.m # 章节数据模型
|
|
||||||
│ ├── WRChapterDownloadManger.m # 章节下载管理
|
|
||||||
│ ├── WRReaderViewModel.m # 阅读器 ViewModel
|
|
||||||
│ └── WRMPReadingManager.m # 公众号阅读管理
|
|
||||||
└── View/
|
|
||||||
└── WRPageView.m # 页面渲染视图
|
|
||||||
|
|
||||||
WeRead/Src/Modules/MediaPlatform/
|
|
||||||
├── Controller/
|
|
||||||
│ ├── WRMPListViewController.m
|
|
||||||
│ └── WRMPSubscribeViewController.m
|
|
||||||
└── Model/
|
|
||||||
├── WRMPCoverManager.m
|
|
||||||
├── WRMPCoverPainter.m
|
|
||||||
├── WRMPStore.m
|
|
||||||
└── WRMPViewModel.m
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EPUB CSS 样式系统
|
|
||||||
|
|
||||||
```
|
|
||||||
加载优先级 (从低到高):
|
|
||||||
1. default.css - 基础 HTML 标签样式 (Safari 默认)
|
|
||||||
2. replace.css - 读书默认替换样式
|
|
||||||
├── 标题样式 (h1-h6, 使用 Source Han Serif CN 字体)
|
|
||||||
├── 代码块 (pre, 使用 Menlo 字体)
|
|
||||||
├── 图片 (.bodyPic, wr-vertical-center-style: 2)
|
|
||||||
├── 引用 (.conQuot)
|
|
||||||
├── 翻译 (.wr-translation)
|
|
||||||
├── 章节工具 (.book-chapter-tool, .chapter-tool)
|
|
||||||
└── 分页控制 (.weread-page-relate)
|
|
||||||
3. dark.css - 暗黑主题样式
|
|
||||||
4. EPUB 内嵌 CSS - 书籍自带样式
|
|
||||||
5. 用户设置 - 字号、行高、主题覆盖
|
|
||||||
```
|
|
||||||
|
|
||||||
**自定义 CSS 属性** (读书私有):
|
|
||||||
- `wr-vertical-center-style: 1|2` - 图片垂直居中方式
|
|
||||||
- `weread-page-relate: true` - 控制分页时的内容关联
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 核心类职责表
|
|
||||||
|
|
||||||
| 类名 | 职责 | 渲染路径 |
|
|
||||||
|---|---|---|
|
|
||||||
| `WRReaderViewController` | 阅读器主控制器,管理阅读状态、进度保存、章节跳转 | 共用 |
|
|
||||||
| `WRPageViewController` | 翻页控制器,基于 UIPageViewController 封装 | 共用 |
|
|
||||||
| `WRPageView` | 页面渲染视图,CoreText 直接绘制 | 路径A |
|
|
||||||
| `WRMPPageView` | 公众号页面视图,WKWebView 包装 | 路径B |
|
|
||||||
| `WREpubTypesetter` | EPUB 排版器,HTML->NSAttributedString | 路径A |
|
|
||||||
| `WRCoreTextLayouter` | CoreText 排版引擎,管理 CTFrame/CTLine | 路径A |
|
|
||||||
| `WRCoreTextLayoutFrame` | 排版帧,管理单页的绘制 | 路径A |
|
|
||||||
| `WRChapterData` | 章节数据模型,存储排版结果和标注 | 路径A |
|
|
||||||
| `WRChapterPageCount` | 分页计算,管理每页的 NSRange | 路径A |
|
|
||||||
| `WREpubPositionConverter` | EPUB 位置转换器 (文件位置<->字符位置) | 路径A |
|
|
||||||
| `WRChapterDownloadManger` | 章节下载管理器 | 共用 |
|
|
||||||
| `WREncryptedFileManager` | 加密文件管理 (DRM) | 共用 |
|
|
||||||
| `WRBookNetwork` | 书籍网络请求 (下载/解密/解压) | 共用 |
|
|
||||||
| `WRMPReadingManager` | 公众号阅读管理器 | 路径B |
|
|
||||||
| `WRMPReadingViewModel` | 公众号阅读 ViewModel (JS Bridge 交互) | 路径B |
|
|
||||||
| `WRReaderViewModel` | 阅读器 ViewModel | 共用 |
|
|
||||||
| `DTCoreTextLayouter` | DTCoreText 排版器 (第三方库修改版) | 路径A |
|
|
||||||
| `DTHTMLAttributedStringBuilder` | HTML->NSAttributedString 构建器 | 路径A |
|
|
||||||
| `WRReaderPencilNoteManager` | Apple Pencil 手写笔记管理 | 共用 |
|
|
||||||
| `WRReaderTranslationManager` | 翻译管理 (繁简转换/中英翻译) | 共用 |
|
|
||||||
| `WRReaderCht2sManager` | 繁体转简体管理 | 共用 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## JavaScript 文件职责
|
|
||||||
|
|
||||||
| 文件 | 职责 |
|
|
||||||
|---|---|
|
|
||||||
| `weread-highlighter.js` | 核心高亮引擎,初始化 Rangy,管理选择和高亮 |
|
|
||||||
| `rangy-core.js` | Rangy 核心库,跨浏览器 Range/Selection 封装 |
|
|
||||||
| `rangy-highlighter.js` | Rangy 高亮模块,管理高亮的创建/删除/序列化 |
|
|
||||||
| `rangy-classapplier.js` | Rangy ClassApplier 模块,CSS 类应用器 |
|
|
||||||
| `rangy-textrange.js` | Rangy TextRange 模块,文本范围操作 |
|
|
||||||
| `Readability.js` | Arc90 Readability 库,提取文章正文 |
|
|
||||||
| `MediaPlatform.js` | 公众号平台 JS,原生-JS 桥接 |
|
|
||||||
| `mpForArticle.js` | 文章相关 JS 逻辑 |
|
|
||||||
| `MPExtra.css` | 公众号额外样式 |
|
|
||||||
| `MediaPlatform.css` | 公众号基础样式 |
|
|
||||||
| `WeReadApi.js` | 读书 JS API (供 WebView 调用原生功能) |
|
|
||||||
| `rich_display.js` | 富文本显示逻辑 |
|
|
||||||
| `cssInjector.js` | CSS 注入器 |
|
|
||||||
| `highlight.min.js` | 代码语法高亮 (highlight.js) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DRM 与安全机制
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ DRM 保护链 │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 1. 传输层: HTTPS + 加密 ZIP │
|
|
||||||
│ - 服务器下发加密的 .zip 文件 │
|
|
||||||
│ - 文件名格式: {bookId}_DECRYPT.zip │
|
|
||||||
│ │
|
|
||||||
│ 2. 解密层: 逐章解密 │
|
|
||||||
│ - WREncryptedFileManager │
|
|
||||||
│ - keyForBookId: (每本书独立密钥) │
|
|
||||||
│ - decryptContentsOfFile:forBookId: │
|
|
||||||
│ │
|
|
||||||
│ 3. 存储层: 本地二次加密 │
|
|
||||||
│ - encryptFileForBookId:atPath:toPath: │
|
|
||||||
│ - 解密后立即重新加密存储 │
|
|
||||||
│ - 防止直接拷贝文件读取 │
|
|
||||||
│ │
|
|
||||||
│ 4. 密钥管理: │
|
|
||||||
│ - WRPreloadBookManager 管理预加载密钥 │
|
|
||||||
│ - 密钥与设备绑定 │
|
|
||||||
│ - 通过 Keychain 安全存储 │
|
|
||||||
│ │
|
|
||||||
│ 5. 免费试读控制: │
|
|
||||||
│ - freeTrialChapterCutOffStringLocaion │
|
|
||||||
│ - 服务端控制试读范围 │
|
|
||||||
│ - 客户端截断显示 │
|
|
||||||
│ │
|
|
||||||
│ 6. 章节付费: │
|
|
||||||
│ - isChapterAvailableForBookId:chapterUid │
|
|
||||||
│ - getCouponBuyChapterWithBookId: │
|
|
||||||
│ - resetChapterPaidIfNeeded │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 字体系统
|
|
||||||
|
|
||||||
```
|
|
||||||
内嵌字体:
|
|
||||||
├── SourceHanSerifCN-Medium.ttf # 思源宋体 (正文默认)
|
|
||||||
├── FZJuZhenXinFang.ttf # 方正聚珍新仿
|
|
||||||
├── Lora-Regular.ttf / Italic.ttf # Lora 衬线体
|
|
||||||
├── PlayfairDisplay-Regular.ttf # Playfair Display
|
|
||||||
├── WeReadLS-Regular/Medium/Bold # 读书 LS 系列
|
|
||||||
├── WeReadRN-Regular.ttf # RN 专用字体
|
|
||||||
├── WeRead-Icon.ttf # 图标字体
|
|
||||||
├── WeRead-Rating-Icon.ttf # 评分图标
|
|
||||||
├── OpenDyslexic-Regular.otf # 阅读障碍友好字体
|
|
||||||
├── WeChatNumber.ttf # 微信数字字体
|
|
||||||
├── SharpGroteskTRIAL*.ttf # Sharp Grotesk 系列
|
|
||||||
└── icon_font.ttf # 通用图标字体
|
|
||||||
|
|
||||||
动态字体:
|
|
||||||
├── CDN 下载: cdn.weread.qq.com/app/assets/font/
|
|
||||||
│ ├── FZLTHProGBK_B/DB/SB.zip # 方正兰亭黑系列
|
|
||||||
│ ├── FZQingKBYSJF-M.zip # 方正清刻本悦宋
|
|
||||||
│ ├── FZSKBXKK.zip # 方正书宋
|
|
||||||
│ ├── FZYBKSK.zip # 方正中楷
|
|
||||||
│ └── SourceHanSansCN-Heavy.zip # 思源黑体
|
|
||||||
│
|
|
||||||
└── WRFontsManager 管理:
|
|
||||||
- unZipFileAndRegisterFont:completionHandler:filePath:lateOverWrite:
|
|
||||||
- 动态下载 + 解压 + 注册
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 预加载与缓存策略
|
|
||||||
|
|
||||||
```
|
|
||||||
预加载策略:
|
|
||||||
├── WRForecastUtils.shouldPreloadChapterUidsForBook:type:bookRank:archiveRank:
|
|
||||||
│ - 根据书籍排名和用户行为预测需要预加载的章节
|
|
||||||
│
|
|
||||||
├── WRChapterDownloadManger._preloadChapterContentWithBook:type:bookRank:
|
|
||||||
│ - 后台预下载相邻章节
|
|
||||||
│
|
|
||||||
├── WRPreloadBookManager
|
|
||||||
│ - saveEncryptKey:forPath:bookId: (缓存密钥)
|
|
||||||
│ - saveFileNameDict:bookId: (缓存文件名映射)
|
|
||||||
│ - clearKV (清理缓存)
|
|
||||||
│
|
|
||||||
└── SDWebImage 缓存:
|
|
||||||
- epubImage 目录缓存书籍图片
|
|
||||||
- com.hackemist.SDWebImageCache.epubImage
|
|
||||||
|
|
||||||
缓存目录结构:
|
|
||||||
├── Documents/
|
|
||||||
│ └── {bookId}/
|
|
||||||
│ ├── plainBookDirectory/ (解密后的 EPUB 文件)
|
|
||||||
│ ├── epubImage/ (书籍图片缓存)
|
|
||||||
│ └── _DECRYPT.zip (下载的加密 ZIP)
|
|
||||||
└── Library/
|
|
||||||
└── {cachePath}/
|
|
||||||
├── epubImage/ (SDWebImage 缓存)
|
|
||||||
└── com.hackemist.SDWebImageCache.epubImage
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 朗读 (TTS) 集成
|
|
||||||
|
|
||||||
```
|
|
||||||
朗读流程:
|
|
||||||
├── WRReadAloudAudio.parseCGIInfos: (解析音频信息)
|
|
||||||
├── TTS 引擎: wxtts (微信语音合成)
|
|
||||||
│ ├── 离线资源: wxtts_offline_resource5.zip
|
|
||||||
│ ├── 在线资源: getWxttsSeginfo / getWxttsVoice
|
|
||||||
│ └── VITS/VALL-E 模型 (高保真语音)
|
|
||||||
│
|
|
||||||
├── 文本分段:
|
|
||||||
│ - weread-highlighter.js 中的 "tts" ClassApplier
|
|
||||||
│ - 标记当前朗读位置
|
|
||||||
│
|
|
||||||
└── 进度同步:
|
|
||||||
- lastListenedChapterOffset / lastListenedChapterUid
|
|
||||||
- MPReading/lastListenedReviewId (公众号朗读进度)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Apple Pencil 手写笔记
|
|
||||||
|
|
||||||
```
|
|
||||||
WRReaderPencilNoteManager:
|
|
||||||
├── 数据存储:
|
|
||||||
│ - writeDrawingDataToLocal:reviewItemId:reviewId:isDraft:
|
|
||||||
│ - drawingFilePathWithReviewItemId:reviewId:isDraft:
|
|
||||||
│ - 本地草稿 + 云端同步
|
|
||||||
│
|
|
||||||
├── 数据上传:
|
|
||||||
│ - uploadPencilDrawing:colorStyle:onlyUploadImage:canRetry:
|
|
||||||
│ - uploadPencilNoteData:suffix:
|
|
||||||
│ - 使用腾讯云 COS 存储
|
|
||||||
│ - authCosForPencilDataWithSuffix: (COS 认证)
|
|
||||||
│
|
|
||||||
├── 数据下载:
|
|
||||||
│ - downloadDrawingDataFromCosWithUrl:desPath:callback:
|
|
||||||
│ - downloadDrawingWithReviewItemId:reviewId:drawingUrl:dataBlock:
|
|
||||||
│
|
|
||||||
└── 图片导出:
|
|
||||||
- imageFilePathWithReviewItemId:
|
|
||||||
- 手绘笔记可导出为图片
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
| 特性 | 实现方式 |
|
|
||||||
|---|---|
|
|
||||||
| **EPUB 解析** | 自研 EPUB Parser,解析 OPF/NCX/XHTML |
|
|
||||||
| **文字排版** | DTCoreText (CoreText 封装) + 自定义 WRCoreTextLayouter |
|
|
||||||
| **页面渲染** | CGContext 直接绘制,不用 UILabel/UITextView |
|
|
||||||
| **翻页效果** | UIPageViewController (UIPageCurl + Scroll) |
|
|
||||||
| **文本选择** | CoreText hit test + Rangy.js (WebView 场景) |
|
|
||||||
| **标注系统** | NSAttributedString 属性注入 + 服务器同步 |
|
|
||||||
| **图片处理** | NSTextAttachment + CDN 尺寸优化 + 白底透明化 |
|
|
||||||
| **DRM** | 逐章加密 + 逐书密钥 + 本地二次加密 |
|
|
||||||
| **公众号文章** | WKWebView + JS Bridge + Rangy 高亮 |
|
|
||||||
| **繁简转换** | CoreText 层面的 convertHansToHant |
|
|
||||||
| **字体** | 内嵌 Source Han Serif CN + 动态下载字体 |
|
|
||||||
| **预加载** | 后台预下载相邻章节 ZIP + 解密缓存 |
|
|
||||||
| **TTS 朗读** | wxtts 引擎 + VITS/VALL-E 高保真模型 |
|
|
||||||
| **手写笔记** | PencilKit + 腾讯云 COS 存储 |
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
# WXRead 剩余问题修复计划
|
|
||||||
|
|
||||||
> 本文档内容已整合进入 [架构对比分析_WXRead_vs_ReadViewSDK.md](/Users/shen/Work/Code/ReadViewSDK/Doc/架构对比分析_WXRead_vs_ReadViewSDK.md)。
|
|
||||||
> 后续请只维护对比文档中的以下部分:
|
|
||||||
>
|
|
||||||
> - `核查摘要`
|
|
||||||
> - `缺失能力清单`
|
|
||||||
> - `收口路线(整合原修复计划)`
|
|
||||||
|
|
||||||
保留本文件仅为兼容旧引用,避免计划与现状继续出现双份口径。
|
|
||||||
12
Doc/index.md
@ -14,15 +14,23 @@
|
|||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| [EPUBCore_功能实现逻辑.md](EPUBCore_功能实现逻辑.md) | `Sources/RDReaderView/EPUBCore/`(31 Swift + 2 资源) | EPUB 解析全流程(ZIP→container.xml→OPF→spine/TOC)、阅读会话状态机、离屏分页测量、`ss-reader://` 资源协议、JS 桥接(6 种消息)、WebView 渲染管线、文本锚点定位、渲染请求模型、缓存策略 |
|
| [EPUBCore_功能实现逻辑.md](EPUBCore_功能实现逻辑.md) | `Sources/RDReaderView/EPUBCore/`(31 Swift + 2 资源) | EPUB 解析全流程(ZIP→container.xml→OPF→spine/TOC)、阅读会话状态机、离屏分页测量、`ss-reader://` 资源协议、JS 桥接(6 种消息)、WebView 渲染管线、文本锚点定位、渲染请求模型、缓存策略 |
|
||||||
| [EPUBTextRendering_功能实现逻辑.md](EPUBTextRendering_功能实现逻辑.md) | `Sources/RDReaderView/EPUBTextRendering/`(13 文件) | DTCoreText HTML→NSAttributedString 渲染管线、片段标记注入/提取、CoreText 分页引擎(含语义边界调整)、文本索引表、Location↔PageNumber 双向转换、分页缓存、性能采样、全文搜索引擎、纯文本构建器 |
|
| [EPUBTextRendering_功能实现逻辑.md](EPUBTextRendering_功能实现逻辑.md) | `Sources/RDReaderView/EPUBTextRendering/`(13 文件) | DTCoreText HTML→NSAttributedString 渲染管线、片段标记注入/提取、CoreText 分页引擎(含语义边界调整)、文本索引表、Location↔PageNumber 双向转换、分页缓存、性能采样、全文搜索引擎、纯文本构建器 |
|
||||||
| [RDReaderView_功能实现逻辑.md](RDReaderView_功能实现逻辑.md) | `Sources/RDReaderView/ReaderView/`(5 文件) | 四种显示模式(pageCurl/horizontalScroll/verticalScroll/horizontalCoverScroll)、DataSource/Delegate 协议、点击三区域翻页、工具栏动画、双页配对与哨兵页、横竖屏适配、RTL 支持 |
|
| [RDReaderView_功能实现逻辑.md](RDReaderView_功能实现逻辑.md) | `Sources/RDReaderView/ReaderView/`(5 文件) | 三种显示模式(pageCurl/horizontalScroll/verticalScroll)、DataSource/Delegate 协议、点击三区域翻页、工具栏动画、双页配对与哨兵页、横竖屏适配、RTL 支持 |
|
||||||
| [EPUBUI_功能实现逻辑.md](EPUBUI_功能实现逻辑.md) | `Sources/RDReaderView/EPUBUI/`(19 文件) | RDEPUBReaderController 全生命周期、三条渲染路径分发、配置变更检测与响应、工具栏/目录/高亮/书签/搜索/设置面板交互流、阅读位置持久化、主题管理、CoreText 页面交互 |
|
| [EPUBUI_功能实现逻辑.md](EPUBUI_功能实现逻辑.md) | `Sources/RDReaderView/EPUBUI/`(19 文件) | RDEPUBReaderController 全生命周期、三条渲染路径分发、配置变更检测与响应、工具栏/目录/高亮/书签/搜索/设置面板交互流、阅读位置持久化、主题管理、CoreText 页面交互 |
|
||||||
|
|
||||||
## 方案讨论文档
|
## 方案讨论与规划文档
|
||||||
|
|
||||||
| 文档 | 说明 |
|
| 文档 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| [阅读器功能开发计划.md](阅读器功能开发计划.md) | 渲染质量三方对比(ReadViewSDK vs WXRead)、功能开发计划与落地状态、优先级路线图 |
|
||||||
|
| [架构对比分析_WXRead_vs_ReadViewSDK.md](架构对比分析_WXRead_vs_ReadViewSDK.md) | ReadViewSDK 与 WXRead 的逐项架构核查,确认主链路复刻完成度 |
|
||||||
| [ReflowableEPUB_WXReadRenderer_Design.md](FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md) | 基于读书的 CoreText 渲染架构,设计 Reflowable EPUB 的增强文本渲染方案(CSS 分层、类型器升级) |
|
| [ReflowableEPUB_WXReadRenderer_Design.md](FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md) | 基于读书的 CoreText 渲染架构,设计 Reflowable EPUB 的增强文本渲染方案(CSS 分层、类型器升级) |
|
||||||
|
|
||||||
|
## 测试与质量
|
||||||
|
|
||||||
|
| 文档 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| [UI自动化测试工作文档.md](UI自动化测试工作文档.md) | XCUITest UI 自动化测试完整工作计划:现状分析、accessibilityIdentifier 补充清单、UI Test Target 创建、测试用例设计(书库/阅读器/工具栏/设置/书签/高亮/目录/截图)、CI 集成方案、实施检查清单 |
|
||||||
|
|
||||||
## 目录约定
|
## 目录约定
|
||||||
|
|
||||||
- `Doc/`:项目级文档(架构、规范、维护指南、模块实现逻辑)
|
- `Doc/`:项目级文档(架构、规范、维护指南、模块实现逻辑)
|
||||||
|
|||||||
617
Doc/阅读器功能开发计划.md
Normal file
@ -0,0 +1,617 @@
|
|||||||
|
# 阅读器功能开发计划
|
||||||
|
|
||||||
|
> 本文档整合了渲染质量三方对比(ReadViewSDK vs WXRead)与功能开发计划,作为阅读器能力演进的单一真值。
|
||||||
|
> 基于读书 v10.0.3 逆向分析,与当前 ReadViewSDK 代码核查结果整合。
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
渲染内核的架构对齐解决的是"代码可维护性"问题。本文档梳理从"能用的阅读器"到"能上架的商业阅读器"还需要补齐哪些能力,每项能力标注三方状态和具体实施计划。
|
||||||
|
|
||||||
|
### 三方对比总览(渲染质量)
|
||||||
|
|
||||||
|
| 缺失项 | 严重度 | ReadViewSDK | WXRead | 差距说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| **竖排文字** | 高 | ❌ 未实现 | ❌ 未实现 | 双方均无 `writing-mode` 支持 |
|
||||||
|
| **Ruby 注音** | 高 | ❌ 未实现 | ❌ 未实现 | 双方均无 `<ruby>`/`<rt>` 处理 |
|
||||||
|
| **数学公式** | 中 | ❌ 未实现 | ⚠️ 仅字体回退 | 非原生渲染范畴,需 WebView 回退 |
|
||||||
|
| **复杂图文分页质量** | 高 | ✅ 已实现 | ✅ 已实现 | 双方均已实现 |
|
||||||
|
| **字体选择器** | 中 | ✅ 已实现 | ⚠️ 管线完备,UI 未见 | 已支持四档字体选择 |
|
||||||
|
| **连字/断字** | 中 | ⚠️ 仅配置标记 | ⚠️ 仅属性声明 | 双方均未实际实现 |
|
||||||
|
| **多语言排版回退** | 中 | ⚠️ 语言检测有,简繁转换无 | ✅ 已实现 | WXRead 额外有简繁转换 |
|
||||||
|
|
||||||
|
### ReadViewSDK 领先 WXRead 的项
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| **`keepWithNext`** | 已实现 `trimmedRangeForKeepWithNext`,WXRead 反编译代码中未找到对应实现 |
|
||||||
|
|
||||||
|
### 不需要对标 WXRead 的项
|
||||||
|
|
||||||
|
以下项 WXRead 自身也未实现,不属于必须补齐的能力:竖排文字、Ruby 注音、数学公式、Hyphenation 断字。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、渲染质量
|
||||||
|
|
||||||
|
### 1. 字体选择器
|
||||||
|
|
||||||
|
**当前状态**:✅ 基础能力已完成。
|
||||||
|
|
||||||
|
已支持系统、宋体、圆体、等宽四档字体选择;设置项可持久化;切换字体会触发重新分页;分页缓存签名已包含字体信息,避免不同字体复用旧缓存。
|
||||||
|
|
||||||
|
**已落地文件清单**:
|
||||||
|
|
||||||
|
| 文件 | 改动 | 状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `RDEPUBReaderConfiguration.swift` | 新增 `RDEPUBReaderFontChoice`,配置增加 `fontChoice` | 已完成 |
|
||||||
|
| `RDEPUBReaderSettings.swift` | 新增 `fontChoice` 持久化,更新 `applying(to:)` 和 `capture(configuration:brightness:)` | 已完成 |
|
||||||
|
| `RDEPUBReaderContext.swift` | `currentTextRenderStyle()` 改用 `configuration.fontChoice.font(ofSize:)` | 已完成 |
|
||||||
|
| `RDURLReaderController.swift` | URL 阅读器同步使用 `fontChoice` 生成文字样式 | 已完成 |
|
||||||
|
| `RDEPUBReaderController+RuntimeBridge.swift` | `requiresRepagination(from:to:)` 增加 `fontChoice` 变更检查 | 已完成 |
|
||||||
|
| `RDEPUBPaginationCacheCoordinator.swift` | 分页缓存签名增加字体名 | 已完成 |
|
||||||
|
| `RDEPUBReaderSettingsViewController.swift` | 新增 `onFontChoiceChange` 回调、字体分段控件和 accessibility identifier | 已完成 |
|
||||||
|
| `RDEPUBReaderChromeCoordinator.swift` | 设置面板接线字体变更回调 | 已完成 |
|
||||||
|
| `SettingsPanelTests.swift` | UI 自动化覆盖字体选择 | 已完成 |
|
||||||
|
|
||||||
|
**当前验收结果**:
|
||||||
|
- ✅ 切换字体后触发重排。
|
||||||
|
- ✅ 字体选择可随阅读器设置持久化。
|
||||||
|
- ✅ 分页缓存按字体隔离。
|
||||||
|
- ✅ UI 自动化已覆盖设置面板字体选择。
|
||||||
|
|
||||||
|
**后续增强计划**:
|
||||||
|
1. 引入更多内置字体包(建议:思源宋体、思源黑体、方正书宋、方正兰亭黑、Lora、OpenDyslexic)。
|
||||||
|
2. 使用 `CTFontManagerRegisterFontsForURL` 注册 bundle 字体。
|
||||||
|
3. 字体列表从枚举升级为资源驱动模型,每项包含 `displayName`、`fontName`、`previewText`、可用性状态。
|
||||||
|
4. 字体选择 UI 从分段控件升级为横向预览卡片,展示真实字体效果。
|
||||||
|
5. 增加字体资源加载失败兜底,失败时回退系统字体并保留用户设置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 暗色模式图片处理
|
||||||
|
|
||||||
|
**当前状态**:✅ 基础能力已完成。
|
||||||
|
|
||||||
|
暗色主题下会对正文图片做显示副本调暗,降低白底图片在深色背景上的刺眼程度。处理只作用于展示层,不修改分页原始内容;封面和小图会跳过,避免误处理封面、图标和装饰图。
|
||||||
|
|
||||||
|
**已落地文件清单**:
|
||||||
|
|
||||||
|
| 文件 | 改动 | 状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `RDEPUBReaderConfiguration.swift` | 新增 `darkImageAdjustmentEnabled` 和 `darkImageBlendRatio` | 已完成 |
|
||||||
|
| `RDEPUBReaderController+RuntimeBridge.swift` | 主题和暗色图片配置变更触发可见页刷新 | 已完成 |
|
||||||
|
| `RDEPUBTextContentView.swift` | DTCoreText 展示内容增加暗色图片显示副本处理 | 已完成 |
|
||||||
|
| `RDEPUBTextContentView.swift` | 增加暗色图片缓存,避免重复生成 | 已完成 |
|
||||||
|
| `SettingsPanelTests.swift` | UI 自动化覆盖暗色主题切换 | 已完成 |
|
||||||
|
|
||||||
|
**当前实现策略**:
|
||||||
|
- 仅当背景亮度低于阈值、配置开启、混合比例大于 0 时启用。
|
||||||
|
- 仅处理正文 inline image attachment。
|
||||||
|
- 跳过封面和小于 80x80 的图片。
|
||||||
|
- 使用 `NSCache` 缓存处理后的图片。
|
||||||
|
- 使用主题背景色按比例覆盖原图,透明区域保持透明。
|
||||||
|
|
||||||
|
**当前验收结果**:
|
||||||
|
- ✅ 暗色模式下图片不再完全以原始亮色块直出。
|
||||||
|
- ✅ 图片内容仍保持可辨认,不做反色。
|
||||||
|
- ✅ 处理结果有缓存。
|
||||||
|
- ✅ UI 自动化已覆盖暗色主题入口。
|
||||||
|
|
||||||
|
**后续增强计划**:
|
||||||
|
1. 增加真实 EPUB 图片样本的截图回归测试。
|
||||||
|
2. 按图片平均亮度自适应 `darkImageBlendRatio`。
|
||||||
|
3. 增加用户侧开关,允许关闭暗色图片处理。
|
||||||
|
4. 如果后续恢复 WebView 路径,再补充 CSS filter 策略。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 简繁转换
|
||||||
|
|
||||||
|
**当前状态**:零实现。语言元数据已提取(`publication.metadata.language`),但仅用于拉丁/CJK CSS 分轨。
|
||||||
|
|
||||||
|
**WXRead 实现参考**:
|
||||||
|
- `WREpubTypesetter` 检测 `book.language` 含 `Hant`/`TW`/`HK` 时调用 `_WRConvertHansToHantIfNeeded()`
|
||||||
|
- 使用 `CFStringTransform` 两步转换:Hans → Latin → Hant
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:转换工具**
|
||||||
|
1. 新增 `RDEPUBTextChineseConverter.swift`,提供:
|
||||||
|
```swift
|
||||||
|
enum RDEPUBChineseScript { case hans, hant }
|
||||||
|
|
||||||
|
func convert(_ text: String, to script: RDEPUBChineseScript) -> String
|
||||||
|
```
|
||||||
|
2. 实现使用 `CFStringTransform`:
|
||||||
|
```swift
|
||||||
|
let mutable = NSMutableString(string: text) as CFMutableString
|
||||||
|
CFStringTransform(mutable, nil, kCFStringTransformToLatin, false) // Hans → Pinyin
|
||||||
|
CFStringTransform(mutable, nil, kCFStringTransformStripDiacritics, false) // Pinyin → stripped
|
||||||
|
// 然后通过字典映射到繁体
|
||||||
|
```
|
||||||
|
或直接使用 Apple 的 `kCFStringTransformHansToHant`(如果可用)。
|
||||||
|
|
||||||
|
**Step 2:判断是否需要转换**
|
||||||
|
3. 在 `RDEPUBTextBookBuilder.build()` 或 `RDEPUBTextRendererSupport.makeChapterRenderRequest()` 中:
|
||||||
|
- 读取 `publication.metadata.language`
|
||||||
|
- 如果语言为 `zh-Hant`/`zh-TW`/`zh-HK` 且用户设置为简体,或语言为 `zh-Hans`/`zh-CN` 且用户设置为繁体,触发转换
|
||||||
|
4. 新增 `RDEPUBReaderConfiguration.chineseScript: RDEPUBChineseScript?`(nil = 跟随书籍)
|
||||||
|
|
||||||
|
**Step 3:应用转换**
|
||||||
|
5. 在 `normalizeReadingAttributes(in:style:)` 之后、返回 `RDEPUBTextChapterRenderRequest` 之前,对 attributed string 的文本内容执行转换
|
||||||
|
6. 需要保持 attributed string 的属性(字体、颜色、附件)不变,只替换字符
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 简体 EPUB 在繁体模式下显示繁体
|
||||||
|
- 繁体 EPUB 在简体模式下显示简体
|
||||||
|
- 转换不影响分页结果(转换后字符数可能变化,需验证)
|
||||||
|
- 高亮、搜索功能在转换后仍然正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 孤行/寡行控制
|
||||||
|
|
||||||
|
**当前状态**:`RDEPUBTextLayoutConfig` 声明了 `avoidOrphans`/`avoidWidows`(默认 true),但 `RDEPUBTextLayouter` 完全不读取这两个标记。
|
||||||
|
|
||||||
|
**实现原理**:
|
||||||
|
- **寡行(widow)**:段落最后一行单独出现在页底 → 应将该行拉到下一页
|
||||||
|
- **孤行(orphan)**:段落第一行单独出现在页首 → 应将前一页最后一行拉过来
|
||||||
|
- 通常只处理寡行(保留至少 2 行在页底),孤行处理会引发连锁重排
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:CoreText 路径**
|
||||||
|
1. 在 `RDEPUBTextLayouter.layoutFramesUsingCoreText()` 的分页循环中(~行 63-137),在 `adjustedRange` 之后、追加 frame 之前,增加寡行检查:
|
||||||
|
```swift
|
||||||
|
if config.avoidWidows {
|
||||||
|
finalRange = trimmedRangeForAvoidWidows(
|
||||||
|
from: ctFrame,
|
||||||
|
proposed: finalRange,
|
||||||
|
lineRanges: lineRanges
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. `trimmedRangeForAvoidWidows` 实现:
|
||||||
|
- 从 proposed range 的最后一行向前检查
|
||||||
|
- 如果最后一行是一个段落的最后一行,且该段落在此页只有 1 行 → 回退到上一个段落边界
|
||||||
|
- 最多移除 2 行(避免过度收缩)
|
||||||
|
|
||||||
|
**Step 2:DTCoreText 路径**
|
||||||
|
3. 在 `layoutFramesUsingDTCoreText()` 中增加相同逻辑
|
||||||
|
|
||||||
|
**Step 3:段落边界检测**
|
||||||
|
4. 利用已有的 `paragraphRange(containing:)` 方法(`NSString.paragraphRange`)获取段落范围
|
||||||
|
5. 对比当前页最后一行的 range 和段落的 range,判断是否为段落唯一一行
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 分页页数可能轻微增加(1-3 页),但排版质量提升
|
||||||
|
- 已知的寡行问题页在修复后不再出现段落最后一行孤悬页底
|
||||||
|
- `avoidOrphans`/`avoidWidows = false` 时行为不变
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Hyphenation 断字
|
||||||
|
|
||||||
|
**当前状态**:`hyphenation: Bool = true` 配置标记已声明,但 `NSParagraphStyle.hyphenationFactor` 从未设置。CoreText 路径更不支持断字。
|
||||||
|
|
||||||
|
**技术约束**:
|
||||||
|
- `NSParagraphStyle.hyphenationFactor` 对 `UITextView`(降级路径)有效
|
||||||
|
- CoreText 直绘路径需要设置 `kCTParagraphStyleSpecifierHyphenationFactor`
|
||||||
|
- 中文场景断字影响较小(中文无空格分词),主要改善西文排版
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:UITextView 降级路径**
|
||||||
|
1. 在 `RDEPUBTextRendererSupport.paragraphStyle(lineSpacing:)`(行 929-934)中增加:
|
||||||
|
```swift
|
||||||
|
style.hyphenationFactor = 1.0
|
||||||
|
```
|
||||||
|
2. 在 `normalizeReadingAttributes()`(行 137-143)中,对已有的 `NSMutableParagraphStyle` 增加:
|
||||||
|
```swift
|
||||||
|
paragraphStyle.hyphenationFactor = 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2:CoreText 直绘路径**
|
||||||
|
3. 在 `RDEPUBTextLayouter` 的 frame 构造中,对 `CTParagraphStyle` 增加 hyphenation factor:
|
||||||
|
```swift
|
||||||
|
var hyphenationFactor: Float = 1.0
|
||||||
|
let settings = [CTParagraphStyleSetting(
|
||||||
|
spec: .hyphenationFactor,
|
||||||
|
valueSize: MemoryLayout<Float>.size,
|
||||||
|
value: &hyphenationFactor
|
||||||
|
)]
|
||||||
|
let paragraphStyle = CTParagraphStyleCreate(settings, settings.count)
|
||||||
|
```
|
||||||
|
4. 将此 paragraph style 设置到 attributed string 的段落属性上
|
||||||
|
|
||||||
|
**Step 3:条件控制**
|
||||||
|
5. 读取 `config.hyphenation` 标记,为 false 时跳过上述设置
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 西文长单词在行末正确断字(显示连字符)
|
||||||
|
- 中文排版不受影响
|
||||||
|
- `hyphenation = false` 时回到当前行为
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、工程成熟度
|
||||||
|
|
||||||
|
### 6. 自动化测试
|
||||||
|
|
||||||
|
**当前状态**:✅ UI 自动化测试基础设施已完成,单元测试和分页回归基准待补。
|
||||||
|
|
||||||
|
已在 Demo 工程中接入 `ReadViewDemoUITests`,覆盖打开书籍、阅读器基础交互、顶部/底部工具栏、设置面板、字体选择和暗色主题切换。
|
||||||
|
|
||||||
|
**已落地内容**:
|
||||||
|
|
||||||
|
| 项 | 状态 |
|
||||||
|
| --- | --- |
|
||||||
|
| UI Test target `ReadViewDemoUITests` | ✅ 已完成 |
|
||||||
|
| Demo 自动打开测试 EPUB 的入口 | ✅ 已完成 |
|
||||||
|
| Accessibility identifier 体系 | ✅ 已完成 |
|
||||||
|
| 阅读器基础打开测试 | ✅ 已完成 |
|
||||||
|
| 顶部/底部工具栏测试 | ✅ 已完成 |
|
||||||
|
| 设置面板测试 | ✅ 已完成 |
|
||||||
|
| 字体选择 UI 测试 | ✅ 已完成 |
|
||||||
|
| 暗色主题切换 UI 测试 | ✅ 已完成 |
|
||||||
|
|
||||||
|
**当前验证结果**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
xcodebuild build: BUILD SUCCEEDED
|
||||||
|
SettingsPanelTests: TEST SUCCEEDED
|
||||||
|
ReadViewDemoUITests 全量 8 个 UI 测试: TEST SUCCEEDED
|
||||||
|
```
|
||||||
|
|
||||||
|
**下一步实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:单元测试基础设施**
|
||||||
|
1. 在 `ReadViewDemo.xcodeproj` 中新增 Unit Test target `ReadViewSDKTests`
|
||||||
|
2. 创建 `Tests/` 目录,配置 `import RDReaderView` 或通过 Demo target 暴露内部测试入口
|
||||||
|
3. 新增 `.xctestplan` 配置,把 UI 测试和单元测试拆分为不同测试组
|
||||||
|
|
||||||
|
**Step 2:分页回归测试(最高优先级)**
|
||||||
|
4. 新增 `RDEPUBPaginationRegressionTests.swift`,固定 6 类样本章节:
|
||||||
|
- 纯正文章节
|
||||||
|
- 图 + 图注 + 正文章节
|
||||||
|
- 脚注小图标章节
|
||||||
|
- 多段标题章节
|
||||||
|
- 大图跨页章节
|
||||||
|
- 长段落连续章节
|
||||||
|
5. 每个样本:构建 `RDEPUBTextBook`,断言总页数不变、已知页的 `contentRange` 不变
|
||||||
|
6. 使用 golden file 对比 `RDEPUBTextChapterPaginationDiagnostic` 输出
|
||||||
|
|
||||||
|
**Step 3:位置映射测试**
|
||||||
|
7. 新增 `RDEPUBLocationMappingTests.swift`:
|
||||||
|
- `location → pageNumber` 正向映射
|
||||||
|
- `pageNumber → location` 逆向映射
|
||||||
|
- 往返一致性断言
|
||||||
|
|
||||||
|
**Step 4:渲染管线测试**
|
||||||
|
8. 新增 `RDEPUBTypesetterTests.swift`:
|
||||||
|
- `normalizeHTML` 输入输出对比
|
||||||
|
- fragment marker 注入/提取一致性
|
||||||
|
- 语义标记注入后 `rdPage*` 属性计数不变
|
||||||
|
|
||||||
|
**Step 5:CI(可选)**
|
||||||
|
9. 配置 GitHub Actions 或 Jenkins,在 PR 时自动运行测试
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- UI 自动化全量测试稳定通过
|
||||||
|
- 6 个分页基准样本的页数和 contentRange 稳定
|
||||||
|
- 位置映射往返测试通过
|
||||||
|
- 新 PR 触发测试自动运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 性能基线
|
||||||
|
|
||||||
|
**当前状态**:`RDEPUBTextPerformanceSampler` 用 `CFAbsoluteTimeGetCurrent()` 采样,结果仅 `print()` 输出。无 Instruments 可视化、无内存监控、无阈值告警。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:os_signpost 集成**
|
||||||
|
1. 在 `RDEPUBTextPerformanceSampler` 中引入 `os.signpost`:
|
||||||
|
```swift
|
||||||
|
import os.signpost
|
||||||
|
let log = OSLog(subsystem: "com.rdreader", category: .pointsOfInterest)
|
||||||
|
```
|
||||||
|
2. 在 build/render/paginate 各阶段插入 `os_signpost(.begin, ...)` / `os_signpost(.end, ...)`
|
||||||
|
3. 这样 Instruments 的 Points of Interest 能直接可视化各阶段耗时
|
||||||
|
|
||||||
|
**Step 2:内存监控**
|
||||||
|
4. 新增 `RDEPUBMemoryMonitor`:
|
||||||
|
```swift
|
||||||
|
func currentMemoryFootprint() -> UInt64 // task_info.resident_size
|
||||||
|
```
|
||||||
|
5. 在 `RDEPUBTextBookBuilder.build()` 的每章循环中记录内存峰值
|
||||||
|
6. 在 `RDEPUBTextPerformanceSample` 中增加 `peakMemoryBytes: UInt64`
|
||||||
|
|
||||||
|
**Step 3:阈值告警**
|
||||||
|
7. 定义基准值:首屏 < 2s、单章渲染 < 500ms、全书构建 < 30s(按书的大小可调)
|
||||||
|
8. 超出阈值时通过 `os_log(.error, ...)` 输出警告
|
||||||
|
9. 在 Demo 中展示性能摘要面板
|
||||||
|
|
||||||
|
**Step 4:持久化**
|
||||||
|
10. 性能样本可选持久化到文件,供回归对比
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- Instruments 能看到各阶段的 signpost 区间
|
||||||
|
- 内存峰值在大书(79MB 样本)构建过程中有记录
|
||||||
|
- 超出阈值时有日志输出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 崩溃防护
|
||||||
|
|
||||||
|
**当前状态**:`RDReaderView` 有 pageCurl 崩溃检测和异步恢复,但无全局异常捕获。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:NSException 捕获**
|
||||||
|
1. 新增 `RDEPUBCrashGuard` 工具类:
|
||||||
|
```swift
|
||||||
|
static func performSafely(_ block: () throws -> Void) rethrows
|
||||||
|
static func performWithObjCExceptionHandling(_ block: () -> Void) -> Bool
|
||||||
|
```
|
||||||
|
2. 在关键入口包裹 `@try/@catch`:
|
||||||
|
- `RDEPUBTextBookBuilder.build()` 的每章循环体
|
||||||
|
- `RDEPUBTextLayouter.layoutFrames()` 的分页循环
|
||||||
|
- `RDEPUBTextRendererSupport.makeChapterRenderRequest()` 的 HTML 处理
|
||||||
|
|
||||||
|
**Step 2:分页/渲染单章隔离**
|
||||||
|
3. 单章渲染/分页失败时,降级到空白页或上一次缓存结果,不中断全书构建
|
||||||
|
4. 在 `RDEPUBTextChapterPaginationDiagnostic` 中记录错误状态
|
||||||
|
|
||||||
|
**Step 3:全局信号处理(可选)**
|
||||||
|
5. 注册 `NSSetUncaughtExceptionHandler` 记录 ObjC 异常
|
||||||
|
6. 注册 signal handler(SIGABRT, SIGSEGV)记录崩溃现场
|
||||||
|
7. 下次启动时上报(如果后续有上报系统)
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 单章渲染异常不导致整个 build 中断
|
||||||
|
- pageCurl 崩溃场景已有保护,验证不退化
|
||||||
|
- 崩溃日志可追踪到具体章节和阶段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 内存管理
|
||||||
|
|
||||||
|
**当前状态**:无显式内存预算。无 `didReceiveMemoryWarning` 处理。`RDEPUBTextBookCache` 磁盘缓存无大小限制。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:内存警告响应**
|
||||||
|
1. 在 `RDEPUBReaderController` 中监听 `UIApplication.didReceiveMemoryWarningNotification`
|
||||||
|
2. 收到警告时:
|
||||||
|
- 清空 `RDReaderPreloadController` 的预加载缓存
|
||||||
|
- 通知 `RDEPUBTextBookCache` 的内存缓存(如果有)清空
|
||||||
|
- 释放非当前可见章节的渲染结果
|
||||||
|
|
||||||
|
**Step 2:章节级内存释放**
|
||||||
|
3. 在 `RDEPUBTextBookBuilder` 构建完成后,不再持有已构建章节的 `NSAttributedString`
|
||||||
|
4. 当前只持有 `RDEPUBTextBook`(包含 `[RDEPUBTextPage]` 的 NSRange),内存占用已较低
|
||||||
|
5. 如果后续引入 attributed string 缓存,需增加 LRU 淘汰策略
|
||||||
|
|
||||||
|
**Step 3:图片内存控制**
|
||||||
|
6. `RDReaderPreloadController` 预加载的 page view 数量与内存挂钩
|
||||||
|
7. 当内存压力时减少 `preloadRadius`(从 1 降到 0)
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 在大书(79MB)构建过程中收到内存警告时,app 不被系统杀掉
|
||||||
|
- 预加载缓存在内存压力下自动收缩
|
||||||
|
- 当前页内容不丢失
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. 增量构建
|
||||||
|
|
||||||
|
**当前状态**:全书一次性分页。`RDEPUBTextBookBuilder.build()` 遍历全部 spine item,输出完整 `RDEPUBTextBook`。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:章节级构建接口**
|
||||||
|
1. 在 `RDEPUBTextBookBuilder` 上新增:
|
||||||
|
```swift
|
||||||
|
func buildChapter(
|
||||||
|
at spineIndex: Int,
|
||||||
|
publication: RDEPUBPublication,
|
||||||
|
parser: RDEPUBParser,
|
||||||
|
pageSize: CGSize,
|
||||||
|
style: RDEPUBTextRenderStyle
|
||||||
|
) -> RDEPUBTextChapter?
|
||||||
|
```
|
||||||
|
2. 内部逻辑从 `build()` 的循环体中提取,单章可独立构建
|
||||||
|
|
||||||
|
**Step 2:按需加载**
|
||||||
|
3. `RDEPUBReaderLoadCoordinator` 在打开书时只构建当前章节和相邻 ±1 章
|
||||||
|
4. 用户翻到新章节时,后台异步构建该章节
|
||||||
|
5. 使用 `DispatchQueue` 串行队列保证线程安全
|
||||||
|
|
||||||
|
**Step 3:与缓存联动**
|
||||||
|
6. `RDEPUBPaginationCacheCoordinator` 支持单章缓存命中检查
|
||||||
|
7. 缓存命中时跳过构建,直接加载
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 大书首次打开时间缩短(只构建 3 章而非全书)
|
||||||
|
- 翻到未构建章节时无明显卡顿(后台预构建)
|
||||||
|
- 全书构建 API 保持不变(兼容现有调用方)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. 缓存管理
|
||||||
|
|
||||||
|
**当前状态**:`RDEPUBTextBookCache` 磁盘缓存无大小限制、无淘汰策略、无选择性失效。无内存缓存。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:磁盘缓存容量控制**
|
||||||
|
1. 新增 `maxDiskCacheSize: UInt64`(默认 200MB)
|
||||||
|
2. 在 `save(_:key:)` 写入后检查总大小
|
||||||
|
3. 超限时按修改时间删除最旧的缓存文件,直到低于阈值
|
||||||
|
|
||||||
|
**Step 2:选择性失效**
|
||||||
|
4. 新增 `invalidate(key:)` 方法,删除指定缓存文件
|
||||||
|
5. 当用户修改 `fontChoice` 或 `fontSize` 时,只失效当前书的缓存(而非 `invalidateAll`)
|
||||||
|
|
||||||
|
**Step 3:内存缓存(NSCache)**
|
||||||
|
6. 在 `RDEPUBTextBookCache` 上层增加 `NSCache<NSString, PaginationCacheArchive>`
|
||||||
|
7. `load(key:)` 先查内存缓存,miss 后查磁盘并回填
|
||||||
|
8. 内存警告时清空 NSCache
|
||||||
|
|
||||||
|
**Step 4:缓存统计持久化**
|
||||||
|
9. `RDEPUBTextBookCache` 新增 `cacheStats` 属性,记录总命中/缺失次数
|
||||||
|
10. 可用于调试和性能分析
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 磁盘缓存不超过设定上限
|
||||||
|
- 修改字号/字体后,旧缓存被正确失效
|
||||||
|
- 连续打开同一本书时,第二次命中内存缓存(< 1ms)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、可访问性
|
||||||
|
|
||||||
|
### 12. VoiceOver
|
||||||
|
|
||||||
|
**当前状态**:9 个 `accessibilityIdentifier`(仅用于 UI 测试),无 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。阅读内容区域无任何无障碍支持。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
**Step 1:工具栏无障碍(最低门槛)**
|
||||||
|
1. `RDEPUBReaderTopToolView`:
|
||||||
|
- `backButton.accessibilityLabel = "返回"`
|
||||||
|
- `bookmarkButton.accessibilityLabel = "书签"` + `accessibilityValue` 反映当前状态(已添加/未添加)
|
||||||
|
- `titleLabel.accessibilityLabel` = 书籍标题
|
||||||
|
2. `RDEPUBReaderBottomToolView`:
|
||||||
|
- 各按钮补充 `accessibilityLabel`("目录"、"书签列表"、"高亮列表"、"添加高亮"、"设置")
|
||||||
|
- 进度 slider 补充 `accessibilityValue`("第 X 页,共 Y 页")
|
||||||
|
|
||||||
|
**Step 2:设置面板**
|
||||||
|
3. `RDEPUBReaderSettingsViewController` 所有控件补充 label 和 traits
|
||||||
|
|
||||||
|
**Step 3:阅读内容区域(中等难度)**
|
||||||
|
4. `RDEPUBTextContentView` 设置 `isAccessibilityElement = true`
|
||||||
|
5. `accessibilityLabel` = 当前页纯文本内容
|
||||||
|
6. 翻页时发出 `UIAccessibility.pageScrolledNotification`
|
||||||
|
|
||||||
|
**Step 4:目录和高亮列表**
|
||||||
|
7. `RDEPUBReaderChapterListController` 列表项设置 `accessibilityLabel`(章节标题 + 页码)
|
||||||
|
8. `RDEPUBReaderHighlightsViewController` 列表项设置 `accessibilityLabel`(高亮文本 + 章节)
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- VoiceOver 用户能完整导航工具栏
|
||||||
|
- 翻页时 VoiceOver 朗读新页内容
|
||||||
|
- 目录和高亮列表可被 VoiceOver 逐项朗读
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Dynamic Type
|
||||||
|
|
||||||
|
**当前状态**:字号由 `RDEPUBReaderConfiguration.fontSize` 控制,不响应系统 `UIContentSizeCategory` 变化。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
1. 在 `RDEPUBReaderSettingsViewController` 中监听 `UIContentSizeCategory.didChangeNotification`
|
||||||
|
2. 当系统字体大小变化时,根据新的 `UIContentSizeCategory` 计算等效字号
|
||||||
|
3. 或者:在 `RDEPUBReaderConfiguration` 中增加 `useSystemFontSize: Bool`,启用时忽略 `fontSize`,使用系统推荐值
|
||||||
|
4. 字号映射表(参考 Apple 的 `preferredFont(forTextStyle:)` 返回值)
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 系统字体调大后,阅读器字号自动跟随
|
||||||
|
- 用户手动调节字号后,覆盖系统设置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. 高对比度
|
||||||
|
|
||||||
|
**当前状态**:6 个固定主题预设,不跟随系统 `UIAccessibility.isDarkerSystemColorsEnabled`。
|
||||||
|
|
||||||
|
**实施步骤**:
|
||||||
|
|
||||||
|
1. `RDEPUBReaderTheme` 增加 `highContrastVariant: RDEPUBReaderTheme?` 属性
|
||||||
|
2. 监听 `UIAccessibility.darkerSystemColorsStatusDidChangeNotification`
|
||||||
|
3. 高对比度启用时,自动切换到高对比度主题变体(更大色彩对比度)
|
||||||
|
4. 或提供独立的"高对比度"主题预设
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 系统高对比度开启后,阅读器自动切换到高对比度主题
|
||||||
|
- 关闭后恢复原主题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. DRM
|
||||||
|
|
||||||
|
**当前状态**:需求文档明确声明 DRM 不在 SDK 范围内。如果需要支持,建议通过以下方式:
|
||||||
|
|
||||||
|
**建议方案**:
|
||||||
|
|
||||||
|
1. 不在 SDK 内实现 DRM,而是通过 `RDEPUBParser` 的输入端控制:
|
||||||
|
- `RDEPUBParser` 接受 `Data` 或 `URL`,调用方可以先解密再传入
|
||||||
|
- 或新增 `RDEPUBDRMProvider` 协议:
|
||||||
|
```swift
|
||||||
|
protocol RDEPUBDRMProvider {
|
||||||
|
func decryptedData(for resourceURL: URL) -> Data?
|
||||||
|
func isProtected(_ resourceURL: URL) -> Bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. SDK 内的 `ss-reader://` URL scheme handler 在读取资源时调用 `DRMProvider`
|
||||||
|
3. 商业 DRM(如 Adobe ADEPT、Readium LCP)由调用方集成,SDK 提供接入点
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- SDK 提供清晰的 DRM 接入协议
|
||||||
|
- 不引入 DRM 依赖,保持 SDK 轻量
|
||||||
|
- 调用方能通过协议接入自己的 DRM 方案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、功能完整度(暂未展开)
|
||||||
|
|
||||||
|
以下功能需求已识别但暂未进入详细开发计划:
|
||||||
|
|
||||||
|
| 缺失项 | 严重度 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **书架/书库管理** | 高 | SDK 只能打开单本书,无书架 UI、阅读历史、分类管理 |
|
||||||
|
| **批注导出/分享** | 高 | 高亮/笔记只有本地存储,无导出、分享、复制到剪贴板 |
|
||||||
|
| **阅读统计** | 中 | 无阅读时长追踪、阅读速度、连续阅读天数 |
|
||||||
|
| **TTS 朗读** | 中 | 微信读书核心功能之一,当前无任何语音相关代码 |
|
||||||
|
| **全局搜索** | 中 | 当前搜索只在单本书内,无跨书搜索 |
|
||||||
|
| **离线/云端同步** | 高 | 无 iCloud/自建同步,阅读进度和笔记只在本地 |
|
||||||
|
| **夜间模式定时切换** | 低 | 有暗色主题但不能跟随系统或定时切换 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、按优先级排序的建议路线
|
||||||
|
|
||||||
|
### P0 — 影响商业发布
|
||||||
|
|
||||||
|
1. **分页回归基准** — UI 自动化测试已启动,下一步需要把分页结果、首屏时间、截图差异纳入回归基准
|
||||||
|
2. **字体选择器增强** — 基础字体选择器已实现,后续需补:更多内置字体包、字体预览、字体资源加载失败兜底
|
||||||
|
3. **书架/书库管理** — 商业阅读器的入口
|
||||||
|
4. **暗色模式图片处理增强** — 基础处理已实现,后续可补:按图片亮度自适应混合比例
|
||||||
|
|
||||||
|
### P1 — 影响用户留存
|
||||||
|
|
||||||
|
5. **批注导出/分享** — 深度阅读用户的核心需求
|
||||||
|
6. **阅读统计/时长追踪** — 用户粘性和产品数据的基础
|
||||||
|
7. **性能基线与大书优化** — 大书卡顿是用户流失的主要原因
|
||||||
|
8. **简繁转换** — 面向港澳台用户需要
|
||||||
|
|
||||||
|
### P2 — 提升竞争力
|
||||||
|
|
||||||
|
9. **TTS 朗读** — 通勤场景、无障碍场景刚需
|
||||||
|
10. **云端同步** — 多设备用户的基本需求
|
||||||
|
11. **VoiceOver 完善** — 合规和品牌形象
|
||||||
|
12. **全局搜索** — 藏书量大时的效率工具
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、总结
|
||||||
|
|
||||||
|
经过三方对比,渲染质量层面的真实差距比最初评估要小:
|
||||||
|
|
||||||
|
- **图文分页质量**:双方基本对齐,我们甚至在 `keepWithNext` 上领先
|
||||||
|
- **真正的差距**:字体包数量、分页回归基准、简繁转换
|
||||||
|
- **WXRead 也没做的**:竖排、ruby、公式、hyphenation——这些不是必须对齐的
|
||||||
|
|
||||||
|
如果目标是"能上架的商业阅读器",当前已补齐字体选择器、暗色模式图片处理和基础 UI 自动化测试。下一步最值得投入的是分页回归基准、更多字体资源和书架/书库管理。
|
||||||
2878
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
@ -7,14 +7,41 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
23BB1155EA379786DAA10A89 /* DisplayTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.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 */; };
|
||||||
|
63B2852E59996922386A3111 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F495E17B90CCF1C7FE8C27 /* AccessibilityIdentifiers.swift */; };
|
||||||
|
9AAFB855EC7AC09D1E0B6742 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 697A5BCF0D93DBEC18E21A09 /* Foundation.framework */; };
|
||||||
|
C08FF8D030048DC5147729E9 /* ReaderOpenCloseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDF066BA12974E6CFBE519F /* ReaderOpenCloseTests.swift */; };
|
||||||
DE1437A969DA1C5F0CBB047D /* Pods_ReadViewDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */; };
|
DE1437A969DA1C5F0CBB047D /* Pods_ReadViewDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */; };
|
||||||
|
FEDB5937CEB858CB06E38E2D /* SettingsPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201C2B482287866487EFAE66 /* SettingsPanelTests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
053B5CD55E42BCD8E4E77C14 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = DE070C152FBF0CC900ED065F /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = DE070C1C2FBF0CC900ED065F;
|
||||||
|
remoteInfo = ReadViewDemo;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
00F495E17B90CCF1C7FE8C27 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.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>"; };
|
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>"; };
|
||||||
423988C9F7F09A1B78D97CC1 /* Pods-ReadViewDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReadViewDemo.debug.xcconfig"; path = "Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo.debug.xcconfig"; sourceTree = "<group>"; };
|
423988C9F7F09A1B78D97CC1 /* Pods-ReadViewDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReadViewDemo.debug.xcconfig"; path = "Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
697A5BCF0D93DBEC18E21A09 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
|
74B4C44287820D68ED6570F8 /* XCUIApplication+Launch.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+Launch.swift"; sourceTree = "<group>"; };
|
||||||
792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReadViewDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReadViewDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
8FFD606A5A1CBDCC3CA87F1C /* ReadViewDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReadViewDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
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>"; };
|
||||||
DE070C1D2FBF0CC900ED065F /* ReadViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReadViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -39,6 +66,14 @@
|
|||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
3B8BD3932B95613EF7F4A449 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
9AAFB855EC7AC09D1E0B6742 /* Foundation.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
DE070C1A2FBF0CC900ED065F /* Frameworks */ = {
|
DE070C1A2FBF0CC900ED065F /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -50,10 +85,28 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
0F03902FB2E1DBD16B6E862F /* iOS */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
697A5BCF0D93DBEC18E21A09 /* Foundation.framework */,
|
||||||
|
);
|
||||||
|
name = iOS;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
5354236B404004825520D52F /* Helpers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
00F495E17B90CCF1C7FE8C27 /* AccessibilityIdentifiers.swift */,
|
||||||
|
74B4C44287820D68ED6570F8 /* XCUIApplication+Launch.swift */,
|
||||||
|
);
|
||||||
|
path = Helpers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9F8446930C35441C7E9A4DFF /* Frameworks */ = {
|
9F8446930C35441C7E9A4DFF /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */,
|
792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */,
|
||||||
|
0F03902FB2E1DBD16B6E862F /* iOS */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -74,6 +127,7 @@
|
|||||||
DE070C1E2FBF0CC900ED065F /* Products */,
|
DE070C1E2FBF0CC900ED065F /* Products */,
|
||||||
BAF21F51585EC06F8713C5C6 /* Pods */,
|
BAF21F51585EC06F8713C5C6 /* Pods */,
|
||||||
9F8446930C35441C7E9A4DFF /* Frameworks */,
|
9F8446930C35441C7E9A4DFF /* Frameworks */,
|
||||||
|
E31A99F5F070435147BC13CC /* ReadViewDemoUITests */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -81,13 +135,53 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DE070C1D2FBF0CC900ED065F /* ReadViewDemo.app */,
|
DE070C1D2FBF0CC900ED065F /* ReadViewDemo.app */,
|
||||||
|
8FFD606A5A1CBDCC3CA87F1C /* ReadViewDemoUITests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E31A99F5F070435147BC13CC /* ReadViewDemoUITests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5354236B404004825520D52F /* Helpers */,
|
||||||
|
EACA0D274176D594361EF6B9 /* ReaderUITests */,
|
||||||
|
);
|
||||||
|
path = ReadViewDemoUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
EACA0D274176D594361EF6B9 /* ReaderUITests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.swift */,
|
||||||
|
20BD15E5D8F04E7E9A239E14 /* ReaderAnnotationTests.swift */,
|
||||||
|
9EDF066BA12974E6CFBE519F /* ReaderOpenCloseTests.swift */,
|
||||||
|
BADF0A18AD034B74A482A4C1 /* ReaderToolbarTests.swift */,
|
||||||
|
201C2B482287866487EFAE66 /* SettingsPanelTests.swift */,
|
||||||
|
);
|
||||||
|
path = ReaderUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
7BE8E325ADE250D48D1A997A /* ReadViewDemoUITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = ADC5920FDF58B08AA59F7567 /* Build configuration list for PBXNativeTarget "ReadViewDemoUITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
3033E27A4E50D8AF46B7388D /* Sources */,
|
||||||
|
3B8BD3932B95613EF7F4A449 /* Frameworks */,
|
||||||
|
ADEE76F68D081663B23CD0BB /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
B0E4ACDE6B79427A20B5BE9A /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = ReadViewDemoUITests;
|
||||||
|
productName = ReadViewDemoUITests;
|
||||||
|
productReference = 8FFD606A5A1CBDCC3CA87F1C /* ReadViewDemoUITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
|
};
|
||||||
DE070C1C2FBF0CC900ED065F /* ReadViewDemo */ = {
|
DE070C1C2FBF0CC900ED065F /* ReadViewDemo */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = DE070C302FBF0CCA00ED065F /* Build configuration list for PBXNativeTarget "ReadViewDemo" */;
|
buildConfigurationList = DE070C302FBF0CCA00ED065F /* Build configuration list for PBXNativeTarget "ReadViewDemo" */;
|
||||||
@ -140,11 +234,19 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
DE070C1C2FBF0CC900ED065F /* ReadViewDemo */,
|
DE070C1C2FBF0CC900ED065F /* ReadViewDemo */,
|
||||||
|
7BE8E325ADE250D48D1A997A /* ReadViewDemoUITests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
ADEE76F68D081663B23CD0BB /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
DE070C1B2FBF0CC900ED065F /* Resources */ = {
|
DE070C1B2FBF0CC900ED065F /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -201,6 +303,20 @@
|
|||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
3033E27A4E50D8AF46B7388D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
63B2852E59996922386A3111 /* AccessibilityIdentifiers.swift in Sources */,
|
||||||
|
3BC5C96D7A0ACF35F2192CC7 /* XCUIApplication+Launch.swift in Sources */,
|
||||||
|
23BB1155EA379786DAA10A89 /* DisplayTypeTests.swift in Sources */,
|
||||||
|
52A6A0C9941A4B5F9B88C6B0 /* ReaderAnnotationTests.swift in Sources */,
|
||||||
|
C08FF8D030048DC5147729E9 /* ReaderOpenCloseTests.swift in Sources */,
|
||||||
|
4509ED928F228F43888E063D /* ReaderToolbarTests.swift in Sources */,
|
||||||
|
FEDB5937CEB858CB06E38E2D /* SettingsPanelTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
DE070C192FBF0CC900ED065F /* Sources */ = {
|
DE070C192FBF0CC900ED065F /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -210,7 +326,63 @@
|
|||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
B0E4ACDE6B79427A20B5BE9A /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
name = ReadViewDemo;
|
||||||
|
target = DE070C1C2FBF0CC900ED065F /* ReadViewDemo */;
|
||||||
|
targetProxy = 053B5CD55E42BCD8E4E77C14 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
258209CBC2293BAD3EF2B836 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MG4Z7FU83W;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.shen.ReadViewDemoUITests;
|
||||||
|
PRODUCT_MODULE_NAME = ReadViewDemoUITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = TouchLinked_dev;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = ReadViewDemo;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
WRAPPER_EXTENSION = xctest;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
2995C92FBA100970A8A427B4 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MG4Z7FU83W;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.shen.ReadViewDemoUITests;
|
||||||
|
PRODUCT_MODULE_NAME = ReadViewDemoUITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = TouchLinked_dev;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = ReadViewDemo;
|
||||||
|
WRAPPER_EXTENSION = xctest;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
DE070C312FBF0CCA00ED065F /* Debug */ = {
|
DE070C312FBF0CCA00ED065F /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 423988C9F7F09A1B78D97CC1 /* Pods-ReadViewDemo.debug.xcconfig */;
|
baseConfigurationReference = 423988C9F7F09A1B78D97CC1 /* Pods-ReadViewDemo.debug.xcconfig */;
|
||||||
@ -411,6 +583,15 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
ADC5920FDF58B08AA59F7567 /* Build configuration list for PBXNativeTarget "ReadViewDemoUITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
258209CBC2293BAD3EF2B836 /* Release */,
|
||||||
|
2995C92FBA100970A8A427B4 /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
DE070C182FBF0CC900ED065F /* Build configuration list for PBXProject "ReadViewDemo" */ = {
|
DE070C182FBF0CC900ED065F /* Build configuration list for PBXProject "ReadViewDemo" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@ -661,6 +661,9 @@ final class ViewController: UIViewController {
|
|||||||
|
|
||||||
if let automationPlan {
|
if let automationPlan {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak controller] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak controller] in
|
||||||
|
if let displayType = automationPlan.displayType {
|
||||||
|
controller?.applyDemoDisplayType(displayType)
|
||||||
|
}
|
||||||
if let pageNumber = automationPlan.pageNumber {
|
if let pageNumber = automationPlan.pageNumber {
|
||||||
_ = controller?.goToDemoPage(pageNumber)
|
_ = controller?.goToDemoPage(pageNumber)
|
||||||
}
|
}
|
||||||
@ -688,7 +691,7 @@ final class ViewController: UIViewController {
|
|||||||
|
|
||||||
private func currentReaderTextStyle() -> RDEPUBTextRenderStyle {
|
private func currentReaderTextStyle() -> RDEPUBTextRenderStyle {
|
||||||
let configuration = RDEPUBReaderConfiguration.default
|
let configuration = RDEPUBReaderConfiguration.default
|
||||||
let font = UIFont.systemFont(ofSize: configuration.fontSize)
|
let font = configuration.fontChoice.font(ofSize: configuration.fontSize)
|
||||||
let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4)
|
let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4)
|
||||||
return RDEPUBTextRenderStyle(
|
return RDEPUBTextRenderStyle(
|
||||||
font: font,
|
font: font,
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||||
|
<rootfiles>
|
||||||
|
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
||||||
|
</rootfiles>
|
||||||
|
</container>
|
||||||
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 507 KiB |
|
After Width: | Height: | Size: 561 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 571 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 513 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 443 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 868 KiB |
|
After Width: | Height: | Size: 589 KiB |
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 571 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 724 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 176 KiB |