Files
ReadViewSDK/Sources/RDReaderView/EPUBCore/RDEPUBReadingSession.swift
T
shenandshen 54798ba578 refactor: 添加中文注释 + 优化模块结构
- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级)
- 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行)
- 根目录翻页容器文件移入 ReaderView/ 目录
- Resources/ 移入 EPUBCore/Resources/(与使用者归属一致)
- RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖)
- RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层)
- 更新 podspec 资源路径
2026-05-25 10:19:14 +08:00

366 lines
14 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.
// RDEPUBReadingSession.swift
// EPUB 阅读会话(状态机)
// 管理阅读器的运行时状态:导航状态(initializing/loading/idle/jumping/moving/repaginating)、
// 活跃页面列表、待应用的分页快照、挂起的导航请求、当前视口和阅读上下文。
// 提供分页快照生成、位置计算、导航队列、阅读上下文更新等核心业务方法。
import Foundation
/// EPUB 阅读会话,管理导航状态、页面列表和阅读进度
/// 状态机:initializing → loading → idle ↔ jumping/moving/repaginating
public final class RDEPUBReadingSession {
/// 分页快照类型别名(页面数组 + 章节信息数组)
public typealias PaginationSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo])
/// 关联的出版物
public let publication: RDEPUBPublication
/// 当前导航状态
public private(set) var navigatorState: RDEPUBNavigatorState = .initializing
/// 当前生效的页面列表
public private(set) var activePages: [EPUBPage] = []
/// 当前生效的章节信息列表
public private(set) var activeChapters: [EPUBChapterInfo] = []
/// 暂存的待应用页面列表(分页计算完成后暂存,等状态允许时才生效)
public private(set) var stagedPages: [EPUBPage]?
/// 暂存的待应用章节信息列表
public private(set) var stagedChapters: [EPUBChapterInfo]?
/// 暂存快照对应的恢复位置
public private(set) var stagedRestoreLocation: RDEPUBLocation?
/// 挂起的导航目标位置(用于 fragment 精确定位)
public private(set) var pendingNavigationLocation: RDEPUBLocation?
/// 挂起的导航目标页码
public private(set) var pendingNavigationPageNum: Int?
/// 当前视口信息
public private(set) var currentViewport: RDEPUBViewport?
/// 当前阅读上下文(位置 + 视口 + 页码 + 章节)
public private(set) var currentReadingContext: RDEPUBReadingContext?
public init(publication: RDEPUBPublication) {
self.publication = publication
}
/// 资源解析器快捷访问
public var resourceResolver: RDEPUBResourceResolver {
publication.resourceResolver
}
/// 状态机转换
public func transition(to state: RDEPUBNavigatorState) {
navigatorState = state
}
/// 重置所有运行时状态(回到 initializing
public func resetRuntimeState() {
navigatorState = .initializing
activePages = []
activeChapters = []
clearPendingNavigation()
clearStagedSnapshot()
currentViewport = nil
currentReadingContext = nil
}
/// 设置当前生效的分页快照
public func setActiveSnapshot(_ snapshot: PaginationSnapshot) {
activePages = snapshot.pages
activeChapters = snapshot.chapters
}
/// 暂存分页快照(待状态允许时才生效)
public func stageSnapshot(_ snapshot: PaginationSnapshot, restoreLocation: RDEPUBLocation?) {
stagedPages = snapshot.pages
stagedChapters = snapshot.chapters
stagedRestoreLocation = restoreLocation
}
/// 获取暂存的分页快照
public func stagedSnapshot() -> PaginationSnapshot? {
guard let stagedPages, let stagedChapters else {
return nil
}
return (stagedPages, stagedChapters)
}
/// 如果状态稳定且有暂存快照,则消费并返回(原子操作)
public func consumeStagedSnapshotIfAllowed() -> (snapshot: PaginationSnapshot, restoreLocation: RDEPUBLocation?)? {
guard navigatorState.isStableForSnapshotApplication,
let stagedPages,
let stagedChapters else {
return nil
}
let restoreLocation = stagedRestoreLocation
clearStagedSnapshot()
return ((stagedPages, stagedChapters), restoreLocation)
}
/// 清除暂存快照
public func clearStagedSnapshot() {
stagedPages = nil
stagedChapters = nil
stagedRestoreLocation = nil
}
/// 清除挂起的导航请求
public func clearPendingNavigation() {
pendingNavigationLocation = nil
pendingNavigationPageNum = nil
}
/// 检查指定页码和 spine 索引是否有挂起的导航位置
public func pendingLocation(forPageNumber pageNumber: Int, spineIndex: Int?) -> RDEPUBLocation? {
guard pendingNavigationPageNum == pageNumber,
let pendingNavigationLocation else {
return nil
}
guard let spineIndex else {
return pendingNavigationLocation
}
let pageHref = resourceResolver.href(forSpineIndex: spineIndex)
guard resourceResolver.normalizedHref(pageHref ?? "") == resourceResolver.normalizedHref(pendingNavigationLocation.href) else {
return nil
}
return pendingNavigationLocation
}
/// 判断指定页面是否包含给定的 spine 索引(固定版式需检查 spread 内所有资源)
public func pageContains(spineIndex: Int, in page: EPUBPage) -> Bool {
if let fixedSpread = page.fixedSpread {
return fixedSpread.resources.contains(where: { $0.spineIndex == spineIndex })
}
return page.spineIndex == spineIndex
}
/// 根据页面信息生成回退位置(用于无精确位置时的兜底)
public func fallbackLocation(for page: EPUBPage, bookIdentifier: String?) -> RDEPUBLocation? {
if let fixedSpread = page.fixedSpread {
return RDEPUBLocation(
bookIdentifier: bookIdentifier,
href: fixedSpread.primaryResource.href,
progression: 0,
lastProgression: 1
)
}
guard let href = resourceResolver.href(forSpineIndex: page.spineIndex) else {
return nil
}
let progression: Double
if page.totalPagesInChapter <= 1 {
progression = 0
} else {
progression = Double(page.pageIndexInChapter) / Double(page.totalPagesInChapter - 1)
}
return RDEPUBLocation(
bookIdentifier: bookIdentifier,
href: href,
progression: progression,
lastProgression: progression
)
}
/// 获取当前可见页面对应的位置
public func currentVisibleLocation(currentPageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? {
guard currentPageNumber > 0,
activePages.indices.contains(currentPageNumber - 1) else {
return nil
}
return fallbackLocation(for: activePages[currentPageNumber - 1], bookIdentifier: bookIdentifier)
}
/// 根据位置获取初始 spine 索引
public func initialSpineIndex(for location: RDEPUBLocation?) -> Int {
guard let location,
let normalizedHref = resourceResolver.normalizedHref(location.href),
let spineIndex = publication.spine.firstIndex(where: { resourceResolver.normalizedHref($0.href) == normalizedHref }) else {
return 0
}
return spineIndex
}
/// 根据位置计算对应的页面索引(支持固定版式和重排模式)
public func pageIndex(for location: RDEPUBLocation, bookIdentifier: String?) -> Int? {
guard location.bookIdentifier == nil || location.bookIdentifier == bookIdentifier else {
return nil
}
guard let targetHref = resourceResolver.normalizedHref(location.href) else {
return nil
}
if publication.layout == .fixed {
return activePages.firstIndex(where: { page in
if let fixedSpread = page.fixedSpread {
return fixedSpread.contains(normalizedHref: targetHref, normalizer: { self.resourceResolver.normalizedHref($0) })
}
guard let href = self.resourceResolver.href(forSpineIndex: page.spineIndex) else {
return false
}
return self.resourceResolver.normalizedHref(href) == targetHref
})
}
guard let spineIndex = publication.spine.firstIndex(where: { resourceResolver.normalizedHref($0.href) == targetHref }) else {
return nil
}
let candidates = activePages.enumerated().filter { $0.element.spineIndex == spineIndex }
guard !candidates.isEmpty else {
return nil
}
if candidates.count == 1 {
return candidates[0].offset
}
let navigationProgression = location.navigationProgression
let localIndex = min(
candidates.count - 1,
max(0, Int(round(navigationProgression * Double(candidates.count - 1))))
)
return candidates[localIndex].offset
}
/// 排队导航到指定位置:规范化位置、查找页面索引、设置挂起导航、切换到 jumping 状态
public func queueNavigation(
to location: RDEPUBLocation,
relativeToSpineIndex spineIndex: Int? = nil,
bookIdentifier: String?
) -> Int? {
guard let normalizedLocation = resourceResolver.normalizedLocation(
location,
relativeToSpineIndex: spineIndex,
bookIdentifier: bookIdentifier
), let pageIndex = pageIndex(for: normalizedLocation, bookIdentifier: bookIdentifier) else {
return nil
}
pendingNavigationLocation = normalizedLocation.fragment == nil ? nil : normalizedLocation
pendingNavigationPageNum = normalizedLocation.fragment == nil ? nil : pageIndex + 1
transition(to: .jumping)
return pageIndex + 1
}
/// 更新阅读上下文:规范化位置、设置视口和阅读上下文、清除挂起导航、回到 idle 状态
public func updateReadingContext(
pageNumber: Int,
location: RDEPUBLocation,
spineIndex: Int,
chapterIndex: Int?,
bookIdentifier: String?
) {
let normalizedLocation = resourceResolver.normalizedLocation(
location,
relativeToSpineIndex: spineIndex,
bookIdentifier: bookIdentifier
) ?? RDEPUBLocation(
bookIdentifier: bookIdentifier,
href: location.href,
progression: location.progression,
lastProgression: location.lastProgression,
fragment: location.fragment
)
let resource = RDEPUBViewportResource(
href: normalizedLocation.href,
spineIndex: spineIndex,
progression: normalizedLocation.progression,
lastProgression: normalizedLocation.lastProgression,
fragment: normalizedLocation.fragment
)
let viewport = RDEPUBViewport(
resources: [resource],
visiblePageNumber: pageNumber,
chapterIndex: chapterIndex,
isFixedLayout: publication.layout == .fixed
)
currentViewport = viewport
currentReadingContext = RDEPUBReadingContext(
location: normalizedLocation,
viewport: viewport,
pageNumber: pageNumber,
chapterIndex: chapterIndex
)
if pendingNavigationPageNum == pageNumber {
clearPendingNavigation()
}
if navigatorState == .jumping || navigatorState == .moving {
transition(to: .idle)
}
}
/// 获取当前阅读位置(优先从阅读上下文获取,回退到可见位置)
public func currentReadingLocation(bookIdentifier: String?) -> RDEPUBLocation? {
currentReadingContext?.location ?? currentVisibleLocation(currentPageNumber: currentViewport?.visiblePageNumber ?? 0, bookIdentifier: bookIdentifier)
}
/// 根据分页计数和偏好设置生成分页快照
/// 固定版式按 spread 分页,重排模式按 spine 索引逐章分页
public func makePaginationSnapshot(
pageCounts: [Int],
preferences: RDEPUBPreferences,
layoutContext: RDEPUBNavigatorLayoutContext
) -> PaginationSnapshot {
if publication.layout == .fixed {
let spreads = publication.makeFixedSpreads(
preferences: preferences,
viewportSize: layoutContext.viewportSize
)
let chapters = spreads.enumerated().map { spreadIndex, spread in
EPUBChapterInfo(
spineIndex: spread.primaryResource.spineIndex,
title: spread.primaryResource.title,
pageCount: 1
)
}
let pages = spreads.enumerated().map { spreadIndex, spread in
EPUBPage(
spineIndex: spread.primaryResource.spineIndex,
chapterIndex: spreadIndex,
pageIndexInChapter: 0,
totalPagesInChapter: 1,
chapterTitle: spread.primaryResource.title,
fixedSpread: spread
)
}
return (pages, chapters)
}
var pages: [EPUBPage] = []
var chapters: [EPUBChapterInfo] = []
for (spineIndex, pageCount) in pageCounts.enumerated() {
guard pageCount > 0, spineIndex < publication.spine.count else { continue }
let chapterIndex = chapters.count
let title = publication.spine[spineIndex].title
chapters.append(
EPUBChapterInfo(
spineIndex: spineIndex,
title: title,
pageCount: pageCount
)
)
for pageIndex in 0..<pageCount {
pages.append(
EPUBPage(
spineIndex: spineIndex,
chapterIndex: chapterIndex,
pageIndexInChapter: pageIndex,
totalPagesInChapter: pageCount,
chapterTitle: title,
fixedSpread: nil
)
)
}
}
return (pages, chapters)
}
}