diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNProgress.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNProgress.swift index 939606e..a914614 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNProgress.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNProgress.swift @@ -13,9 +13,12 @@ struct VPNProgressView: View { var body: some View { VStack { CircularProgressView(value: value) - // We estimate that the last half takes 8 seconds + // We estimate the duration of the last 35% // so it doesn't appear stuck - .autoComplete(threshold: 0.5, duration: 8) + .autoComplete(threshold: 0.65, duration: 8) + // We estimate the duration of the first 25% (spawning Helper) + // so it doesn't appear stuck + .autoStart(until: 0.25, duration: 2) Text(progressMessage) .multilineTextAlignment(.center) } @@ -46,16 +49,16 @@ struct VPNProgressView: View { guard let downloadProgress = progress.downloadProgress else { // We can't make this illegal state unrepresentable because XPC // doesn't support enums with associated values. - return 0.05 + return 0.15 } // 35MB if the server doesn't give us the expected size let totalBytes = downloadProgress.totalBytesToWrite ?? 35_000_000 let downloadPercent = min(1.0, Float(downloadProgress.totalBytesWritten) / Float(totalBytes)) - return 0.4 * downloadPercent + return 0.25 + (0.35 * downloadPercent) case .validating: - return 0.43 + return 0.63 case .startingTunnel: - return 0.50 + return 0.65 } } } diff --git a/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift b/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift index 7b14396..3f97aa1 100644 --- a/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift +++ b/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift @@ -3,13 +3,24 @@ import SwiftUI struct CircularProgressView: View { let value: Float? - var strokeWidth: CGFloat = 4 - var diameter: CGFloat = 22 + var strokeWidth: CGFloat + var diameter: CGFloat var primaryColor: Color = .secondary var backgroundColor: Color = .secondary.opacity(0.3) - var autoCompleteThreshold: Float? - var autoCompleteDuration: TimeInterval? + private var autoComplete: (threshold: Float, duration: TimeInterval)? + private var autoStart: (until: Float, duration: TimeInterval)? + + @State private var currentProgress: Float = 0 + + init(value: Float? = nil, + strokeWidth: CGFloat = 4, + diameter: CGFloat = 22) + { + self.value = value + self.strokeWidth = strokeWidth + self.diameter = diameter + } var body: some View { ZStack { @@ -19,13 +30,23 @@ struct CircularProgressView: View { .stroke(backgroundColor, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round)) Circle() - .trim(from: 0, to: CGFloat(displayValue(for: value))) + .trim(from: 0, to: CGFloat(displayValue(for: currentProgress))) .stroke(primaryColor, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round)) .rotationEffect(.degrees(-90)) - .animation(autoCompleteAnimation(for: value), value: value) } .frame(width: diameter, height: diameter) - + .onAppear { + if let autoStart, value == 0 { + withAnimation(.easeOut(duration: autoStart.duration)) { + currentProgress = autoStart.until + } + } + } + .onChange(of: value) { + withAnimation(currentAnimation(for: value)) { + currentProgress = value + } + } } else { IndeterminateSpinnerView( diameter: diameter, @@ -40,7 +61,7 @@ struct CircularProgressView: View { } private func displayValue(for value: Float) -> Float { - if let threshold = autoCompleteThreshold, + if let threshold = autoComplete?.threshold, value >= threshold, value < 1.0 { return 1.0 @@ -48,23 +69,31 @@ struct CircularProgressView: View { return value } - private func autoCompleteAnimation(for value: Float) -> Animation? { - guard let threshold = autoCompleteThreshold, - let duration = autoCompleteDuration, - value >= threshold, value < 1.0 + private func currentAnimation(for value: Float) -> Animation { + guard let autoComplete, + value >= autoComplete.threshold, value < 1.0 else { + // Use the auto-start animation if it's running, otherwise default. + if let autoStart { + return .easeOut(duration: autoStart.duration) + } return .default } - return .easeOut(duration: duration) + return .easeOut(duration: autoComplete.duration) } } extension CircularProgressView { func autoComplete(threshold: Float, duration: TimeInterval) -> CircularProgressView { var view = self - view.autoCompleteThreshold = threshold - view.autoCompleteDuration = duration + view.autoComplete = (threshold: threshold, duration: duration) + return view + } + + func autoStart(until value: Float, duration: TimeInterval) -> CircularProgressView { + var view = self + view.autoStart = (until: value, duration: duration) return view } }