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:
Trey t
2026-02-17 12:00:35 -06:00
parent 46434af4ab
commit 9b0cb96638
13 changed files with 415 additions and 109 deletions

View File

@@ -10,7 +10,8 @@ import Foundation
import SwiftData
import CryptoKit
actor BootstrapService {
@MainActor
final class BootstrapService {
// MARK: - Errors
@@ -117,7 +118,6 @@ actor BootstrapService {
/// Bootstrap canonical data from bundled JSON if not already done,
/// or re-bootstrap if the bundled data schema version has been bumped.
/// This is the main entry point called at app launch.
@MainActor
func bootstrapIfNeeded(context: ModelContext) async throws {
let syncState = SyncState.current(in: context)
let hasCoreCanonicalData = hasRequiredCanonicalData(context: context)
@@ -172,7 +172,6 @@ actor BootstrapService {
}
}
@MainActor
private func resetSyncProgress(_ syncState: SyncState) {
syncState.lastSuccessfulSync = nil
syncState.lastSyncAttempt = nil
@@ -198,7 +197,6 @@ actor BootstrapService {
// MARK: - Bootstrap Steps
@MainActor
private func bootstrapStadiums(context: ModelContext) async throws {
guard let url = Bundle.main.url(forResource: "stadiums_canonical", withExtension: "json") else {
throw BootstrapError.bundledResourceNotFound("stadiums_canonical.json")
@@ -235,7 +233,6 @@ actor BootstrapService {
}
}
@MainActor
private func bootstrapStadiumAliases(context: ModelContext) async throws {
guard let url = Bundle.main.url(forResource: "stadium_aliases", withExtension: "json") else {
return
@@ -278,7 +275,6 @@ actor BootstrapService {
}
}
@MainActor
private func bootstrapLeagueStructure(context: ModelContext) async throws {
guard let url = Bundle.main.url(forResource: "league_structure", withExtension: "json") else {
return
@@ -318,7 +314,6 @@ actor BootstrapService {
}
}
@MainActor
private func bootstrapTeams(context: ModelContext) async throws {
guard let url = Bundle.main.url(forResource: "teams_canonical", withExtension: "json") else {
throw BootstrapError.bundledResourceNotFound("teams_canonical.json")
@@ -352,7 +347,6 @@ actor BootstrapService {
}
}
@MainActor
private func bootstrapTeamAliases(context: ModelContext) async throws {
guard let url = Bundle.main.url(forResource: "team_aliases", withExtension: "json") else {
return
@@ -394,7 +388,6 @@ actor BootstrapService {
}
}
@MainActor
private func bootstrapGames(context: ModelContext) async throws {
guard let url = Bundle.main.url(forResource: "games_canonical", withExtension: "json") else {
throw BootstrapError.bundledResourceNotFound("games_canonical.json")
@@ -464,7 +457,6 @@ actor BootstrapService {
}
}
@MainActor
private func bootstrapSports(context: ModelContext) async throws {
guard let url = Bundle.main.url(forResource: "sports_canonical", withExtension: "json") else {
return
@@ -500,7 +492,6 @@ actor BootstrapService {
// MARK: - Helpers
@MainActor
private func clearCanonicalData(context: ModelContext) throws {
try context.delete(model: CanonicalStadium.self)
try context.delete(model: StadiumAlias.self)
@@ -511,7 +502,6 @@ actor BootstrapService {
try context.delete(model: CanonicalSport.self)
}
@MainActor
private func hasRequiredCanonicalData(context: ModelContext) -> Bool {
let stadiumCount = (try? context.fetchCount(
FetchDescriptor<CanonicalStadium>(