Pinterest-style detail overlay with floating hero + horizontal feed swipe
Created by: jedmund
Summary
Replaces the push-based IllustDetailView with an overlay-based detail experience modeled on Pinterest:
-
Detail overlay stack —
DetailPresenterowns a stack ofPresentedDetaillayers rendered above every tab; feeds push onto it instead of usingNavigationLink. Top two layers stay mounted so v3 related-illust drill-downs can animate against their source. -
Floating hero/lightbox transitions —
FloatingHeroCoordinator+FloatingLightboxCoordinatordrive cell↔ hero and hero↔ lightbox animations via a single floating image rather than SwiftUI'smatchedGeometryEffect. No cross-fade, no clipping, no two-overlapping-views artifacts. - Pull-to-dismiss — driven off the detail ScrollView's phase + offset, with spring commit targeting the cell's unshifted origin frame.
-
Bookmarks — add/remove endpoints, void
sendoverload onPixivAPIClient, floating heart button in detail, and bookmark indicator on grid cells. -
v2 horizontal feed swipe — detail overlay becomes a
ScrollView(.horizontal) + LazyHStackpaging through the entire feed. Feed proxy is passed throughPresentedDetailso we can scroll the underlying feed to match the swiped-to illust before dismiss. -
Snap-down fix — empirical delta calibration against the lift-origin cell stops the dismiss animation landing ~24pt off on iPad when
.statusBarHidden()collapses the feed's safe-area-top. -
Multi-page carousel rewrite — per-page natural aspect ratios via a new
PixivImageSizeRegistry. The prior carousel stretched every page to the illust's top-level aspect (which only describes page 0), so subsequent pages rendered letterboxed or cropped. Lightbox lift now sizes to the currently-visible page, not page 0. -
Edge-overshoot feed cascade — flicking past the first or last page of a multi-page post advances the outer feed pager to the previous/next illust. Runs on
.simultaneousGesture(DragGesture)with a threshold + predicted-translation gate; the prior.onScrollPhaseChangegate never fired because rubber-bands skip.decelerating. -
Multi-page tap-flash mitigation — outer pager is wrapped in
ScrollViewReaderand primed viaproxy.scrollTo(detail.currentIndex)in.onAppear; content is opacity-gated onhasPrimedScroll(flipped when the live offset reaches the target). Additionally every non-tapped page is hidden via opacity while the floating hero is.lifting, sinceLazyHStackmaterializes idx 0..3 regardless of.scrollPosition(id:).
Notable architecture bits
-
HeroFrameComputer/LightboxFrameComputer— pure functions that compute hero/lightbox frames from screen bounds + safe-area insets so both the floating image and in-place hero agree on geometry. -
CellFrameRegistry+CellFrameReporter— registry of live cell window-frames, published byonGeometryChangeso the coordinator always has a dismiss target. -
PixivImageSizeRegistry— observable cache of decoded image natural sizes keyed by URL.PixivImagereports on cache hit + load; the multi-page carousel and lightbox lift read it for per-page aspect ratios. -
SpringTuner— dev-only live spring parameter editor, moved behind Settings → Developer.
Test plan
-
iPhone 17 Pro sim: tap feed cell → lift settles cleanly → close → no snap at end of dismiss -
iPad Pro 13-inch (M5) landscape: same base case — the 24.5pt shift is most pronounced here -
Pull-to-dismiss from detail — spring targets unshifted cell origin -
Swipe right to adjacent illust whose cell is still mounted → close → lands on the new cell (unshifted) -
Swipe far enough that new cell is off-screen → close → feed scrolls + remounts, dismiss lands correctly -
Bookmark toggle from detail — heart state persists, grid indicator updates -
Multi-page illust — page counter works; each page reserves its own aspect-ratio slot; lightbox opens to the currently-visible page at that page's aspect -
Multi-page edge cascade — flick left past last page advances to next illust; flick right past first page advances to previous -
Multi-page tap on iPad landscape — no ghost pedestal from a neighbor page appears during the hero lift -
Tag feed and profile feeds — detail overlay opens from all three feeds (Home, Search→Tag, Profile)