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:
@@ -17,6 +17,36 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Cached Formatters
|
||||
|
||||
private enum GameFormatters {
|
||||
static let timeFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.timeStyle = .short
|
||||
return f
|
||||
}()
|
||||
static let dateFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateStyle = .medium
|
||||
return f
|
||||
}()
|
||||
static let dayOfWeekFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEEE"
|
||||
return f
|
||||
}()
|
||||
static let localTimeFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "h:mm a z"
|
||||
return f
|
||||
}()
|
||||
static let localTimeShortFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "h:mm a"
|
||||
return f
|
||||
}()
|
||||
}
|
||||
|
||||
struct Game: Identifiable, Codable, Hashable {
|
||||
let id: String // Canonical ID: "game_mlb_2026_bos_nyy_0401"
|
||||
let homeTeamId: String // FK: "team_mlb_bos"
|
||||
@@ -58,21 +88,15 @@ struct Game: Identifiable, Codable, Hashable {
|
||||
var startTime: Date { dateTime }
|
||||
|
||||
var gameTime: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: dateTime)
|
||||
GameFormatters.timeFormatter.string(from: dateTime)
|
||||
}
|
||||
|
||||
var formattedDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter.string(from: dateTime)
|
||||
GameFormatters.dateFormatter.string(from: dateTime)
|
||||
}
|
||||
|
||||
var dayOfWeek: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE"
|
||||
return formatter.string(from: dateTime)
|
||||
GameFormatters.dayOfWeekFormatter.string(from: dateTime)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,16 +130,14 @@ struct RichGame: Identifiable, Hashable, Codable {
|
||||
|
||||
/// Game time formatted in the stadium's local timezone with timezone indicator
|
||||
var localGameTime: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "h:mm a z"
|
||||
let formatter = GameFormatters.localTimeFormatter
|
||||
formatter.timeZone = stadium.timeZone ?? .current
|
||||
return formatter.string(from: game.dateTime)
|
||||
}
|
||||
|
||||
/// Game time formatted in the stadium's local timezone without timezone indicator
|
||||
var localGameTimeShort: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "h:mm a"
|
||||
let formatter = GameFormatters.localTimeShortFormatter
|
||||
formatter.timeZone = stadium.timeZone ?? .current
|
||||
return formatter.string(from: game.dateTime)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user