Files
ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift
T
shenleiandClaude Opus 4.7 c76beed03c feat(reader): 实现大书快速进入与增量分页路径
- PaginationCoordinator: 新增 textReflowable 快速进入路径,支持增量章节构建
  与 staged 合并策略,避免整书一次性构建的主线程阻塞
- TextBookBuilder: 重构为支持单章构建与增量合并模式
- ChapterPageCounter: 优化分页计数逻辑,支持章节级分页缓存
- PageFrameFactory: 新增 CoreText 页面帧工厂,提升排版性能
- ReaderContext/ReaderRuntime: 适配增量分页状态管理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:16:57 +08:00

281 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import UIKit
/// 阅读器共享状态中心:所有 coordinator 通过 context 访问业务状态和便捷方法。
///
/// context 持有:
/// - 业务状态(parser、publication、textBook、pages 等)
/// - UI 配置(configuration、brightness
/// - 持久化策略(persistence
/// - 便捷方法(renderStyle、layoutConfig 等)
/// - 弱引用 controller(仅用于 UIKit 呈现操作)
final class RDEPUBReaderContext {
private let activityLock = NSLock()
private var lastUserNavigationTimestamp: CFAbsoluteTime = 0
// MARK: - 引用
/// 弱引用阅读器控制器,用于 UIKit 呈现操作。
weak var controller: RDEPUBReaderController?
/// 弱引用阅读器视图,用于布局和页面状态查询。
weak var readerView: RDReaderView?
/// 依赖注入容器,提供解析器、分页器等工厂方法。
var dependencies: RDEPUBReaderDependencies = .live
/// 便捷访问当前控制器的运行时协调器集合。
var runtime: RDEPUBReaderRuntime? {
controller?.runtime
}
// MARK: - 业务状态
/// EPUB 解析器实例。
var parser: RDEPUBParser?
/// 解析后的出版物模型。
var publication: RDEPUBPublication?
/// 当前阅读会话,管理页面和章节状态。
var readingSession: RDEPUBReadingSession?
/// 原生文本排版生成的图书模型(仅文本重排模式)。
var textBook: RDEPUBTextBook?
/// 当前书籍的所有书签。
var activeBookmarks: [RDEPUBBookmark] = []
/// 当前书籍的所有高亮标注。
var activeHighlights: [RDEPUBHighlight] = []
/// 当前打开书籍的唯一标识。
var currentBookIdentifier: String?
/// 分页操作令牌,用于取消过期的异步分页任务。
var paginationToken = UUID()
/// Web 内容分页计算器。
var paginator: RDEPUBPaginator?
/// 全文搜索状态。
var searchState: RDEPUBSearchState?
/// 上次文本分页时的页面尺寸,用于检测是否需要重新分页。
var lastTextPaginationPageSize: CGSize?
/// 当前用户文本选区。
var currentSelection: RDEPUBSelection?
// MARK: - 控制器状态(从 controller 下沉)
/// 阅读器配置(字号、字体、主题等)。
var configuration: RDEPUBReaderConfiguration = .default
/// 持久化策略,负责书签、高亮、阅读位置的存取。
var persistence: RDEPUBReaderPersistence?
/// 当前打开的 EPUB 文件 URL。
var epubURL: URL = URL(string: "about:blank")!
/// 是否正在重新分页。
var isRepaginating: Bool = false
/// 是否已完成首次加载。
var didStartInitialLoad: Bool = false
/// 是否为外部传入的纯文本图书。
var isExternalTextBook: Bool = false
/// 外部纯文本文件的 URL。
var textFileURL: URL?
/// 文本图书缓存,避免重复排版。
var textBookCache = RDEPUBTextBookCache()
// MARK: - 初始化
init(controller: RDEPUBReaderController) {
self.controller = controller
self.readerView = controller.readerView
}
// MARK: - 便捷方法
/// 根据当前 readerView 和控制器尺寸构建布局上下文。
func currentLayoutContext() -> RDEPUBNavigatorLayoutContext {
let containerSize = readerView?.bounds.size ?? .zero
let viewSize = controller?.view.bounds.size ?? containerSize
let resolvedSize = containerSize == .zero ? viewSize : containerSize
return RDEPUBNavigatorLayoutContext(
containerSize: resolvedSize,
pagesPerScreen: readerView?.pagesPerScreen ?? 1,
safeAreaInsets: controller?.view.safeAreaInsets ?? .zero,
userInterfaceIdiom: controller?.traitCollection.userInterfaceIdiom ?? .phone,
reflowableContentInsets: configuration.reflowableContentInsets
)
}
/// 根据当前配置生成阅读偏好设置。
func currentPreferences() -> RDEPUBPreferences {
configuration.makePreferences()
}
/// 获取当前文本排版的单页尺寸,优先从 readerView 解析,兜底用布局上下文。
func currentTextPageSize() -> CGSize {
let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil
if let readerView, let pageNum {
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
if resolvedSize.width > 0, resolvedSize.height > 0 {
return resolvedSize
}
}
return currentLayoutContext().viewportSize
}
/// 根据当前配置生成文本渲染样式(字体、行距、颜色)。
func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
let font = configuration.fontChoice.font(ofSize: configuration.fontSize)
let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4)
return RDEPUBTextRenderStyle(
font: font,
lineSpacing: lineSpacing,
textColor: configuration.theme.contentTextColor,
backgroundColor: configuration.theme.contentBackgroundColor
)
}
/// 根据页面尺寸和配置生成文本排版参数。
func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
return RDEPUBTextLayoutConfig(
frameWidth: max(pageSize.width, 1),
frameHeight: max(pageSize.height, 1),
edgeInsets: configuration.reflowableContentInsets,
numberOfColumns: configuration.numberOfColumns,
columnGap: configuration.columnGap,
avoidOrphans: true,
avoidWidows: true,
avoidPageBreakInsideEnabled: true,
hyphenation: true,
imageMaxHeightRatio: 0.85,
fallbackViewportSize: dependencies.environment.fallbackViewportSize
)
}
/// 获取当前配置对应的文本渲染器实例。
func resolvedTextRenderer() -> RDEPUBTextRenderer {
dependencies.makeTextRenderer(configuration.textRenderingEngine)
}
/// 当前活跃的页面列表。
var activePages: [EPUBPage] {
readingSession?.activePages ?? []
}
/// 当前活跃的章节信息列表。
var activeChapters: [EPUBChapterInfo] {
readingSession?.activeChapters ?? []
}
/// 屏幕亮度代理属性,读写均转发给系统环境。
var currentBrightness: CGFloat {
get { dependencies.environment.currentBrightness }
set { dependencies.environment.currentBrightness = newValue }
}
/// 用新快照替换当前活跃的分页快照。
func replaceActiveSnapshot(_ snapshot: RDEPUBReadingSession.PaginationSnapshot) {
readingSession?.setActiveSnapshot(snapshot)
}
/// 清除当前活跃快照,重置运行时状态。
func clearActiveSnapshot() {
readingSession?.resetRuntimeState()
}
/// 工厂方法:创建 EPUB 解析器。
func makeParser() -> RDEPUBParser {
dependencies.makeParser()
}
/// 工厂方法:创建 Web 内容分页计算器。
func makePaginator() -> RDEPUBPaginator {
dependencies.makePaginator()
}
/// 工厂方法:创建 EPUB 文本图书构建器。
func makeTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder {
dependencies.makeTextBookBuilder(resolvedTextRenderer(), textBookCache, layoutConfig)
}
/// 工厂方法:创建纯文本图书构建器。
func makePlainTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder {
dependencies.makePlainTextBookBuilder(resolvedTextRenderer(), layoutConfig)
}
/// 获取当前可见页面的阅读位置。
func currentVisibleLocation() -> RDEPUBLocation? {
controller?.currentVisibleLocation()
}
/// 从持久化存储加载上次保存的阅读位置。
func persistenceLocation() -> RDEPUBLocation? {
guard let currentBookIdentifier else { return nil }
return persistence?.loadLocation(for: currentBookIdentifier)
}
/// 将阅读位置持久化到存储。
func persist(location: RDEPUBLocation) {
guard let currentBookIdentifier else { return }
persistence?.saveLocation(location, for: currentBookIdentifier)
}
/// 记录最近一次用户翻页/跳转行为,用于后台分页让路。
func markUserNavigationActivity() {
activityLock.lock()
lastUserNavigationTimestamp = CFAbsoluteTimeGetCurrent()
activityLock.unlock()
}
/// 距离最近一次用户翻页/跳转已经过去的时间。
func secondsSinceLastUserNavigation() -> CFAbsoluteTime {
activityLock.lock()
let timestamp = lastUserNavigationTimestamp
activityLock.unlock()
guard timestamp > 0 else { return .greatestFiniteMagnitude }
return CFAbsoluteTimeGetCurrent() - timestamp
}
/// 根据规范化 href 获取文本章节数据。
func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? {
guard let textBook, let publication else { return nil }
let normalizedHref = publication.resourceResolver.normalizedHref(href) ?? href
return textBook.chapters.lazy
.first(where: { (publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == normalizedHref })
.flatMap { textBook.chapterData(for: $0.href) }
}
/// 显示加载指示器。
func showLoading() {
controller?.showLoading()
}
/// 隐藏加载指示器。
func hideLoading() {
controller?.hideLoading()
}
/// 将错误转发给控制器处理。
func handle(error: Error) {
controller?.handle(error: error)
}
/// 触发工具栏状态更新。
func updateReaderChrome() {
controller?.updateReaderChrome()
}
/// 刷新可见内容并保持当前阅读位置不变。
func refreshVisibleContentPreservingLocation() {
controller?.refreshVisibleContentPreservingLocation()
}
/// 恢复到指定阅读位置。
func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool {
controller?.restoreReadingLocation(location, animated: animated) ?? false
}
/// 重新分页并保持当前阅读位置。
func repaginatePreservingCurrentLocation() {
controller?.repaginatePreservingCurrentLocation()
}
/// 将当前配置应用到阅读器视图。
func applyReaderViewConfiguration() {
controller?.applyReaderViewConfiguration()
}
/// 同步书签按钮的 UI 状态。
func updateBookmarkChrome() {
controller?.updateBookmarkChrome()
}
}