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:
@@ -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>(
|
||||
|
||||
@@ -10,7 +10,8 @@ import Foundation
|
||||
import SwiftData
|
||||
import CloudKit
|
||||
|
||||
actor CanonicalSyncService {
|
||||
@MainActor
|
||||
final class CanonicalSyncService {
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
@@ -73,7 +74,6 @@ actor CanonicalSyncService {
|
||||
/// - Parameters:
|
||||
/// - context: The ModelContext to use for saving data
|
||||
/// - cancellationToken: Optional token to check for cancellation between entity syncs
|
||||
@MainActor
|
||||
func syncAll(context: ModelContext, cancellationToken: SyncCancellationToken? = nil) async throws -> SyncResult {
|
||||
let startTime = Date()
|
||||
let syncState = SyncState.current(in: context)
|
||||
@@ -391,7 +391,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
|
||||
/// Re-enable sync after it was paused due to failures.
|
||||
@MainActor
|
||||
func resumeSync(context: ModelContext) {
|
||||
let syncState = SyncState.current(in: context)
|
||||
syncState.syncEnabled = true
|
||||
@@ -418,7 +417,6 @@ actor CanonicalSyncService {
|
||||
|
||||
// MARK: - Individual Sync Methods
|
||||
|
||||
@MainActor
|
||||
private func syncStadiums(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?,
|
||||
@@ -449,7 +447,6 @@ actor CanonicalSyncService {
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func syncTeams(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?,
|
||||
@@ -492,7 +489,6 @@ actor CanonicalSyncService {
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func syncGames(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?,
|
||||
@@ -546,7 +542,6 @@ actor CanonicalSyncService {
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func syncLeagueStructure(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?,
|
||||
@@ -571,7 +566,6 @@ actor CanonicalSyncService {
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func syncTeamAliases(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?,
|
||||
@@ -596,7 +590,6 @@ actor CanonicalSyncService {
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func syncStadiumAliases(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?,
|
||||
@@ -621,7 +614,6 @@ actor CanonicalSyncService {
|
||||
return (updated, skippedIncompatible, skippedOlder)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func syncSports(
|
||||
context: ModelContext,
|
||||
since lastSync: Date?,
|
||||
@@ -654,7 +646,6 @@ actor CanonicalSyncService {
|
||||
case skippedOlder
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeStadium(
|
||||
_ remote: Stadium,
|
||||
canonicalId: String,
|
||||
@@ -719,7 +710,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeTeam(
|
||||
_ remote: Team,
|
||||
canonicalId: String,
|
||||
@@ -810,7 +800,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeGame(
|
||||
_ remote: Game,
|
||||
canonicalId: String,
|
||||
@@ -925,7 +914,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeLeagueStructure(
|
||||
_ remote: LeagueStructureModel,
|
||||
context: ModelContext
|
||||
@@ -965,7 +953,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeTeamAlias(
|
||||
_ remote: TeamAlias,
|
||||
context: ModelContext
|
||||
@@ -1004,7 +991,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeStadiumAlias(
|
||||
_ remote: StadiumAlias,
|
||||
context: ModelContext
|
||||
@@ -1041,7 +1027,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mergeSport(
|
||||
_ remote: CanonicalSport,
|
||||
context: ModelContext
|
||||
@@ -1083,7 +1068,6 @@ actor CanonicalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func ensurePlaceholderStadium(
|
||||
canonicalId: String,
|
||||
sport: Sport,
|
||||
|
||||
@@ -372,20 +372,20 @@ final class PhotoGalleryViewModel {
|
||||
}
|
||||
|
||||
isLoadingFullImage = true
|
||||
Task {
|
||||
Task { [weak self] in
|
||||
do {
|
||||
let image = try await photoService.fetchFullImage(for: metadata)
|
||||
fullResolutionImage = image
|
||||
let image = try await self?.photoService.fetchFullImage(for: metadata)
|
||||
self?.fullResolutionImage = image
|
||||
} catch let error as PhotoServiceError {
|
||||
self.error = error
|
||||
self?.error = error
|
||||
// Fall back to thumbnail
|
||||
if let data = metadata.thumbnailData {
|
||||
fullResolutionImage = UIImage(data: data)
|
||||
self?.fullResolutionImage = UIImage(data: data)
|
||||
}
|
||||
} catch {
|
||||
self.error = .downloadFailed(error.localizedDescription)
|
||||
self?.error = .downloadFailed(error.localizedDescription)
|
||||
}
|
||||
isLoadingFullImage = false
|
||||
self?.isLoadingFullImage = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user