feat: 设置面板与章节运行时联动优化
- 设置页打开时仅重新计算当前章节预览,关闭后触发完整补全 - 新增 preview 加载优先级,支持设置页内当前章预览 - 添加设置页防抖机制,合并连续字号/行距变更 - 支持下滑手势关闭设置页,通过 presentation delegate 通知运行时 - 重新分页时跳过导航和尺寸校验,避免状态冲突 - currentTextPageSize 线程安全优化,后台线程使用缓存尺寸 - 更新 CocoaPods 依赖 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c64460988a
commit
6ed3fcb071
@ -43,7 +43,7 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||||
RDReaderView: 54205c82c62ea6c674b43c833642d2643c98c573
|
RDReaderView: 617ec758a5db3c10024acf83b110465ea8343b5b
|
||||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||||
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
||||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||||
|
|||||||
@ -27,9 +27,15 @@
|
|||||||
"ZIPFoundation": [
|
"ZIPFoundation": [
|
||||||
"~> 0.9"
|
"~> 0.9"
|
||||||
],
|
],
|
||||||
"DTCoreText": [],
|
"DTCoreText": [
|
||||||
"SnapKit": [],
|
|
||||||
"SSAlertSwift": []
|
],
|
||||||
|
"SnapKit": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"SSAlertSwift": [
|
||||||
|
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"requires_arc": true,
|
"requires_arc": true,
|
||||||
"swift_version": "5.10"
|
"swift_version": "5.10"
|
||||||
|
|||||||
2
ReadViewDemo/Pods/Manifest.lock
generated
2
ReadViewDemo/Pods/Manifest.lock
generated
@ -43,7 +43,7 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||||
RDReaderView: 54205c82c62ea6c674b43c833642d2643c98c573
|
RDReaderView: 617ec758a5db3c10024acf83b110465ea8343b5b
|
||||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||||
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
||||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||||
|
|||||||
2846
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
2846
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -295,6 +295,9 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
|||||||
updateCurrentSelection(nil)
|
updateCurrentSelection(nil)
|
||||||
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
||||||
|
|
||||||
|
// 如果正在重新分页,跳过后续操作,避免状态冲突
|
||||||
|
guard !isRepaginating else { return }
|
||||||
|
|
||||||
// 用户开始导航时,应用后台解析完成的完整 map
|
// 用户开始导航时,应用后台解析完成的完整 map
|
||||||
let previousCurrentPage = readerView.currentPage
|
let previousCurrentPage = readerView.currentPage
|
||||||
runtime.applyPendingFullPageMapIfNeeded()
|
runtime.applyPendingFullPageMapIfNeeded()
|
||||||
@ -362,10 +365,17 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isReconcilingTextPaginationSize = true
|
isReconcilingTextPaginationSize = true
|
||||||
DispatchQueue.main.async { [weak self] in
|
// 使用防抖延迟,避免连续快速点击字号时重复触发
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.isReconcilingTextPaginationSize = false
|
self.isReconcilingTextPaginationSize = false
|
||||||
guard (self.textBook != nil || self.readerContext.bookPageMap != nil), !self.isRepaginating else { return }
|
guard (self.textBook != nil || self.readerContext.bookPageMap != nil), !self.isRepaginating else { return }
|
||||||
|
// 再次检查尺寸是否仍然变化,避免过期的重新分页
|
||||||
|
let currentSize = self.readerView.resolvedSinglePageSize(pageNum: self.readerView.currentPage)
|
||||||
|
guard currentSize.width > 0, currentSize.height > 0 else { return }
|
||||||
|
let stillChanged = abs(currentSize.width - self.lastTextPaginationPageSize!.width) > 0.5
|
||||||
|
|| abs(currentSize.height - self.lastTextPaginationPageSize!.height) > 0.5
|
||||||
|
guard stillChanged else { return }
|
||||||
self.repaginatePreservingCurrentLocation()
|
self.repaginatePreservingCurrentLocation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ final class RDEPUBChapterLoader {
|
|||||||
|
|
||||||
enum LoadPriority {
|
enum LoadPriority {
|
||||||
case navigation // 前台导航:用户主动跳章,完成后检查导航目标队列
|
case navigation // 前台导航:用户主动跳章,完成后检查导航目标队列
|
||||||
|
case preview // 设置预览:仅构建当前章并回调,不参与导航目标消费
|
||||||
case prefetch // 后台预取:±1 相邻章,完成后仅回填缓存 + 刷新快照
|
case prefetch // 后台预取:±1 相邻章,完成后仅回填缓存 + 刷新快照
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +102,12 @@ final class RDEPUBChapterLoader {
|
|||||||
completion(.success(chapter))
|
completion(.success(chapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .preview:
|
||||||
|
store.markBuilding(false)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(chapter))
|
||||||
|
}
|
||||||
|
|
||||||
case .prefetch:
|
case .prefetch:
|
||||||
// 后台预取:仅回填缓存,标记预取目标完成
|
// 后台预取:仅回填缓存,标记预取目标完成
|
||||||
// 不触发跳章,不检查导航目标队列
|
// 不触发跳章,不检查导航目标队列
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import UIKit
|
|||||||
/// - 同步工具栏的主题和状态
|
/// - 同步工具栏的主题和状态
|
||||||
/// - 弹出设置面板和目录面板
|
/// - 弹出设置面板和目录面板
|
||||||
/// - 处理返回按钮的关闭逻辑
|
/// - 处理返回按钮的关闭逻辑
|
||||||
final class RDEPUBReaderChromeCoordinator {
|
final class RDEPUBReaderChromeCoordinator: NSObject, UIAdaptivePresentationControllerDelegate {
|
||||||
private unowned let context: RDEPUBReaderContext
|
private unowned let context: RDEPUBReaderContext
|
||||||
|
|
||||||
init(context: RDEPUBReaderContext) {
|
init(context: RDEPUBReaderContext) {
|
||||||
@ -19,6 +19,13 @@ final class RDEPUBReaderChromeCoordinator {
|
|||||||
context.controller
|
context.controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||||
|
|
||||||
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
// 设置页面被用户下滑关闭时
|
||||||
|
context.runtime?.settingsPanelDidDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
/// 创建顶部工具栏视图,绑定返回、搜索和书签切换回调。
|
/// 创建顶部工具栏视图,绑定返回、搜索和书签切换回调。
|
||||||
func makeTopToolView() -> RDEPUBReaderTopToolView {
|
func makeTopToolView() -> RDEPUBReaderTopToolView {
|
||||||
let toolView = RDEPUBReaderTopToolView()
|
let toolView = RDEPUBReaderTopToolView()
|
||||||
@ -110,6 +117,10 @@ final class RDEPUBReaderChromeCoordinator {
|
|||||||
func presentSettings() {
|
func presentSettings() {
|
||||||
guard let controller else { return }
|
guard let controller else { return }
|
||||||
guard controller.configuration.showsSettingsPanel else { return }
|
guard controller.configuration.showsSettingsPanel else { return }
|
||||||
|
|
||||||
|
// 通知 Runtime 设置页面即将打开
|
||||||
|
context.runtime?.settingsPanelWillAppear()
|
||||||
|
|
||||||
let settingsController = RDEPUBReaderSettingsViewController(
|
let settingsController = RDEPUBReaderSettingsViewController(
|
||||||
configuration: controller.configuration,
|
configuration: controller.configuration,
|
||||||
brightness: controller.currentBrightness
|
brightness: controller.currentBrightness
|
||||||
@ -135,9 +146,14 @@ final class RDEPUBReaderChromeCoordinator {
|
|||||||
settingsController.onThemeChange = { [weak controller] theme in
|
settingsController.onThemeChange = { [weak controller] theme in
|
||||||
controller?.updateConfiguration { $0.theme = theme }
|
controller?.updateConfiguration { $0.theme = theme }
|
||||||
}
|
}
|
||||||
|
settingsController.onDismiss = { [weak self] in
|
||||||
|
// 通知 Runtime 设置页面已关闭
|
||||||
|
self?.context.runtime?.settingsPanelDidDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
let navigationController = UINavigationController(rootViewController: settingsController)
|
let navigationController = UINavigationController(rootViewController: settingsController)
|
||||||
navigationController.modalPresentationStyle = .pageSheet
|
navigationController.modalPresentationStyle = .pageSheet
|
||||||
|
navigationController.presentationController?.delegate = self
|
||||||
controller.present(navigationController, animated: true)
|
controller.present(navigationController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -122,14 +122,31 @@ final class RDEPUBReaderContext {
|
|||||||
|
|
||||||
/// 获取当前文本排版的单页尺寸,优先从 readerView 解析,兜底用布局上下文。
|
/// 获取当前文本排版的单页尺寸,优先从 readerView 解析,兜底用布局上下文。
|
||||||
func currentTextPageSize() -> CGSize {
|
func currentTextPageSize() -> CGSize {
|
||||||
let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil
|
if Thread.isMainThread {
|
||||||
if let readerView, let pageNum {
|
let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil
|
||||||
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
|
if let readerView, let pageNum {
|
||||||
if resolvedSize.width > 0, resolvedSize.height > 0 {
|
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
|
||||||
return resolvedSize
|
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 currentLayoutContext().viewportSize
|
return dependencies.environment.fallbackViewportSize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据当前配置生成文本渲染样式(字体、行距、颜色)。
|
/// 根据当前配置生成文本渲染样式(字体、行距、颜色)。
|
||||||
|
|||||||
@ -27,6 +27,19 @@ final class RDEPUBReaderRuntime {
|
|||||||
lazy var backgroundCoverageStore = RDEPUBBackgroundCoverageStore(context: context)
|
lazy var backgroundCoverageStore = RDEPUBBackgroundCoverageStore(context: context)
|
||||||
lazy var reconciliationCoordinator = RDEPUBPageMapReconciliationCoordinator(context: context)
|
lazy var reconciliationCoordinator = RDEPUBPageMapReconciliationCoordinator(context: context)
|
||||||
|
|
||||||
|
/// 设置页面是否打开
|
||||||
|
var isSettingsPanelOpen: Bool = false
|
||||||
|
|
||||||
|
/// 标记需要在设置页面关闭后触发完整补全
|
||||||
|
var needsFullRepaginationAfterSettingsClose: Bool = false
|
||||||
|
|
||||||
|
/// 设置页内当前章预览的代次,用于丢弃过期结果
|
||||||
|
private var settingsPreviewGeneration: Int = 0
|
||||||
|
/// 设置页内 preview 防抖任务,只保留最后一次请求。
|
||||||
|
private var pendingSettingsPreviewWorkItem: DispatchWorkItem?
|
||||||
|
/// 设置页内 preview 防抖延迟,合并连续点击。
|
||||||
|
private let settingsPreviewDebounceDelay: TimeInterval = 0.2
|
||||||
|
|
||||||
init(context: RDEPUBReaderContext) {
|
init(context: RDEPUBReaderContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
}
|
}
|
||||||
@ -330,6 +343,9 @@ final class RDEPUBReaderRuntime {
|
|||||||
let readerView = context.readerView,
|
let readerView = context.readerView,
|
||||||
let controller = context.controller else { return }
|
let controller = context.controller else { return }
|
||||||
|
|
||||||
|
// 如果正在重新分页,跳过本次检查,避免状态冲突
|
||||||
|
guard !controller.isRepaginating else { return }
|
||||||
|
|
||||||
// 使用协调器判断是否允许接管
|
// 使用协调器判断是否允许接管
|
||||||
let decision = reconciliationCoordinator.evaluateTakeover(
|
let decision = reconciliationCoordinator.evaluateTakeover(
|
||||||
candidatePageMap: pendingMap,
|
candidatePageMap: pendingMap,
|
||||||
@ -401,7 +417,111 @@ final class RDEPUBReaderRuntime {
|
|||||||
|
|
||||||
/// 重新分页并保持当前阅读位置不变
|
/// 重新分页并保持当前阅读位置不变
|
||||||
func repaginatePreservingCurrentLocation() {
|
func repaginatePreservingCurrentLocation() {
|
||||||
paginationCoordinator.repaginatePreservingCurrentLocation()
|
// 如果设置页面打开,只计算当前章节(热区)
|
||||||
|
if isSettingsPanelOpen {
|
||||||
|
needsFullRepaginationAfterSettingsClose = true
|
||||||
|
paginationCoordinator.cancelActiveMetadataParseWork()
|
||||||
|
scheduleSettingsPreviewRepagination()
|
||||||
|
} else {
|
||||||
|
paginationCoordinator.repaginatePreservingCurrentLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 防抖调度设置页内的当前章 preview,只保留最后一次请求。
|
||||||
|
private func scheduleSettingsPreviewRepagination() {
|
||||||
|
pendingSettingsPreviewWorkItem?.cancel()
|
||||||
|
settingsPreviewGeneration += 1
|
||||||
|
let previewGeneration = settingsPreviewGeneration
|
||||||
|
|
||||||
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self,
|
||||||
|
self.isSettingsPanelOpen,
|
||||||
|
previewGeneration == self.settingsPreviewGeneration else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.pendingSettingsPreviewWorkItem = nil
|
||||||
|
self.chapterRuntimeStore.invalidateAllForSettingsChange()
|
||||||
|
self.repaginateCurrentChapterOnly(previewGeneration: previewGeneration)
|
||||||
|
}
|
||||||
|
pendingSettingsPreviewWorkItem = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(
|
||||||
|
deadline: .now() + settingsPreviewDebounceDelay,
|
||||||
|
execute: workItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 只重新计算当前章节(设置页面打开时使用)
|
||||||
|
private func repaginateCurrentChapterOnly(previewGeneration: Int) {
|
||||||
|
guard let bookPageMap = context.bookPageMap,
|
||||||
|
let readerView = context.readerView else { return }
|
||||||
|
|
||||||
|
let currentPageNumber = readerView.currentPage + 1
|
||||||
|
guard let currentSpineIndex = bookPageMap.spineIndex(forAbsolutePage: currentPageNumber - 1) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let previewLocation = locationCoordinator.currentVisibleLocation() ?? context.persistenceLocation()
|
||||||
|
|
||||||
|
RDEPUBBackgroundTrace.log(
|
||||||
|
"Runtime",
|
||||||
|
"repaginateCurrentChapterOnly spine=\(currentSpineIndex) generation=\(previewGeneration)"
|
||||||
|
)
|
||||||
|
|
||||||
|
chapterLoader.loadChapter(
|
||||||
|
spineIndex: currentSpineIndex,
|
||||||
|
store: chapterRuntimeStore,
|
||||||
|
priority: .preview
|
||||||
|
) { [weak self] result in
|
||||||
|
guard let self,
|
||||||
|
self.isSettingsPanelOpen,
|
||||||
|
previewGeneration == self.settingsPreviewGeneration,
|
||||||
|
let readerView = self.context.readerView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let chapter):
|
||||||
|
let partialMap = self.makePartialPageMap(from: [chapter])
|
||||||
|
self.context.bookPageMap = partialMap
|
||||||
|
self.context.replaceActiveSnapshot(self.makeSnapshot(from: partialMap))
|
||||||
|
readerView.reloadData()
|
||||||
|
|
||||||
|
if let previewLocation,
|
||||||
|
self.locationCoordinator.restoreReadingLocation(previewLocation, animated: false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
readerView.transitionToPage(pageNum: 0, animated: false)
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
RDEPUBBackgroundTrace.log(
|
||||||
|
"Runtime",
|
||||||
|
"repaginateCurrentChapterOnly FAILED spine=\(currentSpineIndex) generation=\(previewGeneration) error=\(error)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置页面即将打开
|
||||||
|
func settingsPanelWillAppear() {
|
||||||
|
pendingSettingsPreviewWorkItem?.cancel()
|
||||||
|
pendingSettingsPreviewWorkItem = nil
|
||||||
|
isSettingsPanelOpen = true
|
||||||
|
needsFullRepaginationAfterSettingsClose = false
|
||||||
|
settingsPreviewGeneration += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置页面已关闭
|
||||||
|
func settingsPanelDidDisappear() {
|
||||||
|
pendingSettingsPreviewWorkItem?.cancel()
|
||||||
|
pendingSettingsPreviewWorkItem = nil
|
||||||
|
isSettingsPanelOpen = false
|
||||||
|
settingsPreviewGeneration += 1
|
||||||
|
// 如果在设置页面期间有配置变化,触发完整补全
|
||||||
|
if needsFullRepaginationAfterSettingsClose {
|
||||||
|
needsFullRepaginationAfterSettingsClose = false
|
||||||
|
RDEPUBBackgroundTrace.log("Runtime", "settingsPanelDidDisappear: triggering full repagination")
|
||||||
|
paginationCoordinator.repaginatePreservingCurrentLocation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 刷新当前可见内容,保持阅读位置不变
|
/// 刷新当前可见内容,保持阅读位置不变
|
||||||
|
|||||||
@ -22,6 +22,8 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
|||||||
var onDisplayTypeChange: ((RDReaderView.DisplayType) -> Void)?
|
var onDisplayTypeChange: ((RDReaderView.DisplayType) -> Void)?
|
||||||
/// 主题变化回调
|
/// 主题变化回调
|
||||||
var onThemeChange: ((RDEPUBReaderTheme) -> Void)?
|
var onThemeChange: ((RDEPUBReaderTheme) -> Void)?
|
||||||
|
/// 设置页面关闭回调
|
||||||
|
var onDismiss: (() -> Void)?
|
||||||
|
|
||||||
// MARK: 主题预设枚举
|
// MARK: 主题预设枚举
|
||||||
|
|
||||||
@ -292,7 +294,9 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func doneAction() {
|
@objc private func doneAction() {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true) { [weak self] in
|
||||||
|
self?.onDismiss?()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func brightnessChanged(_ slider: UISlider) {
|
@objc private func brightnessChanged(_ slider: UISlider) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user