ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift
shenlei 6ed3fcb071 feat: 设置面板与章节运行时联动优化
- 设置页打开时仅重新计算当前章节预览,关闭后触发完整补全
- 新增 preview 加载优先级,支持设置页内当前章预览
- 添加设置页防抖机制,合并连续字号/行距变更
- 支持下滑手势关闭设置页,通过 presentation delegate 通知运行时
- 重新分页时跳过导航和尺寸校验,避免状态冲突
- currentTextPageSize 线程安全优化,后台线程使用缓存尺寸
- 更新 CocoaPods 依赖

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 09:47:50 +08:00

401 lines
16 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
/// - parserpublicationtextBookpages
/// - UI configurationbrightness
/// - persistence
/// - 便renderStylelayoutConfig
/// - 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?
///
/// nil ChapterRuntimeStore 访
var textBook: RDEPUBTextBook?
/// 100KB/1000 NSAttributedString
var bookPageMap: RDEPUBBookPageMap?
///
var activeBookmarks: [RDEPUBBookmark] = []
///
var activeHighlights: [RDEPUBHighlight] = []
///
var currentBookIdentifier: String?
///
var paginationToken = UUID()
/// Web
var paginator: RDEPUBPaginator?
///
var searchState: RDEPUBSearchState?
/// BookPageMap
/// map
var pendingFullPageMap: RDEPUBBookPageMap?
///
var lastTextPaginationPageSize: CGSize?
/// OperationQueue
var lastMetadataParseWallClockMs: Int = 0
/// 使
var lastMetadataParseConcurrency: Int = 0
/// selectionState
var currentSelection: RDEPUBSelection? {
get { selectionState.selection }
set {
if let newValue, !newValue.isEmpty {
selectionState = .selected(newValue)
} else {
selectionState = .idle
}
}
}
///
var selectionState: RDEPUBSelectionState = .idle
// 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 {
if Thread.isMainThread {
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
}
}
let viewportSize = currentLayoutContext().viewportSize
if viewportSize.width > 0, viewportSize.height > 0 {
return viewportSize
}
} else if let lastTextPaginationPageSize,
lastTextPaginationPageSize.width > 0,
lastTextPaginationPageSize.height > 0 {
return lastTextPaginationPageSize
} else {
let mainThreadSize = DispatchQueue.main.sync { [weak self] in
self?.currentTextPageSize() ?? .zero
}
if mainThreadSize.width > 0, mainThreadSize.height > 0 {
return mainThreadSize
}
}
return dependencies.environment.fallbackViewportSize
}
///
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: false,
avoidWidows: false,
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 makeChapterSummaryDiskCache() -> RDEPUBChapterSummaryDiskCache {
let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
?? FileManager.default.temporaryDirectory
let bookID = (currentBookIdentifier ?? "default").sha256Hex
let directory = cachesDirectory
.appendingPathComponent("RDEPUBChapterSummaryCache", isDirectory: true)
.appendingPathComponent(bookID, isDirectory: true)
return RDEPUBChapterSummaryDiskCache(cacheDirectory: directory)
}
func chapterCacheKey(forSpineIndex spineIndex: Int) -> RDEPUBChapterCacheKey {
let contentHash: String
if let parser,
let publication,
publication.spine.indices.contains(spineIndex) {
let href = publication.spine[spineIndex].href
contentHash = parser.htmlString(forRelativePath: href)?.sha256Hex ?? ""
} else {
contentHash = ""
}
return chapterCacheKey(
forSpineIndex: spineIndex,
precomputedContentHash: contentHash,
renderSignature: currentRenderSignature()
)
}
/// 使 contentHash HTML SHA-256
///
func chapterCacheKey(forSpineIndex spineIndex: Int, precomputedContentHash: String) -> RDEPUBChapterCacheKey {
chapterCacheKey(
forSpineIndex: spineIndex,
precomputedContentHash: precomputedContentHash,
renderSignature: currentRenderSignature()
)
}
/// 使 contentHash
/// live context
func chapterCacheKey(
forSpineIndex spineIndex: Int,
precomputedContentHash: String,
renderSignature: String
) -> RDEPUBChapterCacheKey {
RDEPUBChapterCacheKey(
bookID: currentBookIdentifier ?? "",
spineIndex: spineIndex,
renderSignature: renderSignature,
chapterContentHash: precomputedContentHash
)
}
///
func currentRenderSignature() -> String {
let style = currentTextRenderStyle()
let pageSize = currentTextPageSize()
let layoutConfig = currentTextLayoutConfig(pageSize: pageSize)
return [
style.font.fontName,
"\(style.font.pointSize)",
"\(configuration.lineHeightMultiple)",
"\(style.lineSpacing)",
layoutConfig.cacheSignature,
"\(RDEPUBChapterSummary.currentSchemaVersion)"
].joined(separator: "|")
}
func chapterSummary(forSpineIndex spineIndex: Int) -> RDEPUBChapterSummary? {
runtime?.summaryDiskCache.read(for: chapterCacheKey(forSpineIndex: spineIndex))
}
func normalizedSpineIndex(for location: RDEPUBLocation) -> Int? {
guard let publication else { return nil }
let normalizedLocation = publication.resourceResolver.normalizedLocation(
location,
relativeToSpineIndex: nil,
bookIdentifier: currentBookIdentifier
) ?? location
guard let normalizedHref = publication.resourceResolver.normalizedHref(normalizedLocation.href) else {
return nil
}
return publication.spine.firstIndex {
publication.resourceResolver.normalizedHref($0.href) == normalizedHref
}
}
///
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()
}
}