Adapted from Apple's Wishlist TripDetailView:
- 510pt hero image with .scrollEdgeEffectStyle(.soft)
- Trip stats overlay (glass material + MeshGradient, same pattern)
- Title in .largeTitle toolbar (expanded font)
- .toolbarRole(.editor) for clean back navigation
Content sections with real API data:
- Lodging: hotel name, location, check-in/out dates
- Flights & Transport: route, type icon, flight number
- Places: name, category, visit date
- AI Recommendations: first 500 chars of suggestions
- Empty state when no details added
Models: TripDetail, TripTransportation, TripLodging, TripLocation,
TripNote — matching the /api/trip/{id} response shape
GradientOverlay: MeshGradient mask from Wishlist's GradientView
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ROOT CAUSE: @State TripsViewModel inside TripsHomeView was recreated
during navigation transitions. Each recreation set isLoading=true,
flipping the Group conditional, destroying the ScrollView + namespace.
The zoom transition's matchedTransitionSource evaporated.
FIXES (matching Apple's Wishlist pattern):
1. ViewModel owned by MainTabView (like ReaderVM) — survives
view recreation. Pre-loaded on app launch.
2. Loading state rendered INSIDE ScrollView — no conditional
Group wrapper that swaps the entire view tree.
3. Single @Namespace in TripsHomeView, passed to both
UpcomingTripsPageView and PastTripsSection.
4. Zoom transition restored on all NavigationLinks.
Why Apple's sample works: they use @Environment(DataSource.self)
which is app-level stable. Our equivalent: @State at MainTabView.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ROOT CAUSE: .navigationTransition(.zoom) + .matchedTransitionSource
in a paged TabView. When navigating back, the zoom tries to animate
to the source card, but the TabView may have recycled/repositioned
the page. The animation targets a stale frame, causing the card to
flash black or disappear.
FIX: Removed .zoom transitions and .matchedTransitionSource from
both UpcomingTripsPageView and PastTripsSection. Standard push/pop
navigation is stable with paged TabViews.
Also removed all debug logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
onAppear doesn't fire during programmatic scrolling (UIScrollView
contentOffset changes don't trigger SwiftUI lifecycle). Added
onNearBottom callback to ScrollViewDriver — fires when within 500pt
of bottom during auto-scroll tick. 3s cooldown prevents rapid-fire.
Auto-scroll no longer stops at bottom — idles at maxOffset while
loadMore fetches. When new entries arrive, contentSize grows and
scrolling resumes automatically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Images with .fill were expanding beyond the 180pt frame because
SwiftUI's .clipped() doesn't constrain the layout, only the
rendering. Now uses GeometryReader to set explicit width + height
on the image, then clips the container. Guarantees 180pt max
regardless of image aspect ratio.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#22 Image overflow: added .frame(maxWidth: .infinity) before
.frame(height: 180) on AsyncImage to constrain width within card.
Card's .clipShape already clips corners.
#23 Load more not triggering: added loadMoreIfNeeded(for:) that
fires onAppear for entries 5 from the bottom. No longer relies
solely on the bottom sentinel Color.clear which could be missed.
Also increased sentinel height from 1pt to 40pt.
#24 Refresh not updating read state: flushDeferredReads() now
called before vm.refresh() in .refreshable. Deferred marks are
synced to API before re-fetching, so the server returns correct
read states. Also clears markedByScroll set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MacroRing:
- Animates from 0 to target on appear (spring, 1.0s response)
- Center value fades in + scales up with 0.4s delay
- Updates animate smoothly on data change
- .contentTransition(.numericText()) on calorie count
MacroBar:
- Bar width animates from 0 to target on appear (spring, 0.3s delay)
- Updates animate smoothly on data change
- .contentTransition(.numericText()) on values
TodayView:
- Meal sections stagger in: fade up with 0.08s delay per card
- Re-animates on tab switch (onAppear resets animated flag)
- Re-animates on date change
- Spring physics for natural feel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keyboard appears automatically when opening Quick Add from the
food assistant sheet. 300ms delay lets the sheet animation finish
first so the keyboard doesn't interfere with the presentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pill tabs (Quick Add / AI Chat) stay visible and tappable.
Pages are also swipeable. Pills sync with swipe position.
Quick Add shown first, swipe left for AI Chat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Widget background tap → platform://fitness → opens Fitness tab
Widget + button tap → platform://add-food → opens Fitness + food assistant
Small widget: + button in bottom-right corner (emerald green)
Medium widget: + button in bottom-right corner
Lock screen widgets: single tap → fitness (no room for + button)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Widget: .widgetURL(platform://add-food) on all widget sizes
App: .onOpenURL handles platform://add-food → switches to Fitness
tab and opens the food assistant sheet
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Session expires: widget gets 401 → clears stale cookie from
App Group → stops retrying with bad auth → shows cached data
until user opens app and re-authenticates
2. Account switch: login() now calls clearWidgetAuth() BEFORE
syncCookieToWidget() — clears previous user's cached calories
before writing new user's cookie. No brief display of wrong data.
3. Logout: already correct — clearWidgetAuth removes cookie +
cached data, widget shows 0/2000
4. Minimum data: only session cookie + 2 cached numbers + timestamp
in App Group. No passwords, no user IDs, no PII.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Widget:
- Fetches /api/fitness/entries/totals and /api/fitness/goals/for-date
directly from gateway using shared session cookie
- Falls back to cached data in App Group UserDefaults if network fails
- Refreshes every 15 minutes via WidgetKit timeline
- Each phone shows the logged-in user's own data
Auth sharing:
- AuthManager.syncCookieToWidget() copies the session cookie to
App Group UserDefaults on login and auth check
- Widget reads cookie and makes authenticated API calls
- Logout clears widget auth + cached data
Data in App Group (group.com.quadjourney.platform):
- widget_sessionCookie: auth token for API calls
- widget_totalCalories: cached fallback
- widget_calorieGoal: cached fallback
- widget_lastUpdate: cache timestamp
HomeViewModel also writes cache on each loadTodayData() as fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Widget displays:
- systemSmall: calorie ring + "X left" text
- systemMedium: ring + "Calories" / "X of Y" / "X remaining"
- accessoryCircular: gauge ring for lock screen
- accessoryInline: "🔥 845 / 2000 cal" text for lock screen
- accessoryRectangular: linear gauge + calorie count
Data flow: main app writes totalCalories + calorieGoal to
UserDefaults on each loadTodayData(), then calls
WidgetCenter.shared.reloadAllTimelines(). Widget reads on
15-minute refresh cycle.
Note: currently uses standard UserDefaults (same app container).
For production, migrate to App Group UserDefaults so widget
process can read the data. Requires Xcode App Group setup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tab bar action button cycles through speeds on each tap:
- ▶ (play) → tap → Slow (1.5x, hare icon)
- Slow → tap → Med (1.75x, walk icon)
- Med → tap → Fast (2.0x, bolt icon)
- Fast → tap → back to Slow
Touch feed to stop → icon returns to ▶
Removed speed controls from Reader toolbar — all speed control
is now in the single tab bar button. Clean, no overlays.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. .navigationBarTitleDisplayMode(.inline) — title stays at top
2. .background(Color.canvas) — restores warm cream background
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reader now shows only unread entries. Removed:
- Unread/Starred/All sub-tab selector
- Feed filter chip bar (categories)
- All related state (selectedSubTab) and helpers
Glass nav bar shows: "Reader" title + "74 unread" subtitle
Trailing toolbar: grid/list toggle + ellipsis menu
Auto-scroll: speed controls in leading toolbar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed .navigationBarHidden(true) and all custom header layout.
Now uses standard iOS 26 navigation APIs that get Liquid Glass free:
- .navigationTitle("Reader") + .navigationSubtitle("74 unread")
- ToolbarItemGroup for sub-tabs (Unread/Starred/All) on leading
- ToolbarSpacer between groups
- ToolbarItemGroup for grid/list + menu on trailing
- Feed filter chips in .bottomBar toolbar
- When auto-scrolling: toolbar swaps to speed controls
The glass nav bar is translucent — content scrolls underneath.
Collapses to inline glass pill on scroll (system behavior).
No custom backgrounds, no custom layout — all system-managed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uses .safeAreaBar(edge: .bottom) on the Reader content — the iOS 26
API that places content in the collapsed tab bar area, same position
as the Now Playing mini bar. Speed controls [ - ] 1.50x [ + ] appear
in the glass bar when auto-scrolling.
Removed the floating speed pill overlay from ContentView.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Speed pill and feedback button were in separate VStacks with
independent absolute padding, causing misalignment. Now they share
one VStack with .padding(.bottom, 70) at the container level.
The speed pill sits directly above the tab bar area, positioned
relative to the same anchor as all other bottom controls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Speed pill was floating mid-screen (padding.bottom 90). Moved to
bottom (padding.bottom 8) to sit just above the tab bar area.
Switched from ultraThinMaterial to regularMaterial for better
Liquid Glass look with more opacity and stronger blur.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ROOT CAUSE (confirmed by instrumentation):
Every markRead during auto-scroll mutated entries[idx].status,
causing SwiftUI to re-render the row (bold→regular, dot removed).
This changed card height, causing contentSize jumps of 10-128pt
per entry — visible as jitter.
FIX: During auto-scroll, collect entry IDs in deferredReadIDs
instead of mutating entries array. When auto-scroll stops,
flushDeferredReads() applies all status changes at once and
sends a single batched API call.
Manual scroll still marks immediately (no deferral needed since
the user controls the scroll position).
Removed all debug instrumentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calling scrollViewWillBeginDragging/DidEndDragging on the delegate
didn't trigger tabBarMinimizeBehavior — iOS 26 likely tracks actual
touch events, not delegate calls. Reverted to avoid side effects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tabBarMinimizeBehavior needs scrollViewWillBeginDragging to recognize
a scroll session. Now ScrollViewDriver calls it on the original
delegate when auto-scroll starts, and scrollViewDidEndDragging +
scrollViewDidEndDecelerating when it stops.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug: when action tab (value=3) was tapped, selectedTab was already 3
by the time handleActionTap ran. The check 'if selectedTab == 2'
always failed, falling through to food assistant.
Fix: use onChange(of: selectedTab) oldValue to capture which tab the
user was on BEFORE tapping the action button. Pass that to
handleActionTap(from:). If from Reader (2), toggle auto-scroll.
If from Home/Fitness, open food assistant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single separated circle in tab bar (like Photos search icon):
- Home/Fitness: shows + icon, taps opens food assistant
- Reader idle: shows play icon, taps starts auto-scroll
- Reader playing: shows pause icon, taps stops auto-scroll
Icon updates dynamically via computed actionIcon property.
handleActionTap() routes the tap based on selectedTab.
After action, selectedTab returns to the previous tab (doesn't
stay on the invisible "action" tab).
Speed controls still appear as glass capsule overlay when playing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed Tab(role: .search) — it was always visible on all tabs.
Now the floating action button changes based on selected tab:
- Home/Fitness: FAB (+) → opens food assistant (same as before)
- Reader: Play/Pause circle → toggles auto-scroll
- Idle: play.fill icon, warm accent background
- Playing: pause.fill icon, red background
- Tapping toggles between play/pause
Speed controls appear as glass capsule above the FAB when playing.
Auto-scroll stops when switching away from Reader tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrated to new Tab API (iOS 18+). The auto-scroll play button
uses Tab(role: .search) with systemImage: "play.fill" — this gives
the separated circular placement on the trailing side of the tab
bar, identical to the Photos app search icon.
Tapping the play icon:
- Switches to Reader tab
- Starts auto-scroll
When playing: a Liquid Glass speed control capsule appears above
the tab bar with [ - ] 1.00x [ + ] [ stop ].
Removed the old floating glass pill implementation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moved auto-scroll control from ReaderTabView to MainTabView so it
sits at the tab bar level, trailing side — matching the iOS Photos
search icon placement.
Idle: 48px glass circle with play icon (bottom-right, tab bar row)
Playing: expands to capsule with [ - ] 1.00x [ + ] [ stop ]
Spring animation between states.
Grid/list toggle and ellipsis menu moved inline to the sub-tab
header row (next to Unread/Starred/All) so they're always visible
without needing a toolbar.
ReaderTabView now receives isAutoScrolling and scrollSpeed as
bindings from MainTabView.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>