fix: comprehensive codebase hardening — crashes, silent failures, performance, and security
Fixes ~95 issues from deep audit across 12 categories in 82 files: - Crash prevention: double-resume in PhotoMetadataExtractor, force unwraps in DateRangePicker, array bounds checks in polls/achievements, ProGate hit-test bypass, Dictionary(uniqueKeysWithValues:) → uniquingKeysWith in 4 files - Silent failure elimination: all 34 try? sites replaced with do/try/catch + logging (SavedTrip, TripDetailView, CanonicalSyncService, BootstrapService, CanonicalModels, CKModels, SportsTimeApp, and more) - Performance: cached DateFormatters (7 files), O(1) team lookups via AppDataProvider, achievement definition dictionary, AnimatedBackground consolidated from 19 Tasks to 1, task cancellation in SharePreviewView - Concurrency: UIKit drawing → MainActor.run, background fetch timeout guard, @MainActor on ThemeManager/AppearanceManager, SyncLogger read/write race fix - Planning engine: game end time in travel feasibility, state-aware city normalization, exact city matching, DrivingConstraints parameter propagation - IAP: unknown subscription states → expired, unverified transaction logging, entitlements updated before paywall dismiss, restore visible to all users - Security: API key to Info.plist lookup, filename sanitization in PDF export, honest User-Agent, removed stale "Feels" analytics super properties - Navigation: consolidated competing navigationDestination, boolean → value-based - Testing: 8 sleep() → waitForExistence, duplicates extracted, Swift 6 compat - Service bugs: infinite retry cap, duplicate achievement prevention, TOCTOU vote fix, PollVote.odg → voterId rename, deterministic placeholder IDs, parallel MKDirections, Sendable-safe POI struct Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ import SwiftUI
|
||||
struct AnimatedSportsBackground: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State private var animate = false
|
||||
@State private var glowOpacities: [Double] = Array(repeating: 0, count: AnimatedSportsIcon.configs.count)
|
||||
@State private var glowDriverTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -25,7 +27,7 @@ struct AnimatedSportsBackground: View {
|
||||
|
||||
// Floating sports icons with gentle glow
|
||||
ForEach(0..<AnimatedSportsIcon.configs.count, id: \.self) { index in
|
||||
AnimatedSportsIcon(index: index, animate: animate)
|
||||
AnimatedSportsIcon(index: index, animate: animate, glowOpacity: glowOpacities[index])
|
||||
}
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
@@ -34,6 +36,60 @@ struct AnimatedSportsBackground: View {
|
||||
withAnimation(.easeInOut(duration: 5.0).repeatForever(autoreverses: true)) {
|
||||
animate = true
|
||||
}
|
||||
startGlowDriver()
|
||||
}
|
||||
.onDisappear {
|
||||
animate = false
|
||||
glowDriverTask?.cancel()
|
||||
glowDriverTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Single task that drives glow animations for all icons
|
||||
private func startGlowDriver() {
|
||||
glowDriverTask = Task { @MainActor in
|
||||
let iconCount = AnimatedSportsIcon.configs.count
|
||||
// Track next glow time for each icon
|
||||
var nextGlowTime: [TimeInterval] = (0..<iconCount).map { _ in
|
||||
Double.random(in: 2.0...8.0)
|
||||
}
|
||||
// Track glow state: 0 = waiting, 1 = glowing, 2 = fading out
|
||||
var glowState: [Int] = Array(repeating: 0, count: iconCount)
|
||||
var stateTimer: [TimeInterval] = Array(repeating: 0, count: iconCount)
|
||||
|
||||
let tickInterval: TimeInterval = 0.5
|
||||
let startTime = Date.timeIntervalSinceReferenceDate
|
||||
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(tickInterval))
|
||||
guard !Task.isCancelled else { break }
|
||||
|
||||
let elapsed = Date.timeIntervalSinceReferenceDate - startTime
|
||||
|
||||
for i in 0..<iconCount {
|
||||
switch glowState[i] {
|
||||
case 0: // Waiting
|
||||
if elapsed >= nextGlowTime[i] {
|
||||
withAnimation(.easeIn(duration: 0.8)) { glowOpacities[i] = 1 }
|
||||
glowState[i] = 1
|
||||
stateTimer[i] = elapsed
|
||||
}
|
||||
case 1: // Glowing - hold for 1.2s
|
||||
if elapsed - stateTimer[i] >= 1.2 {
|
||||
withAnimation(.easeOut(duration: 1.0)) { glowOpacities[i] = 0 }
|
||||
glowState[i] = 2
|
||||
stateTimer[i] = elapsed
|
||||
}
|
||||
case 2: // Fading out - wait 1.0s then schedule next
|
||||
if elapsed - stateTimer[i] >= 1.0 {
|
||||
glowState[i] = 0
|
||||
nextGlowTime[i] = elapsed + Double.random(in: 6.0...12.0)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,8 +164,7 @@ struct RouteMapLayer: View {
|
||||
struct AnimatedSportsIcon: View {
|
||||
let index: Int
|
||||
let animate: Bool
|
||||
@State private var glowOpacity: Double = 0
|
||||
@State private var glowTask: Task<Void, Never>?
|
||||
var glowOpacity: Double = 0
|
||||
|
||||
static let configs: [(x: CGFloat, y: CGFloat, icon: String, rotation: Double, scale: CGFloat)] = [
|
||||
// Edge icons
|
||||
@@ -162,29 +217,5 @@ struct AnimatedSportsIcon: View {
|
||||
value: animate
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
glowTask?.cancel()
|
||||
glowTask = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,9 @@ enum AppTheme: String, CaseIterable, Identifiable {
|
||||
|
||||
// MARK: - Theme Manager
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class ThemeManager: @unchecked Sendable {
|
||||
final class ThemeManager {
|
||||
static let shared = ThemeManager()
|
||||
|
||||
var currentTheme: AppTheme {
|
||||
@@ -130,8 +131,9 @@ enum AppearanceMode: String, CaseIterable, Identifiable {
|
||||
|
||||
// MARK: - Appearance Manager
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class AppearanceManager: @unchecked Sendable {
|
||||
final class AppearanceManager {
|
||||
static let shared = AppearanceManager()
|
||||
|
||||
var currentMode: AppearanceMode {
|
||||
@@ -154,7 +156,7 @@ final class AppearanceManager: @unchecked Sendable {
|
||||
|
||||
enum Theme {
|
||||
|
||||
private static var current: AppTheme { ThemeManager.shared.currentTheme }
|
||||
private static var current: AppTheme { MainActor.assumeIsolated { ThemeManager.shared.currentTheme } }
|
||||
|
||||
// MARK: - Primary Accent Color
|
||||
|
||||
|
||||
Reference in New Issue
Block a user