fix: 22 audit fixes — concurrency, memory, performance, accessibility
- Move 7 Data(contentsOf:) calls off MainActor via Task.detached (BootstrapService) - Batch-fetch N+1 queries in sync merge loops (CanonicalSyncService) - Predicate-based gamesForTeam fetch instead of fetching all games (DataProvider) - Proper Sendable on RouteInfo with nonisolated(unsafe) polyline (LocationService) - [weak self] in BGTaskScheduler register closures (BackgroundSyncManager) - Cache tripDays, routeWaypoints as @State with recompute (TripDetailView) - Remove unused AnyCancellable, add Task lifecycle management (TripDetailView) - Cache sportStadiums, recentVisits as stored properties (ProgressViewModel) - Dynamic Type fonts replacing hardcoded sizes (OnboardingPaywallView) - Accessibility labels/hints on stadium picker, date picker, map, stats, settings toggle, and day cards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -429,11 +429,16 @@ final class CanonicalSyncService {
|
||||
var skippedIncompatible = 0
|
||||
var skippedOlder = 0
|
||||
|
||||
// Batch-fetch all existing stadiums to avoid N+1 FetchDescriptor lookups
|
||||
let allExistingStadiums = try context.fetch(FetchDescriptor<CanonicalStadium>())
|
||||
let existingStadiumsByCanonicalId = Dictionary(grouping: allExistingStadiums, by: \.canonicalId).compactMapValues(\.first)
|
||||
|
||||
for syncStadium in syncStadiums {
|
||||
// Use canonical ID directly from CloudKit - no UUID-based generation!
|
||||
let result = try mergeStadium(
|
||||
syncStadium.stadium,
|
||||
canonicalId: syncStadium.canonicalId,
|
||||
existingRecord: existingStadiumsByCanonicalId[syncStadium.canonicalId],
|
||||
context: context
|
||||
)
|
||||
|
||||
@@ -465,6 +470,10 @@ final class CanonicalSyncService {
|
||||
var skippedIncompatible = 0
|
||||
var skippedOlder = 0
|
||||
|
||||
// Batch-fetch all existing teams to avoid N+1 FetchDescriptor lookups
|
||||
let allExistingTeams = try context.fetch(FetchDescriptor<CanonicalTeam>())
|
||||
let existingTeamsByCanonicalId = Dictionary(grouping: allExistingTeams, by: \.canonicalId).compactMapValues(\.first)
|
||||
|
||||
for syncTeam in allSyncTeams {
|
||||
// Use canonical IDs directly from CloudKit - no UUID lookups!
|
||||
let result = try mergeTeam(
|
||||
@@ -472,6 +481,7 @@ final class CanonicalSyncService {
|
||||
canonicalId: syncTeam.canonicalId,
|
||||
stadiumCanonicalId: syncTeam.stadiumCanonicalId,
|
||||
validStadiumIds: &validStadiumIds,
|
||||
existingRecord: existingTeamsByCanonicalId[syncTeam.canonicalId],
|
||||
context: context
|
||||
)
|
||||
|
||||
@@ -514,6 +524,10 @@ final class CanonicalSyncService {
|
||||
var skippedIncompatible = 0
|
||||
var skippedOlder = 0
|
||||
|
||||
// Batch-fetch all existing games to avoid N+1 FetchDescriptor lookups
|
||||
let allExistingGames = try context.fetch(FetchDescriptor<CanonicalGame>())
|
||||
let existingGamesByCanonicalId = Dictionary(grouping: allExistingGames, by: \.canonicalId).compactMapValues(\.first)
|
||||
|
||||
for syncGame in syncGames {
|
||||
// Use canonical IDs directly from CloudKit - no UUID lookups!
|
||||
let result = try mergeGame(
|
||||
@@ -525,6 +539,7 @@ final class CanonicalSyncService {
|
||||
validTeamIds: validTeamIds,
|
||||
teamStadiumByTeamId: teamStadiumByTeamId,
|
||||
validStadiumIds: &validStadiumIds,
|
||||
existingRecord: existingGamesByCanonicalId[syncGame.canonicalId],
|
||||
context: context
|
||||
)
|
||||
|
||||
@@ -649,13 +664,19 @@ final class CanonicalSyncService {
|
||||
private func mergeStadium(
|
||||
_ remote: Stadium,
|
||||
canonicalId: String,
|
||||
existingRecord: CanonicalStadium? = nil,
|
||||
context: ModelContext
|
||||
) throws -> MergeResult {
|
||||
// Look up existing
|
||||
let descriptor = FetchDescriptor<CanonicalStadium>(
|
||||
predicate: #Predicate { $0.canonicalId == canonicalId }
|
||||
)
|
||||
let existing = try context.fetch(descriptor).first
|
||||
// Use pre-fetched record if available, otherwise fall back to individual fetch
|
||||
let existing: CanonicalStadium?
|
||||
if let existingRecord {
|
||||
existing = existingRecord
|
||||
} else {
|
||||
let descriptor = FetchDescriptor<CanonicalStadium>(
|
||||
predicate: #Predicate { $0.canonicalId == canonicalId }
|
||||
)
|
||||
existing = try context.fetch(descriptor).first
|
||||
}
|
||||
|
||||
if let existing = existing {
|
||||
// Preserve user fields
|
||||
@@ -715,12 +736,19 @@ final class CanonicalSyncService {
|
||||
canonicalId: String,
|
||||
stadiumCanonicalId: String?,
|
||||
validStadiumIds: inout Set<String>,
|
||||
existingRecord: CanonicalTeam? = nil,
|
||||
context: ModelContext
|
||||
) throws -> MergeResult {
|
||||
let descriptor = FetchDescriptor<CanonicalTeam>(
|
||||
predicate: #Predicate { $0.canonicalId == canonicalId }
|
||||
)
|
||||
let existing = try context.fetch(descriptor).first
|
||||
// Use pre-fetched record if available, otherwise fall back to individual fetch
|
||||
let existing: CanonicalTeam?
|
||||
if let existingRecord {
|
||||
existing = existingRecord
|
||||
} else {
|
||||
let descriptor = FetchDescriptor<CanonicalTeam>(
|
||||
predicate: #Predicate { $0.canonicalId == canonicalId }
|
||||
)
|
||||
existing = try context.fetch(descriptor).first
|
||||
}
|
||||
|
||||
let remoteStadiumId = stadiumCanonicalId?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let existingStadiumId = existing?.stadiumCanonicalId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -809,12 +837,19 @@ final class CanonicalSyncService {
|
||||
validTeamIds: Set<String>,
|
||||
teamStadiumByTeamId: [String: String],
|
||||
validStadiumIds: inout Set<String>,
|
||||
existingRecord: CanonicalGame? = nil,
|
||||
context: ModelContext
|
||||
) throws -> MergeResult {
|
||||
let descriptor = FetchDescriptor<CanonicalGame>(
|
||||
predicate: #Predicate { $0.canonicalId == canonicalId }
|
||||
)
|
||||
let existing = try context.fetch(descriptor).first
|
||||
// Use pre-fetched record if available, otherwise fall back to individual fetch
|
||||
let existing: CanonicalGame?
|
||||
if let existingRecord {
|
||||
existing = existingRecord
|
||||
} else {
|
||||
let descriptor = FetchDescriptor<CanonicalGame>(
|
||||
predicate: #Predicate { $0.canonicalId == canonicalId }
|
||||
)
|
||||
existing = try context.fetch(descriptor).first
|
||||
}
|
||||
|
||||
func resolveTeamId(remote: String, existing: String?) -> String? {
|
||||
if validTeamIds.contains(remote) {
|
||||
|
||||
Reference in New Issue
Block a user