fix: 22 audit fixes — concurrency, memory, performance, accessibility
- Move 7 Data(contentsOf:) calls off MainActor via Task.detached (BootstrapService) - Batch-fetch N+1 queries in sync merge loops (CanonicalSyncService) - Predicate-based gamesForTeam fetch instead of fetching all games (DataProvider) - Proper Sendable on RouteInfo with nonisolated(unsafe) polyline (LocationService) - [weak self] in BGTaskScheduler register closures (BackgroundSyncManager) - Cache tripDays, routeWaypoints as @State with recompute (TripDetailView) - Remove unused AnyCancellable, add Task lifecycle management (TripDetailView) - Cache sportStadiums, recentVisits as stored properties (ProgressViewModel) - Dynamic Type fonts replacing hardcoded sizes (OnboardingPaywallView) - Accessibility labels/hints on stadium picker, date picker, map, stats, settings toggle, and day cards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import MapKit
|
||||
import Combine
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct TripDetailView: View {
|
||||
@@ -41,12 +40,14 @@ struct TripDetailView: View {
|
||||
@State private var itineraryItems: [ItineraryItem] = []
|
||||
@State private var addItemAnchor: AddItemAnchor?
|
||||
@State private var editingItem: ItineraryItem?
|
||||
@State private var subscriptionCancellable: AnyCancellable?
|
||||
@State private var mapUpdateTask: Task<Void, Never>?
|
||||
@State private var draggedItem: ItineraryItem?
|
||||
@State private var draggedTravelId: String? // Track which travel segment is being dragged
|
||||
@State private var dropTargetId: String? // Track which drop zone is being hovered
|
||||
@State private var travelOverrides: [String: TravelOverride] = [:] // Key: travel ID, Value: day + sortOrder
|
||||
@State private var cachedSections: [ItinerarySection] = []
|
||||
@State private var cachedTripDays: [Date] = []
|
||||
@State private var cachedRouteWaypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] = []
|
||||
|
||||
// Apple Maps state
|
||||
@State private var showMultiRouteAlert = false
|
||||
@@ -134,25 +135,32 @@ struct TripDetailView: View {
|
||||
await loadGamesIfNeeded()
|
||||
if allowCustomItems {
|
||||
await loadItineraryItems()
|
||||
await setupSubscription()
|
||||
}
|
||||
recomputeTripDays()
|
||||
recomputeSections()
|
||||
recomputeRouteWaypoints()
|
||||
}
|
||||
.onDisappear {
|
||||
subscriptionCancellable?.cancel()
|
||||
mapUpdateTask?.cancel()
|
||||
demoSaveTask?.cancel()
|
||||
}
|
||||
.onChange(of: itineraryItems) { _, newItems in
|
||||
handleItineraryItemsChange(newItems)
|
||||
recomputeTripDays()
|
||||
recomputeSections()
|
||||
recomputeRouteWaypoints()
|
||||
}
|
||||
.onChange(of: travelOverrides.count) { _, _ in
|
||||
draggedTravelId = nil
|
||||
dropTargetId = nil
|
||||
recomputeTripDays()
|
||||
recomputeSections()
|
||||
recomputeRouteWaypoints()
|
||||
}
|
||||
.onChange(of: loadedGames.count) { _, _ in
|
||||
recomputeTripDays()
|
||||
recomputeSections()
|
||||
recomputeRouteWaypoints()
|
||||
}
|
||||
.overlay {
|
||||
if isExporting { exportProgressOverlay }
|
||||
@@ -194,7 +202,8 @@ struct TripDetailView: View {
|
||||
print("🗺️ [MapUpdate] Mappable: \(info.title) on day \(item.day), sortOrder: \(item.sortOrder)")
|
||||
}
|
||||
}
|
||||
Task {
|
||||
mapUpdateTask?.cancel()
|
||||
mapUpdateTask = Task {
|
||||
updateMapRegion()
|
||||
await fetchDrivingRoutes()
|
||||
}
|
||||
@@ -865,9 +874,16 @@ struct TripDetailView: View {
|
||||
}
|
||||
|
||||
private var tripDays: [Date] {
|
||||
cachedTripDays
|
||||
}
|
||||
|
||||
private func recomputeTripDays() {
|
||||
let calendar = Calendar.current
|
||||
guard let startDate = trip.stops.first?.arrivalDate,
|
||||
let endDate = trip.stops.last?.departureDate else { return [] }
|
||||
let endDate = trip.stops.last?.departureDate else {
|
||||
cachedTripDays = []
|
||||
return
|
||||
}
|
||||
|
||||
var days: [Date] = []
|
||||
var current = calendar.startOfDay(for: startDate)
|
||||
@@ -877,7 +893,7 @@ struct TripDetailView: View {
|
||||
days.append(current)
|
||||
current = calendar.date(byAdding: .day, value: 1, to: current)!
|
||||
}
|
||||
return days
|
||||
cachedTripDays = days
|
||||
}
|
||||
|
||||
private func gamesOn(date: Date) -> [RichGame] {
|
||||
@@ -1045,6 +1061,10 @@ struct TripDetailView: View {
|
||||
|
||||
/// Route waypoints including both game stops and mappable custom items in itinerary order
|
||||
private var routeWaypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] {
|
||||
cachedRouteWaypoints
|
||||
}
|
||||
|
||||
private func recomputeRouteWaypoints() {
|
||||
// Build an ordered list combining game stops and mappable custom items
|
||||
// Items are ordered by (day, sortOrder) - visual order matches route order
|
||||
let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.day }
|
||||
@@ -1125,7 +1145,7 @@ struct TripDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
return waypoints
|
||||
cachedRouteWaypoints = waypoints
|
||||
}
|
||||
|
||||
private func updateMapRegion() {
|
||||
@@ -1357,13 +1377,6 @@ struct TripDetailView: View {
|
||||
|
||||
// MARK: - Itinerary Items (CloudKit persistence)
|
||||
|
||||
private func setupSubscription() async {
|
||||
// TODO: Re-implement CloudKit subscription for ItineraryItem changes
|
||||
// The subscription service was removed during the ItineraryItem refactor.
|
||||
// For now, items are only loaded on view appear.
|
||||
print("📡 [Subscription] CloudKit subscriptions not yet implemented for ItineraryItem")
|
||||
}
|
||||
|
||||
private func loadItineraryItems() async {
|
||||
print("🔍 [ItineraryItems] Loading items for trip: \(trip.id)")
|
||||
do {
|
||||
@@ -1685,6 +1698,8 @@ struct DaySection: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.cardStyle()
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("Day \(dayNumber), \(formattedDate), rest day")
|
||||
} else {
|
||||
// Full game day display
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
@@ -1719,6 +1734,8 @@ struct DaySection: View {
|
||||
}
|
||||
}
|
||||
.cardStyle()
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("Day \(dayNumber), \(formattedDate), \(games.count) game\(games.count > 1 ? "s" : "") in \(gameCity ?? "unknown city")")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2032,6 +2049,8 @@ struct TripMapView: View {
|
||||
}
|
||||
.id(routeVersion) // Force Map to recreate when routes change
|
||||
.mapStyle(colorScheme == .dark ? .standard(elevation: .flat, emphasis: .muted) : .standard)
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel("Trip route map showing \(stopCoordinates.count) stops")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user