Phase 1: Context 拆分 - 新增 RDEPUBReaderState/RDEPUBReaderEnvironment/RDEPUBReaderServices - RDEPUBReaderContext 改为过渡门面,代理到 State/Environment/Services Phase 2: Runtime 拆分 - 新增 RDEPUBPresentationRuntime 处理分页状态管理 - 新增 RDEPUBChapterWarmupOrchestrator 处理章节预热与加载编排 - RDEPUBReaderRuntime 从 1277 行收缩,公共 API 转发到新 facade Phase 0.5: 性能优化 - prepareOnDemandChapter 支持异步模式(allowSynchronousLoad: false) - extendPartialBookPageMapIfNeeded 改为 DispatchGroup 并发加载 - RDEPUBChapterOffsetMap.cfiMap 加 NSLock 保护数据竞争 - CFI Map 构建延迟到后台队列(scheduleDeferredCFIMapBuildIfNeeded) - RDEPUBTextPageRenderView 引入静态位图缓存 - RDEPUBTextContentView 新增 loadingSpinner 占位页 Phase 3: 状态机 - 新增 RDEPUBNavigationStateMachine(含 DEBUG 合法转换校验) - 新增 RDEPUBPaginationState 记录分页来源 Review 修复 - makeSummary 重复方法合并 - ensureNavigationTargetAvailable 同步路径加注释标记 UI 测试 - 新增 AsyncChapterLoadingTests(20 个测试,覆盖全部架构整改场景) - 跨章节翻页、延迟 CFI、状态机、内存警告、预加载、位置恢复等
185 lines
7.0 KiB
Swift
185 lines
7.0 KiB
Swift
import UIKit
|
|
|
|
final class RDEPUBPresentationRuntime {
|
|
|
|
private unowned let context: RDEPUBReaderContext
|
|
|
|
private unowned let locationCoordinator: RDEPUBReaderLocationCoordinator
|
|
|
|
private unowned let jumpSessionManager: RDEPUBJumpSessionManager
|
|
|
|
private unowned let reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator
|
|
|
|
let navigationStateMachine = RDEPUBNavigationStateMachine()
|
|
|
|
private(set) var paginationState = RDEPUBPaginationState()
|
|
|
|
init(
|
|
context: RDEPUBReaderContext,
|
|
locationCoordinator: RDEPUBReaderLocationCoordinator,
|
|
jumpSessionManager: RDEPUBJumpSessionManager,
|
|
reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator
|
|
) {
|
|
self.context = context
|
|
self.locationCoordinator = locationCoordinator
|
|
self.jumpSessionManager = jumpSessionManager
|
|
self.reconciliationCoordinator = reconciliationCoordinator
|
|
}
|
|
|
|
func applyBookPageMap(
|
|
_ bookPageMap: RDEPUBBookPageMap,
|
|
restoreLocation: RDEPUBLocation?,
|
|
finishPagination: (RDEPUBLocation?) -> Void
|
|
) {
|
|
navigationStateMachine.transition(to: .presentingWindow)
|
|
context.textBook = nil
|
|
context.bookPageMap = bookPageMap
|
|
context.pendingFullPageMap = nil
|
|
context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap))
|
|
paginationState.activePageMap = bookPageMap
|
|
paginationState.pendingFullPageMap = nil
|
|
paginationState.source = .initialPartial
|
|
finishPagination(restoreLocation)
|
|
}
|
|
|
|
func refreshBookPageMapInPlace(_ bookPageMap: RDEPUBBookPageMap) {
|
|
if let pendingMap = context.pendingFullPageMap {
|
|
let shouldKeepExisting =
|
|
pendingMap.totalChapters > bookPageMap.totalChapters ||
|
|
(pendingMap.totalChapters == bookPageMap.totalChapters &&
|
|
pendingMap.totalPages >= bookPageMap.totalPages)
|
|
if shouldKeepExisting {
|
|
return
|
|
}
|
|
}
|
|
|
|
navigationStateMachine.transition(to: .reconcilingFullMap)
|
|
context.pendingFullPageMap = bookPageMap
|
|
paginationState.pendingFullPageMap = bookPageMap
|
|
}
|
|
|
|
func applyPendingFullPageMapIfNeeded() {
|
|
guard let pendingMap = context.pendingFullPageMap,
|
|
let readerView = context.readerView,
|
|
let controller = context.controller else { return }
|
|
|
|
guard !controller.isRepaginating else { return }
|
|
|
|
navigationStateMachine.transition(to: .reconcilingFullMap)
|
|
let decision = reconciliationCoordinator.evaluateTakeover(
|
|
candidatePageMap: pendingMap,
|
|
candidateSegment: nil,
|
|
currentWindow: context.bookPageMap,
|
|
jumpSession: jumpSessionManager.activeSession
|
|
)
|
|
|
|
switch decision {
|
|
case .keepCurrentWindow:
|
|
RDEPUBBackgroundTrace.log("Reconciliation", "decision: keepCurrentWindow")
|
|
|
|
case .fullReplace(let newPageMap):
|
|
RDEPUBBackgroundTrace.log("Reconciliation", "decision: fullReplace")
|
|
applyFullPageMapReplacement(newPageMap, readerView: readerView, controller: controller)
|
|
|
|
case .expandWindow, .segmentReplace:
|
|
RDEPUBBackgroundTrace.log("Reconciliation", "decision: unexpected segment decision")
|
|
}
|
|
}
|
|
|
|
func applyExtendedPartialPageMap(
|
|
_ bookPageMap: RDEPUBBookPageMap,
|
|
currentPageNumber: Int,
|
|
currentLocation: RDEPUBLocation?
|
|
) {
|
|
guard let readerView = context.readerView else { return }
|
|
|
|
navigationStateMachine.transition(to: .presentingWindow)
|
|
context.bookPageMap = bookPageMap
|
|
context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap))
|
|
paginationState.activePageMap = bookPageMap
|
|
paginationState.source = .asyncExtension
|
|
|
|
readerView.reloadPageCountOnly()
|
|
if let currentLocation,
|
|
locationCoordinator.restoreReadingLocation(currentLocation, animated: false) {
|
|
return
|
|
}
|
|
readerView.transitionToPage(pageNum: max(currentPageNumber - 1, 0), animated: false)
|
|
}
|
|
|
|
func applySettingsPreviewPageMap(_ bookPageMap: RDEPUBBookPageMap) {
|
|
navigationStateMachine.transition(to: .presentingWindow)
|
|
context.bookPageMap = bookPageMap
|
|
context.replaceActiveSnapshot(makeSnapshot(from: bookPageMap))
|
|
paginationState.activePageMap = bookPageMap
|
|
paginationState.source = .settingsPreview
|
|
}
|
|
|
|
func clear() {
|
|
paginationState = RDEPUBPaginationState()
|
|
navigationStateMachine.transition(to: .idle)
|
|
}
|
|
|
|
func makeSnapshot(from bookPageMap: RDEPUBBookPageMap) -> RDEPUBReadingSession.PaginationSnapshot {
|
|
let pages = bookPageMap.entries.flatMap { entry in
|
|
(0..<entry.pageCount).map { localPageIndex in
|
|
EPUBPage(
|
|
spineIndex: entry.spineIndex,
|
|
chapterIndex: bookPageMap.chapterIndex(forSpineIndex: entry.spineIndex) ?? 0,
|
|
pageIndexInChapter: localPageIndex,
|
|
totalPagesInChapter: entry.pageCount,
|
|
chapterTitle: entry.title,
|
|
fixedSpread: nil
|
|
)
|
|
}
|
|
}
|
|
let chapters = bookPageMap.entries.map { entry in
|
|
EPUBChapterInfo(
|
|
spineIndex: entry.spineIndex,
|
|
title: entry.title,
|
|
pageCount: entry.pageCount
|
|
)
|
|
}
|
|
return (pages, chapters)
|
|
}
|
|
|
|
private func applyFullPageMapReplacement(
|
|
_ newPageMap: RDEPUBBookPageMap,
|
|
readerView: RDReaderView,
|
|
controller: RDEPUBReaderController
|
|
) {
|
|
let currentLocation = locationCoordinator.currentVisibleLocation()
|
|
|
|
context.pendingFullPageMap = nil
|
|
context.textBook = nil
|
|
context.bookPageMap = newPageMap
|
|
context.replaceActiveSnapshot(makeSnapshot(from: newPageMap))
|
|
|
|
paginationState.activePageMap = newPageMap
|
|
paginationState.pendingFullPageMap = nil
|
|
paginationState.source = .fullReplacement
|
|
navigationStateMachine.transition(to: .presentingWindow)
|
|
|
|
if let currentLocation {
|
|
let newPageNumber = controller.pageNumber(for: currentLocation) ?? (readerView.currentPage + 1)
|
|
let newPage = max(0, newPageNumber - 1)
|
|
readerView.reloadPageCountOnly()
|
|
if newPage != readerView.currentPage {
|
|
readerView.transitionToPage(pageNum: newPage, animated: false)
|
|
}
|
|
} else {
|
|
readerView.reloadPageCountOnly()
|
|
}
|
|
|
|
if let currentLocation,
|
|
context.normalizedSpineIndex(for: currentLocation) != nil,
|
|
let activeSession = jumpSessionManager.activeSession {
|
|
let candidateIndices = Set(newPageMap.entries.map { $0.spineIndex })
|
|
let protectedIndices = activeSession.protectedSpineIndices
|
|
if protectedIndices.isSubset(of: candidateIndices) {
|
|
jumpSessionManager.endSession(.coverageComplete)
|
|
}
|
|
}
|
|
}
|
|
}
|