Skip to content

Pinterest-style detail overlay with floating hero + horizontal feed swipe

jedmund requested to merge jedmund/pinterest-detail into main

Created by: jedmund

Summary

Replaces the push-based IllustDetailView with an overlay-based detail experience modeled on Pinterest:

  • Detail overlay stackDetailPresenter owns a stack of PresentedDetail layers rendered above every tab; feeds push onto it instead of using NavigationLink. Top two layers stay mounted so v3 related-illust drill-downs can animate against their source.
  • Floating hero/lightbox transitionsFloatingHeroCoordinator + FloatingLightboxCoordinator drive cellhero and herolightbox animations via a single floating image rather than SwiftUI's matchedGeometryEffect. 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 send overload on PixivAPIClient, floating heart button in detail, and bookmark indicator on grid cells.
  • v2 horizontal feed swipe — detail overlay becomes a ScrollView(.horizontal) + LazyHStack paging through the entire feed. Feed proxy is passed through PresentedDetail so 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 .onScrollPhaseChange gate never fired because rubber-bands skip .decelerating.
  • Multi-page tap-flash mitigation — outer pager is wrapped in ScrollViewReader and primed via proxy.scrollTo(detail.currentIndex) in .onAppear; content is opacity-gated on hasPrimedScroll (flipped when the live offset reaches the target). Additionally every non-tapped page is hidden via opacity while the floating hero is .lifting, since LazyHStack materializes 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 by onGeometryChange so the coordinator always has a dismiss target.
  • PixivImageSizeRegistry — observable cache of decoded image natural sizes keyed by URL. PixivImage reports 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)

🤖 Generated with Claude Code

Merge request reports

Loading