fix: 10 audit fixes — memory safety, performance, accessibility, architecture
- Add a11y label to ProgressMapView reset button and progress bar values - Fix CADisplayLink retain cycle in ItineraryTableViewController via deinit - Add [weak self] to PhotoGalleryViewModel Task closure - Add @MainActor to TripWizardViewModel, remove manual MainActor.run hop - Fix O(n²) rank lookup in PollDetailView/DebugPollPreviewView with enumerated() - Cache itinerarySections via ItinerarySectionBuilder static extraction + @State - Convert CanonicalSyncService/BootstrapService from actor to @MainActor final class - Add .accessibilityHidden(true) to RegionMapSelector Map to prevent duplicate VoiceOver Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ struct TripDetailView: View {
|
||||
@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] = []
|
||||
|
||||
// Apple Maps state
|
||||
@State private var showMultiRouteAlert = false
|
||||
@@ -134,14 +135,20 @@ struct TripDetailView: View {
|
||||
await loadItineraryItems()
|
||||
await setupSubscription()
|
||||
}
|
||||
recomputeSections()
|
||||
}
|
||||
.onDisappear { subscriptionCancellable?.cancel() }
|
||||
.onChange(of: itineraryItems) { _, newItems in
|
||||
handleItineraryItemsChange(newItems)
|
||||
recomputeSections()
|
||||
}
|
||||
.onChange(of: travelOverrides.count) { _, _ in
|
||||
draggedTravelId = nil
|
||||
dropTargetId = nil
|
||||
recomputeSections()
|
||||
}
|
||||
.onChange(of: loadedGames.count) { _, _ in
|
||||
recomputeSections()
|
||||
}
|
||||
.overlay {
|
||||
if isExporting { exportProgressOverlay }
|
||||
@@ -536,7 +543,7 @@ struct TripDetailView: View {
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
VStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in
|
||||
ForEach(Array(cachedSections.enumerated()), id: \.offset) { index, section in
|
||||
itineraryRow(for: section, at: index)
|
||||
}
|
||||
}
|
||||
@@ -801,11 +808,11 @@ struct TripDetailView: View {
|
||||
private func findDayForTravelSegment(_ segment: TravelSegment) -> Int {
|
||||
// Find which day this travel segment belongs to by looking at sections
|
||||
// Travel appears BEFORE the arrival day, so look FORWARD to find arrival day
|
||||
for (index, section) in itinerarySections.enumerated() {
|
||||
for (index, section) in cachedSections.enumerated() {
|
||||
if case .travel(let s, _) = section, s.id == segment.id {
|
||||
// Look forward to find the arrival day
|
||||
for i in (index + 1)..<itinerarySections.count {
|
||||
if case .day(let dayNumber, _, _) = itinerarySections[i] {
|
||||
for i in (index + 1)..<cachedSections.count {
|
||||
if case .day(let dayNumber, _, _) = cachedSections[i] {
|
||||
return dayNumber
|
||||
}
|
||||
}
|
||||
@@ -816,9 +823,7 @@ struct TripDetailView: View {
|
||||
|
||||
/// Create a stable anchor ID for a travel segment (UUIDs regenerate on reload)
|
||||
private func stableTravelAnchorId(_ segment: TravelSegment, at index: Int) -> String {
|
||||
let from = TravelInfo.normalizeCityName(segment.fromLocation.name)
|
||||
let to = TravelInfo.normalizeCityName(segment.toLocation.name)
|
||||
return "travel:\(index):\(from)->\(to)"
|
||||
ItinerarySectionBuilder.stableTravelAnchorId(segment, at: index)
|
||||
}
|
||||
|
||||
/// Move item to a new day and sortOrder position
|
||||
@@ -841,63 +846,16 @@ struct TripDetailView: View {
|
||||
print("✅ [Move] Synced \(title) with day: \(day), sortOrder: \(sortOrder)")
|
||||
}
|
||||
|
||||
/// Build itinerary sections: shows ALL days with travel, custom items, and add buttons
|
||||
private var itinerarySections: [ItinerarySection] {
|
||||
var sections: [ItinerarySection] = []
|
||||
let days = tripDays
|
||||
|
||||
// Use TravelPlacement for consistent day calculation (shared with tests).
|
||||
var travelByDay = TravelPlacement.computeTravelByDay(trip: trip, tripDays: days)
|
||||
|
||||
// Apply user overrides on top of computed defaults.
|
||||
for (segmentIndex, segment) in trip.travelSegments.enumerated() {
|
||||
let travelId = stableTravelAnchorId(segment, at: segmentIndex)
|
||||
guard let override = travelOverrides[travelId] else { continue }
|
||||
|
||||
// Validate override is within valid day range
|
||||
if let validRange = validDayRange(for: travelId),
|
||||
validRange.contains(override.day) {
|
||||
// Remove from computed position (search all days)
|
||||
for key in travelByDay.keys {
|
||||
travelByDay[key]?.removeAll { $0.id == segment.id }
|
||||
}
|
||||
travelByDay.keys.forEach { key in
|
||||
if travelByDay[key]?.isEmpty == true { travelByDay[key] = nil }
|
||||
}
|
||||
// Place at overridden position
|
||||
travelByDay[override.day, default: []].append(segment)
|
||||
}
|
||||
}
|
||||
|
||||
// Process ALL days
|
||||
for (index, dayDate) in days.enumerated() {
|
||||
let dayNum = index + 1
|
||||
let gamesOnDay = gamesOn(date: dayDate)
|
||||
|
||||
// Travel for this day (if any) - appears before day header
|
||||
for travelSegment in (travelByDay[dayNum] ?? []) {
|
||||
let segIdx = trip.travelSegments.firstIndex(where: { $0.id == travelSegment.id }) ?? 0
|
||||
sections.append(.travel(travelSegment, segmentIndex: segIdx))
|
||||
}
|
||||
|
||||
// Day section - shows games or minimal rest day display
|
||||
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
|
||||
|
||||
// Custom items for this day (sorted by sortOrder)
|
||||
if allowCustomItems {
|
||||
// Add button first - always right after day header
|
||||
sections.append(.addButton(day: dayNum))
|
||||
|
||||
let dayItems = itineraryItems
|
||||
.filter { $0.day == dayNum && $0.isCustom }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in dayItems {
|
||||
sections.append(.customItem(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
/// Recompute cached itinerary sections from current state.
|
||||
private func recomputeSections() {
|
||||
cachedSections = ItinerarySectionBuilder.build(
|
||||
trip: trip,
|
||||
tripDays: tripDays,
|
||||
games: games,
|
||||
travelOverrides: travelOverrides,
|
||||
itineraryItems: itineraryItems,
|
||||
allowCustomItems: allowCustomItems
|
||||
)
|
||||
}
|
||||
|
||||
private var tripDays: [Date] {
|
||||
|
||||
Reference in New Issue
Block a user