fix: 13 audit fixes — memory, concurrency, performance, accessibility

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>
This commit is contained in:
Trey t
2026-02-18 22:09:06 -06:00
parent 20ac1a7e59
commit 5511e07538
9 changed files with 196 additions and 185 deletions

View File

@@ -457,6 +457,8 @@ struct TripDetailView: View {
Text(sport.rawValue)
.font(.caption2)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(sport.rawValue)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(sport.themeColor.opacity(0.2))
@@ -968,14 +970,7 @@ struct TripDetailView: View {
private func fetchDrivingRoutes() async {
// Use routeWaypoints which includes game stops + mappable custom items
let waypoints = routeWaypoints
print("🗺️ [FetchRoutes] Computing routes with \(waypoints.count) waypoints:")
for (index, wp) in waypoints.enumerated() {
print("🗺️ [FetchRoutes] \(index): \(wp.name) (custom: \(wp.isCustomItem))")
}
guard waypoints.count >= 2 else {
print("🗺️ [FetchRoutes] Not enough waypoints, skipping")
return
}
guard waypoints.count >= 2 else { return }
isLoadingRoutes = true
var allCoordinates: [[CLLocationCoordinate2D]] = []
@@ -1013,7 +1008,6 @@ struct TripDetailView: View {
}
await MainActor.run {
print("🗺️ [FetchRoutes] Setting \(allCoordinates.count) route segments")
routeCoordinates = allCoordinates
mapUpdateTrigger = UUID() // Force map to re-render with new routes
isLoadingRoutes = false
@@ -1051,6 +1045,7 @@ struct TripDetailView: View {
// Items are ordered by (day, sortOrder) - visual order matches route order
let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.day }
#if DEBUG
print("🗺️ [Waypoints] Building waypoints. Mappable items by day:")
for (day, items) in itemsByDay.sorted(by: { $0.key < $1.key }) {
for item in items.sorted(by: { $0.sortOrder < $1.sortOrder }) {
@@ -1058,11 +1053,14 @@ struct TripDetailView: View {
print("🗺️ [Waypoints] Day \(day): \(title), sortOrder: \(item.sortOrder)")
}
}
#endif
var waypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] = []
let days = tripDays
#if DEBUG
print("🗺️ [Waypoints] Trip has \(days.count) days")
#endif
for (dayIndex, dayDate) in days.enumerated() {
let dayNumber = dayIndex + 1
@@ -1077,7 +1075,9 @@ struct TripDetailView: View {
return day >= arrival && day <= departure
})?.city
#if DEBUG
print("🗺️ [Waypoints] Day \(dayNumber): city=\(dayCity ?? "none"), games=\(gamesOnDay.count)")
#endif
// Game stop for this day (only add once per city to avoid duplicates)
if let city = dayCity {
@@ -1098,16 +1098,15 @@ struct TripDetailView: View {
if let stop = trip.stops.first(where: { $0.city == city }) {
if let stadiumId = stop.stadium,
let stadium = dataProvider.stadium(for: stadiumId) {
print("🗺️ [Waypoints] Adding \(stadium.name) (stadium)")
waypoints.append((stadium.name, stadium.coordinate, false))
} else if let coord = stop.coordinate {
// No stadium ID but stop has coordinate
print("🗺️ [Waypoints] Adding \(city) (city coord)")
waypoints.append((city, coord, false))
}
}
} else {
#if DEBUG
print("🗺️ [Waypoints] \(city) already in waypoints, skipping")
#endif
}
}
@@ -1116,7 +1115,6 @@ struct TripDetailView: View {
let sortedItems = items.sorted { $0.sortOrder < $1.sortOrder }
for item in sortedItems {
if let info = item.customInfo, let coord = info.coordinate {
print("🗺️ [Waypoints] Adding \(info.title) (sortOrder: \(item.sortOrder))")
waypoints.append((info.title, coord, true))
}
}
@@ -1776,6 +1774,8 @@ struct GameRow: View {
.padding(Theme.Spacing.sm)
.background(Theme.cardBackgroundElevated(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small))
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(game.game.sport.rawValue): \(game.awayTeam.name) at \(game.homeTeam.name), \(game.stadium.name), \(game.localGameTimeShort)")
}
}
@@ -1994,11 +1994,9 @@ struct TripMapView: View {
}
var body: some View {
let _ = print("🗺️ [TripMapView] Rendering with \(routeCoordinates.count) route segments, version: \(routeVersion)")
Map(position: $cameraPosition, interactionModes: []) {
// Routes (driving directions)
ForEach(Array(routeCoordinates.enumerated()), id: \.offset) { index, coords in
let _ = print("🗺️ [TripMapView] Drawing route \(index) with \(coords.count) points")
if !coords.isEmpty {
MapPolyline(MKPolyline(coordinates: coords, count: coords.count))
.stroke(Theme.routeGold, lineWidth: 4)