Skip to content

fix(reader): use stable page IDs instead of indices for preloading#654

Merged
everpcpc merged 2 commits intomainfrom
fix/reader-preload-stale-index
Mar 12, 2026
Merged

fix(reader): use stable page IDs instead of indices for preloading#654
everpcpc merged 2 commits intomainfrom
fix/reader-preload-stale-index

Conversation

@everpcpc
Copy link
Copy Markdown
Owner

Problem

When the DIVINA reader opens a book that belongs to a multi-segment series, preloadNextSegmentIfNeeded may prepend the previous book's pages to the readerPages array. This shifts all page indices, causing background preload tasks that captured raw indices to load pages from the wrong book. The cleanup pass then evicts the correctly-loaded visible pages, leaving the UI stuck on a loading spinner — most noticeable in dual-page and cover modes.

Approach

Replace every external index-based preload path with stable ReaderPageID lookups that resolve the index at call time, so stale captures are impossible. Additionally, track the set of currently visible page IDs in the scheduler so cleanup never evicts pages the user is looking at.

For CoverPageView (pure SwiftUI → PageScrollView UIViewRepresentable), images were loaded but the UI never refreshed because the image cache lives outside @Observable. Fixed by having PageScrollView's Coordinator register a pagePresentationInvalidationObserver — the same pattern the scroll reader already uses via NativePagedPagePresentationCoordinator.

Scope

  • ReaderPageLoadScheduler: merge preloadImageForPage(at:) into preloadImage(for:) as single entry point; simplify preloadPages from task-group to sequential loop (tasks were serialising on MainActor anyway); add visiblePageIDs protection in cleanup; remove dead setPreloadedImage(forPageIndex:) overload
  • ReaderViewModel: remove public preloadImageForPage(at:) wrapper
  • ScrollPageView (iOS/macOS): switch preloadVisiblePages from index-based to pageID-based loading
  • CoverPageView: make preloadPresentationWindow async with prioritizeVisiblePageLoads; collapse redundant guard block
  • NativeImagePageViewController: use preloadImage(for:) instead of capturing stale index
  • PageScrollView (iOS/macOS): register presentation invalidation observer for automatic UIKit refresh

Validation

  • All platforms build (iOS, macOS, tvOS)
  • Scroll mode: first and second page load immediately in dual-page mode
  • Cover mode: pages display on initial open without requiring a page turn
  • Page cleanup does not evict visible pages

When segments are prepended/appended in multi-segment readers, page indices
shift and become stale. Background preload tasks that captured raw indices
would load pages from the wrong book, and cleanup would evict correct pages.

Changes:
- Replace all index-based preload paths with stable ReaderPageID-based ones
- Remove preloadImageForPage(at:) from ReaderViewModel public API
- Make ReaderPageLoadScheduler.preloadImage(for:) the single entry point
- Track visiblePageIDs in scheduler to protect them from cleanup eviction
- Add presentation invalidation observer to PageScrollView (iOS/macOS) so
  CoverPageView refreshes when images finish loading
- Simplify preloadPages to sequential loop (was using task group that
  serialized on MainActor anyway)
- Clean up dead code: unused setPreloadedImage(forPageIndex:) overload and
  redundant guard block in CoverPageView
@everpcpc everpcpc force-pushed the fix/reader-preload-stale-index branch from 1eadfa7 to 6284e15 Compare March 12, 2026 12:28
@everpcpc everpcpc merged commit 21470f4 into main Mar 12, 2026
3 checks passed
@everpcpc everpcpc deleted the fix/reader-preload-stale-index branch March 12, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant