@@ -8,45 +8,35 @@ struct CircularProgressView: View {
88var primaryColor : Color = . secondary
99var backgroundColor : Color = . secondary. opacity ( 0.3 )
1010
11- @State private var rotation = 0.0
12- @State private var trimAmount : CGFloat = 0.15
13-
1411var autoCompleteThreshold : Float ?
1512var autoCompleteDuration : TimeInterval ?
1613
1714var body : some View {
1815ZStack {
19- // Background circle
20- Circle ( )
21- . stroke ( backgroundColor, style: StrokeStyle ( lineWidth: strokeWidth, lineCap: . round) )
22- . frame ( width: diameter, height: diameter)
23- Group {
24- if let value{
25- // Determinate gauge
16+ if let value{
17+ ZStack {
18+ Circle ( )
19+ . stroke ( backgroundColor, style: StrokeStyle ( lineWidth: strokeWidth, lineCap: . round) )
20+
2621Circle ( )
2722. trim ( from: 0 , to: CGFloat ( displayValue ( for: value) ) )
2823. stroke ( primaryColor, style: StrokeStyle ( lineWidth: strokeWidth, lineCap: . round) )
29- . frame ( width: diameter, height: diameter)
3024. rotationEffect ( . degrees( - 90 ) )
3125. animation ( autoCompleteAnimation ( for: value) , value: value)
32- } else {
33- // Indeterminate gauge
34- Circle ( )
35- . trim ( from: 0 , to: trimAmount)
36- . stroke ( primaryColor, style: StrokeStyle ( lineWidth: strokeWidth, lineCap: . round) )
37- . frame ( width: diameter, height: diameter)
38- . rotationEffect ( . degrees( rotation) )
3926}
27+ . frame ( width: diameter, height: diameter)
28+
29+ } else {
30+ IndeterminateSpinnerView (
31+ diameter: diameter,
32+ strokeWidth: strokeWidth,
33+ primaryColor: NSColor ( primaryColor) ,
34+ backgroundColor: NSColor ( backgroundColor)
35+ )
36+ . frame ( width: diameter, height: diameter)
4037}
4138}
4239. frame ( width: diameter+ strokeWidth* 2 , height: diameter+ strokeWidth* 2 )
43- . onAppear {
44- if value== nil {
45- withAnimation ( . linear( duration: 0.8 ) . repeatForever ( autoreverses: false ) ) {
46- rotation= 360
47- }
48- }
49- }
5040}
5141
5242private func displayValue( for value: Float ) -> Float {
@@ -78,3 +68,55 @@ extension CircularProgressView {
7868return view
7969}
8070}
71+
72+ // We note a constant >10% CPU usage when using a SwiftUI rotation animation that
73+ // repeats forever, while this implementation, using Core Animation, uses <1% CPU.
74+ struct IndeterminateSpinnerView : NSViewRepresentable {
75+ var diameter : CGFloat
76+ var strokeWidth : CGFloat
77+ var primaryColor : NSColor
78+ var backgroundColor : NSColor
79+
80+ func makeNSView( context _: Context ) -> NSView {
81+ let view = NSView ( frame: NSRect ( x: 0 , y: 0 , width: diameter, height: diameter) )
82+ view. wantsLayer= true
83+
84+ guard let viewLayer= view. layerelse { return view}
85+
86+ let fullPath = NSBezierPath (
87+ ovalIn: NSRect ( x: 0 , y: 0 , width: diameter, height: diameter)
88+ ) . cgPath
89+
90+ let backgroundLayer = CAShapeLayer ( )
91+ backgroundLayer. path= fullPath
92+ backgroundLayer. strokeColor= backgroundColor. cgColor
93+ backgroundLayer. fillColor= NSColor . clear. cgColor
94+ backgroundLayer. lineWidth= strokeWidth
95+ viewLayer. addSublayer ( backgroundLayer)
96+
97+ let foregroundLayer = CAShapeLayer ( )
98+
99+ foregroundLayer. frame= viewLayer. bounds
100+ foregroundLayer. path= fullPath
101+ foregroundLayer. strokeColor= primaryColor. cgColor
102+ foregroundLayer. fillColor= NSColor . clear. cgColor
103+ foregroundLayer. lineWidth= strokeWidth
104+ foregroundLayer. lineCap= . round
105+ foregroundLayer. strokeStart= 0
106+ foregroundLayer. strokeEnd= 0.15
107+ viewLayer. addSublayer ( foregroundLayer)
108+
109+ let rotationAnimation = CABasicAnimation ( keyPath: " transform.rotation " )
110+ rotationAnimation. fromValue= 0
111+ rotationAnimation. toValue= 2 * Double. pi
112+ rotationAnimation. duration= 1.0
113+ rotationAnimation. repeatCount= . infinity
114+ rotationAnimation. isRemovedOnCompletion= false
115+
116+ foregroundLayer. add ( rotationAnimation, forKey: " rotationAnimation " )
117+
118+ return view
119+ }
120+
121+ func updateNSView( _: NSView , context _: Context ) { }
122+ }