feat(reader): 实现大书快速进入与增量分页路径

- PaginationCoordinator: 新增 textReflowable 快速进入路径,支持增量章节构建
  与 staged 合并策略,避免整书一次性构建的主线程阻塞
- TextBookBuilder: 重构为支持单章构建与增量合并模式
- ChapterPageCounter: 优化分页计数逻辑,支持章节级分页缓存
- PageFrameFactory: 新增 CoreText 页面帧工厂,提升排版性能
- ReaderContext/ReaderRuntime: 适配增量分页状态管理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-06-02 21:16:57 +08:00
co-authored by Claude Opus 4.7
parent 488350d956
commit c76beed03c
15 changed files with 984 additions and 268 deletions
@@ -25,6 +25,12 @@ import UIKit
/// navigationController?.pushViewController(controller, animated: true)
/// ```
public final class RDURLReaderController: UIViewController {
private struct PendingDemoPageRequest {
let pageNumber: Int
let animated: Bool
var attemptCount: Int
}
/// URL
private let bookURL: URL
/// EPUB
@@ -41,6 +47,10 @@ public final class RDURLReaderController: UIViewController {
label.text = "reader=opening"
return label
}()
private var pendingDemoPageRequest: PendingDemoPageRequest?
private var isRetryingPendingDemoPage = false
private let maxPendingDemoPageAttempts = 24
private let pendingDemoPageRetryDelay: TimeInterval = 0.25
///
/// - Parameters:
@@ -88,9 +98,12 @@ public final class RDURLReaderController: UIViewController {
/// - Returns:
@discardableResult
public func goToDemoPage(_ pageNumber: Int, animated: Bool = false) -> Bool {
let moved = readerController?.go(toPageNumber: pageNumber, animated: animated) ?? false
let moved = performDemoPageNavigation(pageNumber, animated: animated)
if moved {
emitDemoState(prefix: "page=\(pageNumber)")
pendingDemoPageRequest = nil
isRetryingPendingDemoPage = false
} else {
queuePendingDemoPageNavigation(pageNumber, animated: animated)
}
return moved
}
@@ -187,6 +200,73 @@ public final class RDURLReaderController: UIViewController {
embeddedController as? RDEPUBReaderController
}
@discardableResult
private func performDemoPageNavigation(_ pageNumber: Int, animated: Bool) -> Bool {
guard let readerController else { return false }
guard pageNumber > 0 else { return false }
let totalPages = readerController.textBook?.pages.count ?? readerController.activePages.count
let numPages = readerController.readerView.numberOfPages()
print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): textBook.pages=\(totalPages), readerView.numberOfPages=\(numPages), currentPage=\(readerController.readerView.currentPage)")
guard pageNumber <= totalPages else { return false }
readerController.readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated)
print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): after transition, currentPage=\(readerController.readerView.currentPage)")
emitDemoState(prefix: "page=\(pageNumber)")
return true
}
private func queuePendingDemoPageNavigation(_ pageNumber: Int, animated: Bool) {
if let pendingDemoPageRequest,
pendingDemoPageRequest.pageNumber == pageNumber,
pendingDemoPageRequest.animated == animated {
return
}
pendingDemoPageRequest = PendingDemoPageRequest(
pageNumber: pageNumber,
animated: animated,
attemptCount: 0
)
retryPendingDemoPageIfNeeded()
}
private func retryPendingDemoPageIfNeeded() {
guard !isRetryingPendingDemoPage else { return }
guard pendingDemoPageRequest != nil else { return }
isRetryingPendingDemoPage = true
DispatchQueue.main.asyncAfter(deadline: .now() + pendingDemoPageRetryDelay) { [weak self] in
self?.attemptPendingDemoPageNavigation()
}
}
private func attemptPendingDemoPageNavigation() {
guard var pendingDemoPageRequest else {
isRetryingPendingDemoPage = false
return
}
if performDemoPageNavigation(pendingDemoPageRequest.pageNumber, animated: pendingDemoPageRequest.animated) {
self.pendingDemoPageRequest = nil
isRetryingPendingDemoPage = false
return
}
pendingDemoPageRequest.attemptCount += 1
if pendingDemoPageRequest.attemptCount >= maxPendingDemoPageAttempts {
self.pendingDemoPageRequest = nil
isRetryingPendingDemoPage = false
print("[ReadViewDemo] automation page=\(pendingDemoPageRequest.pageNumber) failed after \(pendingDemoPageRequest.attemptCount) attempts")
return
}
self.pendingDemoPageRequest = pendingDemoPageRequest
DispatchQueue.main.asyncAfter(deadline: .now() + pendingDemoPageRetryDelay) { [weak self] in
self?.attemptPendingDemoPageNavigation()
}
}
/// Demo
private func logDemoState(prefix: String) {
guard let readerController else { return }
@@ -259,7 +339,13 @@ public final class RDURLReaderController: UIViewController {
}
extension RDURLReaderController: RDEPUBReaderDelegate {
public func epubReader(_ reader: UIViewController, didOpen publication: RDEPUBPublication) {
retryPendingDemoPageIfNeeded()
emitDemoState()
}
public func epubReader(_ reader: UIViewController, didUpdateLocation location: RDEPUBLocation) {
retryPendingDemoPageIfNeeded()
emitDemoState()
}