User testing polish: Photos tab, Settings, Widgets, Tags, and bug fixes#32
Conversation
Photos tab - Replace select/grid-size/jump-to-top toolbar buttons with: context-menu Select, pinch-to-zoom grid size, tappable two-line principal nav item (title + visible date range) that scrolls to top on tap - Remove .softTopScrollEdge() blur at the top of the grid - Fix landscape detection using verticalSizeClass instead of geometry ratio (the old approach broke when the keyboard shrank the view height) - Fix tag-filter deep-link re-seeding: use hasSeededInitialTags flag instead of activeTags.isEmpty so removing all chips doesn't trigger a re-seed Settings - Disable Reload button and show ProgressView while scanning - Show last-synced as an absolute timestamp (was relative) - Remove "LocalGallery — read-only viewer" footer - Simplify version string to CFBundleShortVersionString only Widgets - Move photo into containerBackground across all three widgets so the image fills edge-to-edge without the black bars iOS 17+ adds around content-layer views; split WidgetHeroView into WidgetBackgroundImage + caption-only HeroView - Replace synthetic "Recently" Memories-widget fallback with the first real stored memory (trip, person, folder, etc.) Tags - Extend hierarchical prefix matching (Places/*) to Objects/* and Scenes/* in both TagIndex virtual-parent generation and PhotoGridScreen filter Bug fixes - Slideshow: add underlayIndex state so the crossfade underlay always holds the "from" photo; previously goPrev() immediately swapped it to index-2, causing a visible flash of the wrong image before the fade completed - Collections memory cards: fall back to any resolvable photo in photoIDs when coverPhotoID can't be found (happens when a photo is moved/renamed, changing its SHA-256 UUID), preventing empty placeholder cards Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ReviewTargeted polish pass — scope is well-bounded and the description maps cleanly onto the diff. Below are findings from a read-through. Correctness — looks good
Worth considering1. 2. Duplicated namespace allowlist
Guaranteed to drift the next time a namespace is added. Suggest a single source of truth — e.g. a 3. Pinch gesture is one-step per pinch 4. Scroll-to-top discoverability when 5. 6. Memory fallback exclusion list Tests / security / performanceNo tests added — consistent with existing coverage for UI/widget code. No new I/O patterns or concurrency changes. No security-relevant changes. SummaryLGTM with two non-blocking items worth picking up before merge: (1) verify |
…dback, date format, opt-in fallback - rescan(silent:) — add parameter (default true) so pull-to-refresh stays silent and only Settings' Reload button triggers the isScanning spinner; fixes unintended scan UI leak into the Photos tab refreshable - TagNamespace.matchesByPrefix — single source of truth for the three hierarchical namespaces (Places, Objects, Scenes); replaces the duplicated triple-check in TagIndex and PhotoGridScreen so new namespaces only need to be added in one place - Pinch gesture live feedback — switch from onEnded-only to onChanged with a pinchBaseTier captured at gesture start; cells now resize in real time during the pinch, matching the feel of system Photos; large pinches (>1.5x, <0.67x) jump two tiers at once - Last Synced format — compact .month(.abbreviated).day().hour().minute() instead of the full .dateTime default (removes year and "at", prevents wrapping on narrow widths) - Widget memory fallback — convert from opt-out (exclude onThisDay/yearsAgo) to opt-in set [.trip, .personOverTime, .folderEvent, .photoDensity, .birthday] so future MemoryType additions don't silently appear as fallback candidates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When photos are moved or renamed their SHA-256 UUID changes, leaving cached memories pointing at IDs that no longer exist in searchService.photoByID. MemoryCardView already falls back through photoIDs to find any resolvable photo for the card image, but when every ID in a memory is stale it still shows a gray empty placeholder. visibleMemories now mirrors that resolution order: keep a memory only if coverPhotoID or any photoID resolves to a known photo. Fully-stale memories are hidden rather than shown as gray cards. They reappear correctly after the next memory regeneration run (which uses the current photo UUIDs). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…card visual Scoring - personOverTime: +20 flat bonus (was pure formula) — people memories now rank well above folder memories regardless of library size - trip / subtrip base bonus: 18 (was 8) — trips likewise clear folders - folderEvent: multipliers halved (0.4/photo, 1.0/span-day) so folders only surface when no better content exists These bring the approximate rank order to: birthday (100+) > onThisDay > personOverTime/trip > yearsAgo > photoDensity > folderEvent Cap - Top-N reduced from 20 to 10; the rail stays scannable and higher-quality memories aren't buried behind a wall of folder cards Empty card visual - When coverURL is nil (thumbnail load failure) show a photo.on.rectangle SF Symbol centred on the placeholder rectangle instead of the near-invisible bgGrouped fill that blended into the bg background and appeared as a gap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Redesigned icon: beige folder with polaroid (left lip) on orange
gradient background
- AccentColor asset now defines orange (0.769, 0.541, 0.243) with
dark mode variant; Design.accentColor reads from asset via
Color("AccentColor") so changes propagate app-wide
- Nav bar standard appearance uses opaque background at 0.90 alpha
for better readability of inline title over photo grid
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for scroll performance, especially noticeable on the 20k All Photos grid: 1. PhotoGridScreen: decouple section visibility tracking from @State. The per-cell onAppear/onDisappear was mutating @State on every cell enter/exit, forcing SwiftUI to diff the entire 20k-item ForEach tree every frame. Replaced with a non-observed SectionTracker class; the date-range string syncs to @State on a 300ms debounce (~3 re-evals/s instead of dozens). 2. PhotoGridScreen: keep the live date-range Text out of the ScrollView content on the Photos tab. The toolbar principal already handles it; having a changing sibling inside the scroll content was invalidating the LazyVGrid layout. 3. ThumbnailService: force-decode disk-cached thumbnails via ImageIO with kCGImageSourceShouldCacheImmediately instead of UIImage(data:). The lazy UIImage created by UIImage(data:) deferred JPEG decode to the render pipeline, where it ran unbounded for every visible cell, exhausting IOSurfaces (the CMPhotoJFIFUtilities -17102 cascade). Also added a DecodeLimiter actor (limit 4) for fresh thumbnail generation from source images. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Polish pass covering all findings from a user testing session, plus two bugs found after.
Photos tab
.softTopScrollEdge()blur at the top of the gridgeo.size.width > geo.size.heightwhich returnedtruewhenever the keyboard shrank the view height; now uses@Environment(\.verticalSizeClass) == .compactactiveTags.isEmptyguard with ahasSeededInitialTagsflag so removing all chips no longer triggers a re-seedSettings
ProgressViewrow appears during the scanCFBundleShortVersionStringonly (e.g.1.0.0)Widgets
.containerBackgroundacross all three widgets (MemoriesWidget,FolderWidget,TagsWidget) — this is what caused the black bars: iOS 17+ automatically adds safe-area padding around content-layer views, but not aroundcontainerBackgroundWidgetHeroViewintoWidgetBackgroundImage(goes incontainerBackground, renders the photo edge-to-edge with a bottom gradient scrim) and a caption-onlyWidgetHeroView(title + subtitle + optional glyph)Tags
Objects/*andScenes/*inTagIndexPhotoGridScreen.recomputeFilterto the same three namespacesBug fixes
photos[(index-1+count)%count], so callinggoPrev()immediately moved the underlay toindex-2before the fade completed, flashing the wrong photo. Fixed with anunderlayIndexstate variable that captures the current photo beforeindexchanges, used ingoPrev(),goNext(), and the timer auto-advanceMemoryCardView.coverURLreturnednilwhenever the cover photo's UUID couldn't be found in the search index (e.g. photo moved/renamed, changing its SHA-256-derived UUID). Fixed by falling back to the first resolvable photo inmemory.photoIDsbefore showing the empty gray rectangleTest plan
1.0.0, no build number in parentheses🤖 Generated with Claude Code