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:
@@ -109,6 +109,7 @@ struct AnimatedSportsIcon: View {
|
||||
let index: Int
|
||||
let animate: Bool
|
||||
@State private var glowOpacity: Double = 0
|
||||
@State private var glowTask: Task<Void, Never>?
|
||||
|
||||
static let configs: [(x: CGFloat, y: CGFloat, icon: String, rotation: Double, scale: CGFloat)] = [
|
||||
// Edge icons
|
||||
@@ -162,37 +163,28 @@ struct AnimatedSportsIcon: View {
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
startRandomGlow()
|
||||
}
|
||||
}
|
||||
|
||||
private func startRandomGlow() {
|
||||
let initialDelay = Double.random(in: 2.0...8.0)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) {
|
||||
triggerGlow()
|
||||
}
|
||||
}
|
||||
|
||||
private func triggerGlow() {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
|
||||
// Slow fade in
|
||||
withAnimation(.easeIn(duration: 0.8)) {
|
||||
glowOpacity = 1
|
||||
}
|
||||
|
||||
// Hold briefly then slow fade out
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
|
||||
withAnimation(.easeOut(duration: 1.0)) {
|
||||
glowOpacity = 0
|
||||
glowTask = Task { @MainActor in
|
||||
// Random initial delay
|
||||
try? await Task.sleep(for: .seconds(Double.random(in: 2.0...8.0)))
|
||||
while !Task.isCancelled {
|
||||
guard !Theme.Animation.prefersReducedMotion else {
|
||||
try? await Task.sleep(for: .seconds(6.0))
|
||||
continue
|
||||
}
|
||||
// Slow fade in
|
||||
withAnimation(.easeIn(duration: 0.8)) { glowOpacity = 1 }
|
||||
// Hold briefly then slow fade out
|
||||
try? await Task.sleep(for: .seconds(1.2))
|
||||
guard !Task.isCancelled else { break }
|
||||
withAnimation(.easeOut(duration: 1.0)) { glowOpacity = 0 }
|
||||
// Wait before next glow
|
||||
try? await Task.sleep(for: .seconds(Double.random(in: 6.0...12.0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Longer interval between glows
|
||||
let nextGlow = Double.random(in: 6.0...12.0)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + nextGlow) {
|
||||
triggerGlow()
|
||||
.onDisappear {
|
||||
glowTask?.cancel()
|
||||
glowTask = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user