Critical:
- ProgressViewModel: use single stored ModelContext instead of creating
new ones per operation (deleteVisit silently no-op'd)
- ProgressViewModel: convert expensive computed properties to stored
with explicit recompute after mutations (3x recomputation per render)
Memory:
- AnimatedSportsIcon: replace recursive GCD asyncAfter with Task loop,
cancelled in onDisappear (19 unkillable timer chains)
- ItineraryItemService: remove [weak self] from actor Task (semantically
wrong, silently drops flushPendingUpdates)
- VisitPhotoService: remove [weak self] from @MainActor Task closures
Concurrency:
- StoreManager: replace nested MainActor.run{Task{}} with direct await
in listenForTransactions (fire-and-forget race)
- VisitPhotoService: move JPEG encoding/file writing off MainActor via
nonisolated static helper + Task.detached
- SportsIconImageGenerator: replace GCD dispatch with Task.detached for
structured concurrency compliance
Performance:
- Game/RichGame: cache DateFormatters as static lets instead of
allocating per-call (hundreds of allocations in schedule view)
- TripDetailView: wrap ~10 routeWaypoints print() in #if DEBUG, remove
2 let _ = print() from TripMapView.body (fires every render)
Accessibility:
- GameRow: add combined VoiceOver label (was reading abbreviations
letter-by-letter)
- Sport badges: add accessibilityLabel to prevent SF symbol name readout
- SportsTimeApp: post UIAccessibility.screenChanged after bootstrap
completes so VoiceOver users know app is ready
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include segment index in travel anchor IDs ("travel:INDEX:from->to")
so Follow Team trips visiting the same city pair multiple times get
unique, independently addressable travel segments. Prevents override
dictionary collisions and incorrect validDayRange lookups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add VoiceOver labels, hints, and element grouping across all 60+ views
- Add Reduce Motion support (Theme.Animation.prefersReducedMotion) to all animations
- Replace fixed font sizes with semantic Dynamic Type styles
- Hide decorative elements from VoiceOver with .accessibilityHidden(true)
- Add .minimumHitTarget() modifier ensuring 44pt touch targets
- Add AccessibilityAnnouncer utility for VoiceOver announcements
- Improve color contrast values in Theme.swift for WCAG AA compliance
- Extract CloudKitContainerConfig for explicit container identity
- Remove PostHog debug console log from AnalyticsManager
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update CloudKit container ID to iCloud.com.88oakapps.SportsTime across all services
- Update IAP product IDs to match new bundle ID (com.88oakapps.SportsTime)
- Add app landing page with light, welcoming design matching app aesthetic
- Update entitlements and project configuration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Travel constraint validation was not working because ItineraryConstraints
had no game items to validate against - games came from RichGame objects
but were never converted to ItineraryItem for constraint checking.
Changes:
- Add city parameter to ItemKind.game enum case
- Create game ItineraryItems from RichGame data in buildItineraryData()
- Update isValidTravelPosition to compare against actual game sortOrders
- Fix tests to use appropriate game sortOrder conventions
Now travel is properly constrained to appear before arrival city games
and after departure city games.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add cancelPendingSync() method for explicit cleanup
- Use [weak self] capture to prevent potential retain issues
- Check Task.isCancelled before and after sleep
- Catch CancellationError from Task.sleep for immediate cancellation response
- Extract debounceInterval as constant
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Debounced updates (1.5s), local-first with silent retry.
Supports game, travel, and custom item kinds.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>