From 9e9120701181f86e7294fa90ae8e107747298b1b Mon Sep 17 00:00:00 2001 From: shen <> Date: Sat, 4 Jul 2026 19:21:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20EPUB=20=E7=BF=BB=E9=A1=B5?= =?UTF-8?q?=E4=B8=8E=E5=90=8E=E5=8F=B0=E5=88=86=E9=A1=B5=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuildPipeline/RDEPUBTextBookCache.swift | 8 +- .../RDEPUBMetadataParseWorker.swift | 86 ++++++++++- .../RDEPUBPresentationRuntime.swift | 134 +++++++++++++++--- .../RDEPUBReaderEnvironment.swift | 29 +++- .../RDEPUBReaderPaginationCoordinator.swift | 17 +++ .../Paging/RDReaderPreloadController.swift | 18 ++- .../ReaderView/RDReaderView.swift | 14 +- 7 files changed, 274 insertions(+), 32 deletions(-) diff --git a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookCache.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookCache.swift index 71ef854..7f746c2 100644 --- a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookCache.swift +++ b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookCache.swift @@ -137,7 +137,7 @@ public final class RDEPUBTextBookCache { let fileURL = cacheDirectory.appendingPathComponent(key) guard FileManager.default.fileExists(atPath: fileURL.path) else { #if DEBUG - print("[Cache] load MISS key=\(key)") +// print("[Cache] load MISS key=\(key)") #endif return nil } @@ -148,7 +148,7 @@ public final class RDEPUBTextBookCache { from: data ) else { #if DEBUG - print("[Cache] load MISS key=\(key) (unarchive returned nil)") +// print("[Cache] load MISS key=\(key) (unarchive returned nil)") #endif return nil } @@ -157,12 +157,12 @@ public final class RDEPUBTextBookCache { result[chapter.href] = chapter.toCache() } #if DEBUG - print("[Cache] load HIT key=\(key) chapters=\(result.count)") +// print("[Cache] load HIT key=\(key) chapters=\(result.count)") #endif return result } catch { #if DEBUG - print("[Cache] load MISS key=\(key) error=\(error)") +// print("[Cache] load MISS key=\(key) error=\(error)") #endif return nil } diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBMetadataParseWorker.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBMetadataParseWorker.swift index 82918e2..654746f 100644 --- a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBMetadataParseWorker.swift +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBMetadataParseWorker.swift @@ -56,6 +56,8 @@ final class RDEPUBMetadataParseWorker { private let catalog: [(key: RDEPUBChapterCacheKey, spineIndex: Int, href: String, title: String)] + private let progressLogStride = 4 + init( context: RDEPUBReaderContext, cancellationController: RDEPUBMetadataParseCancellationController, @@ -116,19 +118,38 @@ final class RDEPUBMetadataParseWorker { func start(token: UUID, restoreLocation: RDEPUBLocation?) { let cancellationController = self.cancellationController - DispatchQueue.global(qos: .utility).async { [weak self] in - guard let self else { return } + DispatchQueue.global(qos: .utility).async { [self] in + RDEPUBBackgroundTrace.log( + "Metadata", + "workerDispatched token=\(token.uuidString)" + ) let context = self.context guard let context, context.controller != nil, !cancellationController.isCancelled, - context.paginationToken == token else { return } + context.paginationToken == token else { + RDEPUBBackgroundTrace.log( + "Metadata", + "workerAbortedBeforeStart reason=contextUnavailableOrTokenMismatch" + ) + return + } defer { context.runtime?.paginationCoordinator.finishMetadataParseCancellationController(cancellationController) } guard context.controller != nil, !cancellationController.isCancelled, - context.paginationToken == token else { return } + context.paginationToken == token else { + RDEPUBBackgroundTrace.log( + "Metadata", + "workerAbortedAfterStart reason=contextUnavailableOrTokenMismatch" + ) + return + } if let restoredPageMap = self.restoreBookPageMapIfPossible() { + RDEPUBBackgroundTrace.log( + "Metadata", + "restoreBookPageMapIfPossible hit totalChapters=\(restoredPageMap.totalChapters) totalPages=\(restoredPageMap.totalPages)" + ) DispatchQueue.main.async { guard context.paginationToken == token, context.controller != nil, @@ -175,13 +196,26 @@ final class RDEPUBMetadataParseWorker { let uncachedSpineIndices = prioritizedSpineIndices + RDEPUBBackgroundTrace.log( + "Metadata", + "waitingForInteractionCooldown elapsedSinceNavigation=\(String(format: "%.2f", context.secondsSinceLastUserNavigation())) uncached=\(uncachedSpineIndices.count)" + ) self.waitForReadingInteractionToSettle(cancellationController: cancellationController) guard !cancellationController.isCancelled, context.controller != nil, context.paginationToken == token else { + RDEPUBBackgroundTrace.log( + "Metadata", + "workerAbortedDuringCooldown" + ) return } + RDEPUBBackgroundTrace.log( + "Metadata", + "start totalBuildable=\(self.allBuildableIndices.count) cached=\(cachedSpineIndices.count) uncached=\(uncachedSpineIndices.count) concurrency=\(self.workerCount) refreshInterval=\(self.pageMapRefreshInterval)" + ) + let wallClockStart = CFAbsoluteTimeGetCurrent() var totalRenderMs: Double = 0 var totalWriteMs: Double = 0 @@ -283,6 +317,10 @@ final class RDEPUBMetadataParseWorker { resultLock.lock() parseState.summariesBySpineIndex[spineIndex] = renderResult parseState.totalResolvedCount += 1 + let resolvedCount = parseState.totalResolvedCount + let shouldLogProgress = resolvedCount == self.allBuildableIndices.count + || resolvedCount == cachedSpineIndices.count + 1 + || resolvedCount % self.progressLogStride == 0 if parseState.totalResolvedCount - parseState.lastAppliedCount >= refreshInterval || parseState.totalResolvedCount == self.allBuildableIndices.count { parseState.lastAppliedCount = parseState.totalResolvedCount @@ -290,6 +328,17 @@ final class RDEPUBMetadataParseWorker { } resultLock.unlock() + if shouldLogProgress { + let progressPercent = Self.progressPercent( + resolved: resolvedCount, + total: self.allBuildableIndices.count + ) + RDEPUBBackgroundTrace.log( + "Metadata", + "chapterReady spine=\(spineIndex) resolved=\(resolvedCount)/\(self.allBuildableIndices.count) progress=\(progressPercent)% pageCount=\(renderResult.pageCount)" + ) + } + if let snapshot { let mergeStart = CFAbsoluteTimeGetCurrent() let partialMap = self.buildPageMap(summaries: snapshot) @@ -297,6 +346,10 @@ final class RDEPUBMetadataParseWorker { timingLock.lock() totalMergeMs += mergeElapsed timingLock.unlock() + RDEPUBBackgroundTrace.log( + "Metadata", + "partialMap resolvedChapters=\(snapshot.count) totalPages=\(partialMap.totalPages) mergeMs=\(Int(mergeElapsed))" + ) DispatchQueue.main.async { guard context.paginationToken == token, context.controller != nil, @@ -314,6 +367,10 @@ final class RDEPUBMetadataParseWorker { timingLock.lock() failedChapters += 1 timingLock.unlock() + RDEPUBBackgroundTrace.log( + "Metadata", + "chapterFailed spine=\(spineIndex) retryScheduled=true error=\(String(describing: error))" + ) self.scheduleRetry( spineIndex: spineIndex, @@ -354,6 +411,10 @@ final class RDEPUBMetadataParseWorker { let finalMergeStart = CFAbsoluteTimeGetCurrent() let pageMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex) let finalMergeMs = Int((CFAbsoluteTimeGetCurrent() - finalMergeStart) * 1000) + RDEPUBBackgroundTrace.log( + "Metadata", + "finish resolved=\(parseState.summariesBySpineIndex.count)/\(self.allBuildableIndices.count) totalPages=\(pageMap.totalPages) elapsedMs=\(wallClockMs) renderMs=\(renderTotal) writeMs=\(writeTotal) mergeMs=\(mergeTotal + finalMergeMs) failed=\(failed)" + ) if let coverageStore = context.runtime?.backgroundCoverageStore { let resolvedSpineIndices = Set(parseState.summariesBySpineIndex.keys) @@ -407,10 +468,18 @@ final class RDEPUBMetadataParseWorker { cancellationController: RDEPUBMetadataParseCancellationController ) { guard retryCount < Self.maxRetryCount else { + RDEPUBBackgroundTrace.log( + "Metadata", + "retryAborted spine=\(spineIndex) retryCount=\(retryCount)" + ) return } let delay = Self.retryDelays[min(retryCount, Self.retryDelays.count - 1)] + RDEPUBBackgroundTrace.log( + "Metadata", + "retryScheduled spine=\(spineIndex) retryCount=\(retryCount + 1) delayMs=\(Int(delay * 1000))" + ) DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + delay) { [weak self] in guard let self, let context = self.context else { return } @@ -474,6 +543,10 @@ final class RDEPUBMetadataParseWorker { if shouldRefresh { let partialMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex) + RDEPUBBackgroundTrace.log( + "Metadata", + "retryPartialMap resolvedChapters=\(parseState.summariesBySpineIndex.count) totalPages=\(partialMap.totalPages)" + ) DispatchQueue.main.async { guard context.paginationToken == self.token, context.controller != nil, @@ -495,6 +568,11 @@ final class RDEPUBMetadataParseWorker { } } + private static func progressPercent(resolved: Int, total: Int) -> Int { + guard total > 0 else { return 0 } + return Int((Double(resolved) / Double(total) * 100.0).rounded()) + } + private func restoreBookPageMapIfPossible() -> RDEPUBBookPageMap? { guard let summaryDiskCache else { return nil } guard summaryDiskCache.isCacheComplete(keys: catalog.map(\.key)) else { diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBPresentationRuntime.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBPresentationRuntime.swift index 62efa51..9d34f6e 100644 --- a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBPresentationRuntime.swift +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBPresentationRuntime.swift @@ -21,6 +21,8 @@ final class RDEPUBPresentationRuntime { private unowned let reconciliationCoordinator: RDEPUBPageMapReconciliationCoordinator + private var pendingCommitRetryWorkItem: DispatchWorkItem? + init( context: RDEPUBReaderContext, locationCoordinator: RDEPUBReaderLocationCoordinator, @@ -62,11 +64,22 @@ final class RDEPUBPresentationRuntime { guard let readerView = context.readerView, let controller = context.controller else { return } - guard !controller.isRepaginating else { return } - guard !readerView.isPageCurlTransitioning else { + guard !context.pendingPageMapUpdates.isEmpty else { + cancelPendingCommitRetry() return } + guard !controller.isRepaginating else { + schedulePendingCommitRetry() + return + } + guard !readerView.isPageCurlTransitioning else { + schedulePendingCommitRetry() + return + } + + cancelPendingCommitRetry() + let rankedUpdates = rankedPendingPageMapUpdates() for (index, update) in rankedUpdates { if commitPendingPageMapUpdate( @@ -75,9 +88,16 @@ final class RDEPUBPresentationRuntime { readerView: readerView, controller: controller ) { + if !context.pendingPageMapUpdates.isEmpty { + schedulePendingCommitRetry() + } return } } + + if !context.pendingPageMapUpdates.isEmpty { + schedulePendingCommitRetry() + } } func queueExtendedPartialPageMap( @@ -139,19 +159,40 @@ final class RDEPUBPresentationRuntime { controller: RDEPUBReaderController ) { let currentLocation = locationCoordinator.currentVisibleLocation() + let livePageBeforeApply = readerView.currentPage + 1 context.textBook = nil applyPageMapToLiveModel(newPageMap) + RDEPUBBackgroundTrace.log( + "Reconciliation", + "applyFullPageMapReplacement livePageBeforeApply=\(livePageBeforeApply) totalPages=\(newPageMap.totalPages) totalChapters=\(newPageMap.totalChapters)" + ) if let currentLocation { - if rebindVisibleLocation(currentLocation, readerView: readerView, controller: controller) == false { - let newPageNumber = controller.pageNumber(for: currentLocation) ?? (readerView.currentPage + 1) - let newPage = max(0, newPageNumber - 1) - rebindVisiblePage( - to: newPage, - readerView: readerView - ) + let resolvedTargetPage = controller.pageNumber(for: currentLocation) + let shouldTrustResolvedLocation = shouldTrustFullReplaceResolvedPage( + resolvedTargetPage, + livePageBeforeApply: livePageBeforeApply + ) + RDEPUBBackgroundTrace.log( + "Reconciliation", + "applyFullPageMapReplacement decision livePage=\(livePageBeforeApply) resolvedTargetPage=\(resolvedTargetPage ?? -1) trustResolved=\(shouldTrustResolvedLocation) href=\(currentLocation.href)" + ) + + if shouldTrustResolvedLocation, + rebindVisibleLocation(currentLocation, readerView: readerView, controller: controller) { + return } + + let fallbackPage = max(livePageBeforeApply, 1) + RDEPUBBackgroundTrace.log( + "Reconciliation", + "applyFullPageMapReplacement preserveLivePage fallbackPage=\(fallbackPage) currentReaderPage=\(readerView.currentPage + 1)" + ) + rebindVisiblePage( + to: fallbackPage - 1, + readerView: readerView + ) } else { readerView.reloadPageCountOnly() } @@ -223,17 +264,23 @@ final class RDEPUBPresentationRuntime { return false } - case .extendPartial(_, let currentLocation): + case .extendPartial(let capturedPageNumber, let currentLocation): removePendingPageMapUpdate(at: index) applyPageMapToLiveModel(update.pageMap) - if let currentLocation, + let livePageNumber = max(readerView.currentPage, 0) + 1 + let shouldTrustCapturedLocation = livePageNumber == max(capturedPageNumber, 1) + RDEPUBBackgroundTrace.log( + "Reconciliation", + "extendPartial commit capturedPage=\(capturedPageNumber) livePage=\(livePageNumber) trustCaptured=\(shouldTrustCapturedLocation) totalPages=\(update.pageMap.totalPages) totalChapters=\(update.pageMap.totalChapters)" + ) + if shouldTrustCapturedLocation, + let currentLocation, rebindVisibleLocation(currentLocation, readerView: readerView, controller: controller) { return true } - // Fallback: prefer the live readerView.currentPage over the stale captured - // currentPageNumber, which may be outdated by the time this commit runs - // (especially in pageCurl mode where the user may have turned several pages - // since the extension was initiated). + // Prefer the live readerView page when the user has moved since the + // extension request was created; otherwise a stale captured location can + // snap pageCurl back to the previous page after the turn completes. let livePageIndex = max(readerView.currentPage, 0) rebindVisiblePage( to: livePageIndex, @@ -250,6 +297,19 @@ final class RDEPUBPresentationRuntime { } private func rebindVisiblePage(to pageIndex: Int, readerView: RDReaderView) { + if pageIndex == readerView.currentPage { + RDEPUBBackgroundTrace.log( + "Reconciliation", + "rebindVisiblePage pageUnchanged currentPage=\(readerView.currentPage + 1) displayType=\(readerView.currentDisplayType)" + ) + readerView.reloadPageCountOnly() + return + } + + RDEPUBBackgroundTrace.log( + "Reconciliation", + "rebindVisiblePage targetPage=\(pageIndex + 1) currentPage=\(readerView.currentPage + 1) displayType=\(readerView.currentDisplayType) isPageCurlTransitioning=\(readerView.isPageCurlTransitioning)" + ) if readerView.currentDisplayType == .pageCurl { if readerView.isPageCurlTransitioning { // Defer the transition until the current page-curl animation completes, @@ -257,6 +317,10 @@ final class RDEPUBPresentationRuntime { DispatchQueue.main.async { [weak readerView] in guard let readerView, !readerView.isPageCurlTransitioning else { return } let livePageIndex = max(readerView.currentPage, 0) + RDEPUBBackgroundTrace.log( + "Reconciliation", + "rebindVisiblePage deferredTransition livePage=\(livePageIndex + 1)" + ) readerView.transitionToPage(pageNum: livePageIndex, animated: false) } } else { @@ -276,6 +340,10 @@ final class RDEPUBPresentationRuntime { controller: RDEPUBReaderController ) -> Bool { guard let targetPageNumber = controller.pageNumber(for: location) else { + RDEPUBBackgroundTrace.log( + "Reconciliation", + "rebindVisibleLocation failedToResolve locationHref=\(location.href) currentPage=\(readerView.currentPage + 1)" + ) return false } @@ -284,9 +352,18 @@ final class RDEPUBPresentationRuntime { forAbsolutePageNumber: targetPageNumber, allowSynchronousLoad: false ) == false { + RDEPUBBackgroundTrace.log( + "Reconciliation", + "rebindVisibleLocation prepareOnDemandBlocked targetPage=\(targetPageNumber) currentPage=\(readerView.currentPage + 1)" + ) return false } + RDEPUBBackgroundTrace.log( + "Reconciliation", + "rebindVisibleLocation targetPage=\(targetPageNumber) currentPage=\(readerView.currentPage + 1) href=\(location.href)" + ) + rebindVisiblePage( to: max(targetPageNumber - 1, 0), readerView: readerView @@ -353,4 +430,29 @@ final class RDEPUBPresentationRuntime { && candidate.pageMap.totalPages >= existing.pageMap.totalPages ) } -} \ No newline at end of file + + private func shouldTrustFullReplaceResolvedPage( + _ resolvedTargetPage: Int?, + livePageBeforeApply: Int + ) -> Bool { + guard let resolvedTargetPage else { return false } + return abs(resolvedTargetPage - livePageBeforeApply) <= 1 + } + + private func schedulePendingCommitRetry() { + guard pendingCommitRetryWorkItem == nil else { return } + + let workItem = DispatchWorkItem { [weak self] in + guard let self else { return } + self.pendingCommitRetryWorkItem = nil + self.commitPendingPageMapUpdateIfNeeded() + } + pendingCommitRetryWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: workItem) + } + + private func cancelPendingCommitRetry() { + pendingCommitRetryWorkItem?.cancel() + pendingCommitRetryWorkItem = nil + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderEnvironment.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderEnvironment.swift index d9dfa4d..ea02800 100644 --- a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderEnvironment.swift +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderEnvironment.swift @@ -1,5 +1,27 @@ import UIKit +enum RDEPUBTextPageLayoutMetrics { + + static let pageNumberTrailingPadding: CGFloat = 4 + + static let pageNumberFooterPadding: CGFloat = 8 + + static let pageNumberReservedHeight: CGFloat = ceil(UIFont.systemFont(ofSize: 13).lineHeight) + pageNumberFooterPadding + + static func contentInsets( + configuration: RDEPUBReaderConfiguration, + safeAreaInsets: UIEdgeInsets + ) -> UIEdgeInsets { + let configInsets = configuration.reflowableContentInsets + return UIEdgeInsets( + top: max(configInsets.top, safeAreaInsets.top), + left: max(configInsets.left, safeAreaInsets.left), + bottom: max(configInsets.bottom, safeAreaInsets.bottom) + pageNumberReservedHeight, + right: max(configInsets.right, safeAreaInsets.right) + ) + } +} + final class RDEPUBReaderEnvironment { weak var controller: RDEPUBReaderController? @@ -46,11 +68,14 @@ final class RDEPUBReaderEnvironment { configuration: RDEPUBReaderConfiguration, pageSize: CGSize ) -> RDEPUBTextLayoutConfig { - let layoutContext = currentLayoutContext(configuration: configuration) + let safeAreaInsets = controller?.view.safeAreaInsets ?? .zero return RDEPUBTextLayoutConfig( frameWidth: max(pageSize.width, 1), frameHeight: max(pageSize.height, 1), - edgeInsets: layoutContext.safeReflowableContentInsets, + edgeInsets: RDEPUBTextPageLayoutMetrics.contentInsets( + configuration: configuration, + safeAreaInsets: safeAreaInsets + ), numberOfColumns: configuration.numberOfColumns, columnGap: configuration.columnGap, avoidOrphans: false, diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift index ea065e1..e4bdecf 100644 --- a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift @@ -191,12 +191,21 @@ final class RDEPUBReaderPaginationCoordinator { runtime: runtime, layoutSnapshot: layoutSnapshot ) + RDEPUBBackgroundTrace.log( + "Pagination", + "anchorChapterReady spine=\(runtimeChapter.spineIndex) pages=\(runtimeChapter.pages.count)" + ) let initialChapters = self.loadInitialInteractiveRuntimeChapters( anchorChapter: runtimeChapter, publication: publication, runtime: runtime, layoutSnapshot: layoutSnapshot ) + let initialPageCount = initialChapters.reduce(0) { $0 + $1.pages.count } + RDEPUBBackgroundTrace.log( + "Pagination", + "initialInteractiveChapters ready count=\(initialChapters.count) pages=\(initialPageCount) spines=\(initialChapters.map(\.spineIndex))" + ) DispatchQueue.main.async { guard context.paginationToken == token, @@ -220,9 +229,17 @@ final class RDEPUBReaderPaginationCoordinator { parser: parser, publication: publication ) + RDEPUBBackgroundTrace.log( + "Pagination", + "startingMetadataWorker token=\(token.uuidString) anchorSpine=\(runtimeChapter.spineIndex) partialPages=\(partialMap.totalPages) partialChapters=\(partialMap.totalChapters)" + ) worker.start(token: token, restoreLocation: restoreLocation) } } catch { + RDEPUBBackgroundTrace.log( + "Pagination", + "initialPaginationFailed error=\(String(describing: error)) prioritizedCandidates=\(prioritizedCandidates)" + ) DispatchQueue.main.async { guard context.paginationToken == token, context.controller != nil else { return } diff --git a/Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift b/Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift index 6e9e6c8..20f7f32 100644 --- a/Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift +++ b/Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift @@ -78,8 +78,12 @@ final class RDReaderPreloadController { environment: Environment, contentViewProvider: (Int, UIView?) -> UIView? ) -> UIView { - let reusableView = detachedReusablePageView(for: pageNum) - let view = contentViewProvider(pageNum, reusableView) ?? reusableView ?? UIView() + let view: UIView + if let reusableView = detachedReusablePageView(for: pageNum) { + view = reusableView + } else { + view = contentViewProvider(pageNum, nil) ?? UIView() + } if shouldCache(view: view, for: pageNum, environment: environment) { pageCurlCachedViews[pageNum] = view } else { @@ -111,8 +115,14 @@ final class RDReaderPreloadController { preloadHostView.frame = parentView.bounds for targetPage in targets { - let existing = detachedReusablePageView(for: targetPage) - let contentView = contentViewProvider(targetPage, existing) ?? existing ?? UIView() + let contentView: UIView + if let existing = preloadedPageViews[targetPage], existing.superview === preloadHostView { + contentView = existing + } else if let cached = pageCurlCachedViews.removeValue(forKey: targetPage), cached.superview == nil { + contentView = cached + } else { + contentView = contentViewProvider(targetPage, nil) ?? UIView() + } let shouldCacheContentView = shouldCache(view: contentView, for: targetPage, environment: environment) if shouldCacheContentView { preloadedPageViews[targetPage] = contentView diff --git a/Sources/RDReaderView/ReaderView/RDReaderView.swift b/Sources/RDReaderView/ReaderView/RDReaderView.swift index 05a9d72..d2a7be9 100644 --- a/Sources/RDReaderView/ReaderView/RDReaderView.swift +++ b/Sources/RDReaderView/ReaderView/RDReaderView.swift @@ -763,6 +763,14 @@ public class RDReaderView: UIView { guard let safePageNum = clampedPageNumber(pageNum) else { return } switch currentDisplayType { case .pageCurl: + if !animated, safePageNum == currentPage, !detectPageViewControllerFault(pageViewController) { + RDReaderTapDebug.log( + "ReaderView.transitionToPage", + "skip same page page=\(safePageNum) animated=\(animated)" + ) + return + } + let request = PageTransitionRequest(pageNum: safePageNum, animated: animated) if shouldQueuePageTransition(request) { return @@ -823,6 +831,7 @@ public class RDReaderView: UIView { } public func reloadData() { + invalidatePageCaches() switchReaderDisplayType(currentDisplayType) topToolView = resolvedTopChromeView() bottomToolView = resolvedBottomChromeView() @@ -830,8 +839,9 @@ public class RDReaderView: UIView { public func reloadPageCountOnly() { if currentDisplayType == .pageCurl { - if !isPageCurlTransitioning, currentPage >= 0, numberOfPages() > 0 { - transitionToPage(pageNum: currentPage, animated: false) + let totalPages = numberOfPages() + if !isPageCurlTransitioning, currentPage >= totalPages, totalPages > 0 { + transitionToPage(pageNum: totalPages - 1, animated: false) } } else { collectionView.reloadData()