From 8b0981474af2df68bb42b51a67507248bb2948cc Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 16 Jun 2025 13:49:53 +1000 Subject: [PATCH] chore: improve performance of indeterminate spinner --- .../Views/CircularProgressView.swift | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift b/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift index fc359e83..7b143969 100644 --- a/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift +++ b/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift @@ -8,45 +8,35 @@ struct CircularProgressView: View { var primaryColor: Color = .secondary var backgroundColor: Color = .secondary.opacity(0.3) - @State private var rotation = 0.0 - @State private var trimAmount: CGFloat = 0.15 - var autoCompleteThreshold: Float? var autoCompleteDuration: TimeInterval? var body: some View { ZStack { - // Background circle - Circle() - .stroke(backgroundColor, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round)) - .frame(width: diameter, height: diameter) - Group { - if let value { - // Determinate gauge + if let value { + ZStack { + Circle() + .stroke(backgroundColor, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round)) + Circle() .trim(from: 0, to: CGFloat(displayValue(for: value))) .stroke(primaryColor, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round)) - .frame(width: diameter, height: diameter) .rotationEffect(.degrees(-90)) .animation(autoCompleteAnimation(for: value), value: value) - } else { - // Indeterminate gauge - Circle() - .trim(from: 0, to: trimAmount) - .stroke(primaryColor, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round)) - .frame(width: diameter, height: diameter) - .rotationEffect(.degrees(rotation)) } + .frame(width: diameter, height: diameter) + + } else { + IndeterminateSpinnerView( + diameter: diameter, + strokeWidth: strokeWidth, + primaryColor: NSColor(primaryColor), + backgroundColor: NSColor(backgroundColor) + ) + .frame(width: diameter, height: diameter) } } .frame(width: diameter + strokeWidth * 2, height: diameter + strokeWidth * 2) - .onAppear { - if value == nil { - withAnimation(.linear(duration: 0.8).repeatForever(autoreverses: false)) { - rotation = 360 - } - } - } } private func displayValue(for value: Float) -> Float { @@ -78,3 +68,55 @@ extension CircularProgressView { return view } } + +// We note a constant >10% CPU usage when using a SwiftUI rotation animation that +// repeats forever, while this implementation, using Core Animation, uses <1% CPU. +struct IndeterminateSpinnerView: NSViewRepresentable { + var diameter: CGFloat + var strokeWidth: CGFloat + var primaryColor: NSColor + var backgroundColor: NSColor + + func makeNSView(context _: Context) -> NSView { + let view = NSView(frame: NSRect(x: 0, y: 0, width: diameter, height: diameter)) + view.wantsLayer = true + + guard let viewLayer = view.layer else { return view } + + let fullPath = NSBezierPath( + ovalIn: NSRect(x: 0, y: 0, width: diameter, height: diameter) + ).cgPath + + let backgroundLayer = CAShapeLayer() + backgroundLayer.path = fullPath + backgroundLayer.strokeColor = backgroundColor.cgColor + backgroundLayer.fillColor = NSColor.clear.cgColor + backgroundLayer.lineWidth = strokeWidth + viewLayer.addSublayer(backgroundLayer) + + let foregroundLayer = CAShapeLayer() + + foregroundLayer.frame = viewLayer.bounds + foregroundLayer.path = fullPath + foregroundLayer.strokeColor = primaryColor.cgColor + foregroundLayer.fillColor = NSColor.clear.cgColor + foregroundLayer.lineWidth = strokeWidth + foregroundLayer.lineCap = .round + foregroundLayer.strokeStart = 0 + foregroundLayer.strokeEnd = 0.15 + viewLayer.addSublayer(foregroundLayer) + + let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation") + rotationAnimation.fromValue = 0 + rotationAnimation.toValue = 2 * Double.pi + rotationAnimation.duration = 1.0 + rotationAnimation.repeatCount = .infinity + rotationAnimation.isRemovedOnCompletion = false + + foregroundLayer.add(rotationAnimation, forKey: "rotationAnimation") + + return view + } + + func updateNSView(_: NSView, context _: Context) {} +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy