Skip to content

refactor(epub): rebuild iOS cover mode on one web view per chapter - #1236

Merged
everpcpc merged 2 commits into
mainfrom
refactor/epub-cover-single-webview
Oct 9, 2026
Merged

everpcpc merged 2 commits into
mainfrom
refactor/epub-cover-single-webview

Conversation

@everpcpc

@everpcpc everpcpc commented Oct 9, 2026

Copy link
Copy Markdown
Member

What

Rebuilds the iOS EPUB cover mode on the single-web-view-per-chapter + snapshot-transition model that the macOS cover view and the iOS paged-scroll view already use, per #1234:

  • Chapter-granularity hosts: the three-controller deck (front/next/previous, one full WKWebView per page) and the five-entry page-controller cache are gone. At most three EpubPageViewControllers stay alive: the current chapter plus, only while at a chapter edge, the preloaded next/previous chapter. At steady state mid-chapter exactly one web view is alive, versus up to five whole-chapter DOMs before.
  • Turns within a chapter no longer spin up another web view (no CSS re-injection, no second pagination pass): the live web view scrolls to the target page while a snapshot of the current page performs the cover slide. Forward turns slide the snapshot away over the live view; backward turns slide the live view in over the snapshot (the moving page stays live).
  • Cross-chapter drags reveal the preloaded neighbor chapter's web view underneath the sliding snapshot; on commit the neighbor becomes current and the old chapter is reused as the opposite neighbor.
  • Programmatic turns (taps, TOC, progress jumps) run the same slide animation when Animate Page Turns is on, and jump instantly when off — same triggers as before.
  • Unchanged: tap zones, boundary rubber-band resistance, onEndReached, RTL mirroring, overlay labels, theme/font live updates, progress recording, preferLastPageOnReady for previous-chapter-last-page turns.

Why

Per-page web views were the expensive part of the paged modes: every intra-chapter turn re-laid-out a chapter that a neighboring controller had already laid out, and the deck needed cache/eviction bookkeeping to stay alive. Laying a chapter out once and turning pages as JS scrolls removes both.

Validation

  • make format; make build green on iOS/macOS/tvOS.
  • iPhone 18 Pro simulator, Moby Dick (EN EPUB), Cover mode:
    • Tap turn: programmatic slide animates and commits (6/10 → 7/10).
    • Forward intra-chapter drag captured mid-gesture: current-page snapshot slides under the finger, next page revealed live beneath; commits on release.
    • Backward intra-chapter drag captured mid-gesture: live web view (target page) slides in from the leading edge over the snapshot; commits back.
    • Short drag below the commit threshold cancels and stays on the origin page.
    • Cross-chapter forward drag at a chapter end captured mid-gesture: preloaded next chapter (XLI, fully rendered) revealed beneath the snapshot; commits to 1/18.
    • Cross-chapter backward drag from a chapter start captured mid-gesture: previous chapter's last page slides in live; commits back to 10/10.
    • Rotation: re-paginates and restores the position on the way back (10/10 → landscape → 10/10); A/B against main shows identical rotation behavior (landscape lands on an approximate earlier chunk of the chapter on both — pre-existing re-pagination approximation, unchanged by this PR).
  • Not explicitly exercised: first/last-page-of-book rubber-band (logic carried over unchanged), RTL books (same mirrored-sign math as before).

Part of #1234

Replace the three-controller deck and the five-entry page-controller
cache with chapter-granularity hosts (current + cross-chapter
neighbors). Page turns capture a snapshot of the current page: forward
turns slide the snapshot away over the live web view already scrolled
to the target page, backward turns slide the live web view in over the
snapshot. Cross-chapter drags use the preloaded neighbor web view
underneath, so only 1-3 web views stay alive instead of up to 5, and a
chapter is laid out once instead of once per page.

Part of #1234
- scroll reused neighbor hosts to the target page when a cross-chapter
  slide begins; a host reused after a mid-chapter jump could otherwise
  reveal a stale page before snapping to the target on commit
- keep the origin snapshot covering the live view until the cancel
  scroll-back lands, so a cancelled same-chapter turn no longer flashes
  the target page (the backward case also needs the live view kept
  off-screen since it sits above the snapshot)
- remove on-demand cross-chapter hosts that are not registered
  neighbors when a slide is cancelled, instead of leaving hidden
  orphans attached to the container
- reset rubber-band residue when a slide session begins so a direction
  reversal at the book boundary starts from a clean frame
@everpcpc
everpcpc merged commit 621f79e into main Oct 9, 2026
3 checks passed
@everpcpc
everpcpc deleted the refactor/epub-cover-single-webview branch October 9, 2026 11:26
everpcpc added a commit that referenced this pull request Oct 9, 2026
#1237)

Rebuild the iOS page-curl transition on the same
single-web-view-per-chapter architecture the cover mode moved to in
#1236, instead of one WKWebView per page.

What:
- one live EpubPageViewController per chapter plus preloaded boundary
  neighbors (at most 3 web views), replacing the 5-entry per-page
  controller cache, the pending/reserved controller bookkeeping with
  its 0.3s cleanup timer, and the backside-image LRU
- UIPageViewController slots become dumb shells
  (CurlPageShellViewController): a chapter-internal flip covers the
  front shell with a snapshot of the current page and migrates the
  live web view into a pre-sized target shell scrolled to the target
  page, so the curling leaf is a static image while the reveal and the
  committed page are the same live view
- cross-chapter flips keep the live leaf and reveal the preloaded
  neighbor chapter, rotating roles on commit
- interactive turns commit or restore in didFinishAnimating,
  programmatic turns in their own setViewControllers completion; the
  internal pan recognizers carry a target that cancels dataSource
  prefetches abandoned before a curl starts, so a dropped gesture
  cannot wedge the turn state
- tap zones, book-end refusal with onEndReached, RTL spine/semantic
  mapping, backside style, overlay labels, and progress recording
  behave as before

Validation: built all platforms; verified on iOS simulator with an
English EPUB — tap turns both directions, interactive curls both
directions with mid-gesture captures (static leaf, mirrored leaf back,
live revealed page), short-drag cancels, cross-chapter curls both
directions with the neighbor pre-scrolled to its first/last page,
book-start boundary refusal, single-page chapter crossing, and
rotation reflow. RTL books untested (mapping carried over unchanged).

Part of #1234
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