import SwiftUI @MainActor @Observable final class SplashViewModel { private(set) var isVisible = true private(set) var loadingProgress: Double = 0 private static var hasCompletedThisSession = false private var hasStarted = false private let minimumDisplayDuration: Duration = .seconds(1.35) init() { if Self.hasCompletedThisSession { isVisible = false loadingProgress = 1 } } func runIfNeeded() async { guard !Self.hasCompletedThisSession, !hasStarted else { return } hasStarted = true await run() Self.hasCompletedThisSession = true } private func run() async { withAnimation(.easeOut(duration: 0.25)) { loadingProgress = 0.08 } async let networkPrepare: Void = NetworkClient.prepare() async let minimumDelay: Void = waitMinimumDuration() async let progressAnimation: Void = animateLoadingProgress() _ = await (networkPrepare, minimumDelay, progressAnimation) withAnimation(.easeOut(duration: 0.22)) { loadingProgress = 1 } try? await Task.sleep(for: .milliseconds(150)) withAnimation(.easeInOut(duration: 0.45)) { isVisible = false } } private func waitMinimumDuration() async { try? await Task.sleep(for: minimumDisplayDuration) } private func animateLoadingProgress() async { let start: Double = 0.08 let target: Double = 0.92 let duration = 1.35 let steps = 30 let stepDuration = duration / Double(steps) for step in 1...steps { if Task.isCancelled { return } let fraction = Double(step) / Double(steps) let eased = start + (target - start) * easeOutCubic(fraction) withAnimation(.linear(duration: stepDuration)) { loadingProgress = eased } try? await Task.sleep(for: .seconds(stepDuration)) } } private func easeOutCubic(_ value: Double) -> Double { 1 - pow(1 - value, 3) } }