feat: 设置面板与章节运行时联动优化

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
shenlei 2026-06-17 09:47:50 +08:00
parent c64460988a
commit 6ed3fcb071
10 changed files with 1626 additions and 1430 deletions

View File

@ -43,7 +43,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
RDReaderView: 54205c82c62ea6c674b43c833642d2643c98c573
RDReaderView: 617ec758a5db3c10024acf83b110465ea8343b5b
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351

View File

@ -27,9 +27,15 @@
"ZIPFoundation": [
"~> 0.9"
],
"DTCoreText": [],
"SnapKit": [],
"SSAlertSwift": []
"DTCoreText": [
],
"SnapKit": [
],
"SSAlertSwift": [
]
},
"requires_arc": true,
"swift_version": "5.10"

View File

@ -43,7 +43,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
RDReaderView: 54205c82c62ea6c674b43c833642d2643c98c573
RDReaderView: 617ec758a5db3c10024acf83b110465ea8343b5b
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351

File diff suppressed because it is too large Load Diff

View File

@ -295,6 +295,9 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
updateCurrentSelection(nil)
reconcileTextPaginationSizeIfNeeded(for: pageNum)
//
guard !isRepaginating else { return }
// map
let previousCurrentPage = readerView.currentPage
runtime.applyPendingFullPageMapIfNeeded()
@ -362,10 +365,17 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
}
isReconcilingTextPaginationSize = true
DispatchQueue.main.async { [weak self] in
// 使
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
guard let self else { return }
self.isReconcilingTextPaginationSize = false
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()
}
}

View File

@ -16,6 +16,7 @@ final class RDEPUBChapterLoader {
enum LoadPriority {
case navigation //
case preview //
case prefetch // ±1 +
}
@ -101,6 +102,12 @@ final class RDEPUBChapterLoader {
completion(.success(chapter))
}
case .preview:
store.markBuilding(false)
DispatchQueue.main.async {
completion(.success(chapter))
}
case .prefetch:
//
//

View File

@ -8,7 +8,7 @@ import UIKit
/// -
/// -
/// -
final class RDEPUBReaderChromeCoordinator {
final class RDEPUBReaderChromeCoordinator: NSObject, UIAdaptivePresentationControllerDelegate {
private unowned let context: RDEPUBReaderContext
init(context: RDEPUBReaderContext) {
@ -19,6 +19,13 @@ final class RDEPUBReaderChromeCoordinator {
context.controller
}
// MARK: - UIAdaptivePresentationControllerDelegate
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
//
context.runtime?.settingsPanelDidDisappear()
}
///
func makeTopToolView() -> RDEPUBReaderTopToolView {
let toolView = RDEPUBReaderTopToolView()
@ -110,6 +117,10 @@ final class RDEPUBReaderChromeCoordinator {
func presentSettings() {
guard let controller else { return }
guard controller.configuration.showsSettingsPanel else { return }
// Runtime
context.runtime?.settingsPanelWillAppear()
let settingsController = RDEPUBReaderSettingsViewController(
configuration: controller.configuration,
brightness: controller.currentBrightness
@ -135,9 +146,14 @@ final class RDEPUBReaderChromeCoordinator {
settingsController.onThemeChange = { [weak controller] theme in
controller?.updateConfiguration { $0.theme = theme }
}
settingsController.onDismiss = { [weak self] in
// Runtime
self?.context.runtime?.settingsPanelDidDisappear()
}
let navigationController = UINavigationController(rootViewController: settingsController)
navigationController.modalPresentationStyle = .pageSheet
navigationController.presentationController?.delegate = self
controller.present(navigationController, animated: true)
}

View File

@ -122,6 +122,7 @@ final class RDEPUBReaderContext {
/// 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)
@ -129,7 +130,23 @@ final class RDEPUBReaderContext {
return resolvedSize
}
}
return currentLayoutContext().viewportSize
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
}
///

View File

@ -27,6 +27,19 @@ final class RDEPUBReaderRuntime {
lazy var backgroundCoverageStore = RDEPUBBackgroundCoverageStore(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) {
self.context = context
}
@ -330,6 +343,9 @@ final class RDEPUBReaderRuntime {
let readerView = context.readerView,
let controller = context.controller else { return }
//
guard !controller.isRepaginating else { return }
// 使
let decision = reconciliationCoordinator.evaluateTakeover(
candidatePageMap: pendingMap,
@ -401,8 +417,112 @@ final class RDEPUBReaderRuntime {
///
func 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()
}
}
///
func refreshVisibleContentPreservingLocation() {

View File

@ -22,6 +22,8 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
var onDisplayTypeChange: ((RDReaderView.DisplayType) -> Void)?
///
var onThemeChange: ((RDEPUBReaderTheme) -> Void)?
///
var onDismiss: (() -> Void)?
// MARK:
@ -292,7 +294,9 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
}
@objc private func doneAction() {
dismiss(animated: true)
dismiss(animated: true) { [weak self] in
self?.onDismiss?()
}
}
@objc private func brightnessChanged(_ slider: UISlider) {