13 KiB
Phase 1: 对齐现状、边界与重构切入点 - Research
Researched: 2026-05-21 Domain: iOS EPUB reader architecture / DTCoreText-based native reflowable rendering Confidence: HIGH
<user_constraints>
User Constraints (from CONTEXT.md)
No user constraints - all decisions at the agent's discretion. </user_constraints>
<architectural_responsibility_map>
Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|---|---|---|---|
| EPUB URL entry and format routing | Browser/Client | — | RDURLReaderController decides epub vs txt and embeds the correct reader controller. |
| EPUB structure parsing and reading-profile split | Browser/Client | — | RDEPUBParser and RDEPUBPublication classify .textReflowable vs webFixedLayout / webInteractive. |
| Reflowable native chapter rendering | Browser/Client | — | RDEPUBDTCoreTextRenderer and RDEPUBTextBookBuilder own HTML to NSAttributedString and page slicing. |
| Fixed-layout and interactive content rendering | Browser/Client | — | RDEPUBWebView and RDEPUBPaginator keep WKWebView as the rendering and measurement path. |
| Page container and interaction shell | Browser/Client | — | RDReaderView is the stable presentation container that Phase 1 must not modify. |
</architectural_responsibility_map>
<research_summary>
Summary
当前仓库已经存在一条独立的原生 reflowable 路径,而不是“所有 EPUB 都走 WebView”。RDEPUBReaderController.paginatePublication 会在 publication.readingProfile == .textReflowable 时改走 RDEPUBTextBookBuilder,使用 RDEPUBDTCoreTextRenderer 把章节 HTML 转成 NSAttributedString,再通过 ss_pageRanges(size:) 基于 CoreText 可见范围切页。该路径最终产出 RDEPUBTextBook / RDEPUBTextPage,并由 RDEPUBTextContentView 在现有 RDReaderView 容器内展示。
Fixed Layout 与交互式 EPUB 的边界已经在代码里清晰存在。RDEPUBParser.readingProfile() 将 metadata.layout == .fixed 归类为 .webFixedLayout,并把包含脚本/多媒体/iframe/form 等交互内容的资源归类为 .webInteractive。这两类内容继续走 RDEPUBWebView / RDEPUBPaginator / WKWebView 体系,不应被纳入本次原生重构。
WXRead 参考资料说明,目标不是“再包一层轻量 renderer”,而是把当前 DTCoreText 路径升级为完整的 typesetter + layouter + layout frame 体系:五层 CSS 级联、自定义 DTCoreText 属性键、块级避免断页、页面级元数据、图片与背景等页面语义。对本仓库来说,最稳妥的切入点是保留 RDURLReaderController -> RDEPUBReaderController -> RDReaderView 外层契约不变,把增强集中在 EPUBTextRendering 内部及其与 reader state 的对接点。
Primary recommendation: Phase 1 应先把当前 .textReflowable 调用链、WebView 边界、兼容性约束和可演进触点写成明确文档,再以这些文档作为 Phase 2/3 的实现基线。
</research_summary>
<standard_stack>
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| UIKit | iOS 15+ SDK | Reader container, controllers, text view presentation | 现有 reader UI 与 RDReaderView 全部建立在 UIKit 上。 |
| CoreText | System | Native pagination and text measurement | 当前 ss_pageRanges(size:) 直接使用 CTFramesetterCreateFrame / CTFrameGetVisibleStringRange。 |
| DTCoreText | 1.6.28 | HTML/CSS to NSAttributedString |
当前原生 reflowable 渲染核心已经依赖它,后续定制也必须围绕它展开。 |
| WebKit | System | Fixed-layout / interactive EPUB rendering and pagination measurement | 非 reflowable-native 路径仍依赖 WKWebView。 |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| DTFoundation | 1.7.19 | DTCoreText supporting parser/runtime pieces | DTCoreText HTML 解析与附件支持所需。 |
| ZIPFoundation | 0.9.20 | EPUB archive handling | EPUB 解压与资源访问基础设施。 |
| SnapKit | 5.7.1 | UI layout helper | Reader UI 周边布局已有依赖,但不是本次内核切入点。 |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
Directly evolving EPUBTextRendering |
A second native engine alongside the existing one | 会增加长期双轨维护成本,并破坏 roadmap 已锁定的“旧引擎直接演进”原则。 |
| DTCoreText-based native rendering | Full WKWebView for all EPUB modes |
无法满足 WXRead-style page metadata / page-frame semantics / native selection control goals。 |
Keeping RDReaderView stable |
Reworking page container and gesture stack together | 改动半径过大,Phase 1 明确要求把翻页容器排除出本次重构。 |
Installation:
pod install
</standard_stack>
<architecture_patterns>
Architecture Patterns
System Architecture Diagram
RDURLReaderController
-> RDEPUBReaderController
-> RDEPUBParser.parse()
-> RDEPUBPublication / readingProfile()
-> textReflowable
-> RDEPUBTextBookBuilder
-> RDEPUBDTCoreTextRenderer
-> DTHTMLAttributedStringBuilder
-> NSAttributedString
-> RDEPUBTextPaginationSupport.ss_pageRanges()
-> RDEPUBTextBook / RDEPUBTextPage
-> RDEPUBTextContentView
-> RDReaderView
-> webFixedLayout
-> RDEPUBWebView.loadFixedSpread()
-> RDReaderView
-> webInteractive
-> RDEPUBPaginator + RDEPUBWebView.loadPage()
-> RDReaderView
Recommended Project Structure
Sources/RDReaderView/
├── EPUBCore/ # Parser, publication, web pagination, render requests
├── EPUBTextRendering/ # Native reflowable rendering and pagination
├── EPUBUI/ # Reader controller, content views, settings
└── RDReaderView.swift # Stable page container and gestures
Pattern 1: Preserve the outer reader contract
What: Keep RDURLReaderController, RDEPUBReaderController, persistence, location mapping, and RDReaderView as the stable shell while replacing internals inside EPUBTextRendering.
When to use: Any phase that upgrades the reflowable engine without expanding scope into the page container.
Example: RDEPUBReaderController.paginatePublication already branches into text vs web rendering without changing the reader container contract.
Pattern 2: Keep rendering and presentation separate
What: Produce rich RDEPUBTextBook / RDEPUBTextPage models first, then let RDEPUBTextContentView render those models.
When to use: When introducing page-level metadata, selection offsets, image semantics, or new pagination logic.
Example: RDEPUBTextBookBuilder owns chapter rendering and page slicing, while RDEPUBTextContentView only paints attributed substrings and overlays highlights/search state.
Pattern 3: Branch by publication profile early
What: Decide native reflowable vs fixed vs interactive before rendering work starts.
When to use: Any future entry-point or repagination logic.
Example: RDEPUBParser.readingProfile() uses layout == .fixed and interactive-content detection to keep WKWebView modes out of the native path.
Anti-Patterns to Avoid
- Touching
RDReaderViewduring engine migration: Phase 1 explicitly excludes page curl / scroll / gesture container changes. - Building a parallel renderer shell: The roadmap already rejects a “new engine beside old engine” approach.
- Mixing WebView-only semantics into the native path:
textReflowableshould evolve aroundNSAttributedStringand CoreText, not around hiddenWKWebViewdependencies. </architecture_patterns>
<dont_hand_roll>
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| HTML parsing | A brand-new EPUB HTML parser | DTCoreText builder extension points | 仓库已经围绕 DTCoreText 建立基础能力,重写 parser 成本高且风险大。 |
| EPUB mode classification | Ad-hoc view-layer checks | RDEPUBParser.readingProfile() |
当前 profile 分流已经覆盖 fixed / interactive / text reflowable。 |
| Reader shell replacement | A new page container | Existing RDReaderView contract |
现有 container 已承担翻页模式、双页与方向逻辑,超出本次范围。 |
| Location/highlight transport | A second mapping format | Existing RDEPUBLocation + RDEPUBTextOffsetRangeInfo |
这些类型已被 highlights / search / restore location 链路依赖。 |
Key insight: 应该复用现有阅读器契约和数据模型,把新增复杂度集中在旧引擎内部,而不是在外围重造一层基础设施。 </dont_hand_roll>
<common_pitfalls>
Common Pitfalls
Pitfall 1: Confusing native reflowable with the WebView paginator
What goes wrong: 把 RDEPUBPaginator 当成所有 reflowable 的基础,导致 Phase 2/3 方案偏向继续强化 WebView。
Why it happens: 当前工程同时存在 textReflowable 和基于 WKWebView 的分页/展示路径。
How to avoid: 在所有 Phase 1 输出里明确:RDEPUBPaginator 服务于 fixed 或 interactive/web paths;原生重构主战场是 EPUBTextRendering。
Warning signs: 计划任务把 RDEPUBPaginator 作为 CSS 五层或页面元数据的主要落点。
Pitfall 2: Breaking RDReaderView assumptions while changing pagination
What goes wrong: 新分页结果不再满足现有 page index / container reuse / orientation repagination 约定。
Why it happens: 容易把引擎升级和容器升级绑在一起处理。
How to avoid: 把 RDEPUBTextBook.pages / RDEPUBTextPage 视为 Phase 2/3 的兼容面,并在策略中显式约束 RDReaderView 不改。
Warning signs: 方案需要修改 RDReaderView.DataSource、翻页方向、双页逻辑或 content view reuse 协议。
Pitfall 3: Introducing richer layout semantics without preserving location mapping
What goes wrong: 新分页器能排版复杂元素,但阅读位置、高亮、搜索结果定位失效。
Why it happens: 只关注排版帧,不关注 pageStartOffset/pageEndOffset 与 RDEPUBLocation 的传递。
How to avoid: 把 offset continuity、fragment offset、selection range transport 作为 Phase 2/3 的第一类兼容目标。
Warning signs: 设计里没有说明新页面元数据如何回写 RDEPUBTextBook、RDEPUBSelection、RDEPUBHighlight。
</common_pitfalls>
<code_examples>
Code Examples
Current reflowable split
if publication.readingProfile == .textReflowable {
let renderer = resolvedTextRenderer()
let builder = RDEPUBTextBookBuilder(renderer: renderer)
let textBook = try builder.build(
parser: parser,
publication: publication,
pageSize: pageSize,
style: renderStyle
)
}
Current fixed vs interactive boundary
public func readingProfile() -> RDEPUBReadingProfile {
if metadata.layout == .fixed {
return .webFixedLayout
}
return hasInteractiveContent() ? .webInteractive : .textReflowable
}
Current native pagination primitive
while visibleRange.location + visibleRange.length < length {
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), path, nil)
visibleRange = CTFrameGetVisibleStringRange(frame)
ranges.append(NSRange(location: location, length: visibleRange.length))
location += visibleRange.length
}
</code_examples>
<sota_updates>
State of the Art (2024-2025)
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| Generic HTML to attributed string only | HTML to attributed string plus page-level layout semantics | 已反映在 WXRead 参考资料中 | 需要把 CSS/HTML 处理与分页语义一起设计,而不是只换 builder options。 |
| Simple visible-range page slicing | Block-aware layout frames and custom pagination metadata | 已反映在 WXRead 参考资料中 | 当前 CTFrameGetVisibleStringRange 模式会成为主要替换对象。 |
| Styling as a small option bag | Multi-layer stylesheet cascade | 已反映在 WXRead 参考资料中 | dtOptions 需要被更明确的 stylesheet pipeline 取代或扩展。 |
New tools/patterns to consider:
- DTCoreText attribute extension points: 适合承接 WXRead 风格自定义属性键。
- Page-level layout model: 需要在
RDEPUBTextChapter/RDEPUBTextPage附近引入更丰富的元数据,而不是只保存范围。
Deprecated/outdated:
- “Simple DTCoreText renderer + pageRanges” 作为长期方案:对复杂块元素、页面语义和 CSS 分层都不够。
- “为原生重构同时替换翻页容器”:与当前范围控制相冲突。 </sota_updates>
<open_questions>
Open Questions
LegacyRDReaderController/RDEPUBTextPaging.swift与新EPUBTextRendering之间哪些实现差异仍值得迁移,哪些应直接废弃,需要在 Phase 1 执行阶段做一次并列审计。Doc/FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md与当前源码是否已经出现偏差,需要在 Plan 01-02 中显式核对。- 未来自定义 DTCoreText 属性应优先挂在
NSAttributedString、页模型,还是单独的 layout frame 对象上,需要在策略文档中做出建议但不提前定版实现。 </open_questions>