Illust detail view with matched-geo lightbox + R-18 setting
Created by: jedmund
Summary
Tapping a cell in the home feed now pushes an IllustDetailView with an iOS 18 zoom navigation transition, and tapping the detail hero opens a matched-geometry lightbox with pinch-zoom / pan / double-tap-zoom. Adds a "Show R-18 content" toggle in Profile.
-
Detail view —
pixillate/Views/IllustDetailView.swift. Full-width grey hero container sized viacontainerRelativeFrame(.vertical) { h, _ in h - 200 }so the image fits the viewport with a 200pt peek of the metadata below. Metadata block (title, user row with avatar, caption, tag pills) fades in with a 0.25s-delayed animation so it doesn't grow-with-the-zoom mid-transition. -
Lightbox —
pixillate/Views/IllustLightboxView.swift+ZoomableImageView.swift. Not afullScreenCover— it's a SwiftUI.overlayon the detail view so the detail content visibly dims behind it. The hero and the lightbox share amatchedGeometryEffectid so the lightbox scales up from the hero's frame. Inside the lightbox, aUIScrollView-backedZoomableImageView(layout-aware via aZoomableScrollViewsubclass) gives real pinch/pan/inertia/double-tap-zoom. Pull-down at min zoom dismisses back to the detail view — needsalwaysBounceVertical = trueso the scroll view wins the gesture contest against the parent zoom-navigation dismissal. -
R-18 setting —
pixillate/Storage/AppSettings.swift+ toggle inProfileView. The grid cells blur restricted illusts when off, for safe scrolling / video. The detail view intentionally ignores the setting — if the user tapped through, they've already chosen to look. -
Model —
PixivIllustgrewcaption,tags,metaSinglePage, andAuthor.profileImageURLs.mediumso the detail view has everything it needs in one round trip. AbestImageURLcomputed falls back frommeta_single_page.original_image_urltoimage_urls.large. -
PixivImage — new
maxWidthparameter so the detail hero can override the grid's 300pt downsample cap without inflating grid cells.
Known issues (for follow-up)
-
Black-cell-on-next-tap after dismissal. Reproducible on a physical device: tap a cell → detail view opens → dismiss via swipe-back / back button / pull-down → tap another cell → the image either doesn't respond or flashes black until it scrolls off-screen and back. Scrolling the cell out of view and back fully recovers it. Speculated cause is iOS 18 zoom-navigation cleanup leaving residual state on the destination hosting controller. Tried narrowing
matchedTransitionSourceto thePixivImageand restructuring the detail view to haveScrollView(notZStack) as its root with.navigationTransitionapplied to the root — neither fully fixed it. Candidates to try next: swapNavigationLinkforButton+NavigationPath, or drop.matchedTransitionSource/.navigationTransitionto bisect which side is the trigger. -
Interactive finger tracking during dismissal. iOS 18's zoom navigation doesn't track the user's finger during pull-down — the reverse animation only runs on commit. A Pinterest-style pull that follows the finger with scale/offset would need a custom
DragGesturecoordinated with theScrollViewscroll gesture (only active at scroll top) and the matched-geometry reverse. Noted in the file header, deferred for its own pass.
Test plan
-
Tap a cell in the home grid → detail view opens with a zoom transition from the cell's image. -
Detail hero fills the width with a grey letterbox; metadata peek visible at the bottom of the viewport. -
Tap the hero → lightbox zooms up from the hero frame via matched geometry; background dims. -
In the lightbox, pinch to zoom, pan when zoomed, double-tap to toggle zoom. -
Pull down in the lightbox at min zoom → collapses back to detail view (not all the way to grid). -
X button dismisses the lightbox with matched-geo reverse. -
Toggle "Show R-18 content" in Profile → grid cells un-blur / blur; detail view always shows unblurred. -
Multi-page illusts still render their first page correctly (no crash on missing meta_pagesdecode).