Skip to content

Commit 5b72522

Browse files
Merge pull request #80 from componentskit/improve-circular-progress-styles
Improve circular progress styles
2 parents 7128163 + 01e29b3 commit 5b72522

File tree

7 files changed

+133
-237
lines changed

7 files changed

+133
-237
lines changed

Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ struct CircularProgressPreview: View {
77
@State private var currentValue: CGFloat = Self.initialValue
88

99
private let circularProgress = UKCircularProgress(
10+
initialValue: Self.initialValue,
1011
model: Self.initialModel
1112
)
1213

@@ -36,17 +37,21 @@ struct CircularProgressPreview: View {
3637
Form {
3738
ComponentColorPicker(selection: self.$model.color)
3839
CaptionFontPicker(selection: self.$model.font)
40+
Picker("Line Cap", selection: self.$model.lineCap) {
41+
Text("Rounded").tag(CircularProgressVM.LineCap.rounded)
42+
Text("Square").tag(CircularProgressVM.LineCap.square)
43+
}
3944
Picker("Line Width", selection: self.$model.lineWidth) {
4045
Text("Default").tag(Optional<CGFloat>.none)
4146
Text("2").tag(Optional<CGFloat>.some(2))
4247
Text("4").tag(Optional<CGFloat>.some(4))
4348
Text("8").tag(Optional<CGFloat>.some(8))
4449
}
45-
SizePicker(selection: self.$model.size)
46-
Picker("Style", selection: self.$model.style) {
47-
Text("Light").tag(CircularProgressVM.Style.light)
48-
Text("Striped").tag(CircularProgressVM.Style.striped)
50+
Picker("Shape", selection: self.$model.shape) {
51+
Text("Circle").tag(CircularProgressVM.Shape.circle)
52+
Text("Arc").tag(CircularProgressVM.Shape.arc)
4953
}
54+
SizePicker(selection: self.$model.size)
5055
}
5156
.onReceive(self.timer) { _ in
5257
if self.currentValue < self.model.maxValue {
@@ -71,7 +76,6 @@ struct CircularProgressPreview: View {
7176

7277
private static var initialModel = CircularProgressVM {
7378
$0.label = "0%"
74-
$0.style = .light
7579
}
7680
}
7781

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
extension CircularProgressVM {
5+
/// Defines the style of line endings.
6+
public enum LineCap {
7+
/// The line ends with a semicircular arc that extends beyond the endpoint, creating a rounded appearance.
8+
case rounded
9+
/// The line ends exactly at the endpoint with a flat edge.
10+
case square
11+
}
12+
}
13+
14+
// MARK: - UIKit Helpers
15+
16+
extension CircularProgressVM.LineCap {
17+
var shapeLayerLineCap: CAShapeLayerLineCap {
18+
switch self {
19+
case .rounded:
20+
return .round
21+
case .square:
22+
return .butt
23+
}
24+
}
25+
}
26+
27+
// MARK: - SwiftUI Helpers
28+
29+
extension CircularProgressVM.LineCap {
30+
var cgLineCap: CGLineCap {
31+
switch self {
32+
case .rounded:
33+
return .round
34+
case .square:
35+
return .butt
36+
}
37+
}
38+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension CircularProgressVM {
4+
/// Defines the shapes for the circular progress component.
5+
public enum Shape {
6+
/// Renders a complete circle to represent the progress.
7+
case circle
8+
/// Renders only a portion of the circle (an arc) to represent progress.
9+
case arc
10+
}
11+
}

Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressStyle.swift

Lines changed: 0 additions & 9 deletions
This file was deleted.

Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift

Lines changed: 39 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,37 @@ public struct CircularProgressVM: ComponentVM {
77
/// Defaults to `.accent`.
88
public var color: ComponentColor = .accent
99

10-
/// The style of the circular progress indicator.
11-
///
12-
/// Defaults to `.light`.
13-
public var style: Style = .light
10+
/// The font used for the circular progress label text.
11+
public var font: UniversalFont?
1412

15-
/// The size of the circular progress.
16-
///
17-
/// Defaults to `.medium`.
18-
public var size: ComponentSize = .medium
13+
/// An optional label to display inside the circular progress.
14+
public var label: String?
1915

20-
/// The minimum value of the circular progress.
21-
///
22-
/// Defaults to `0`.
23-
public var minValue: CGFloat = 0
16+
/// The style of line endings.
17+
public var lineCap: LineCap = .rounded
18+
19+
/// The width of the circular progress stroke.
20+
public var lineWidth: CGFloat?
2421

2522
/// The maximum value of the circular progress.
2623
///
2724
/// Defaults to `100`.
2825
public var maxValue: CGFloat = 100
2926

30-
/// The width of the circular progress stroke.
31-
public var lineWidth: CGFloat?
27+
/// The minimum value of the circular progress.
28+
///
29+
/// Defaults to `0`.
30+
public var minValue: CGFloat = 0
3231

33-
/// An optional label to display inside the circular progress.
34-
public var label: String?
32+
/// The shape of the circular progress indicator.
33+
///
34+
/// Defaults to `.circle`.
35+
public var shape: Shape = .circle
3536

36-
/// The font used for the circular progress label text.
37-
public var font: UniversalFont?
37+
/// The size of the circular progress.
38+
///
39+
/// Defaults to `.medium`.
40+
public var size: ComponentSize = .medium
3841

3942
/// Initializes a new instance of `CircularProgressVM` with default values.
4043
public init() {}
@@ -68,6 +71,22 @@ extension CircularProgressVM {
6871
y: self.preferredSize.height / 2
6972
)
7073
}
74+
var startAngle: CGFloat {
75+
switch self.shape {
76+
case .circle:
77+
return -0.5 * .pi
78+
case .arc:
79+
return 0.75 * .pi
80+
}
81+
}
82+
var endAngle: CGFloat {
83+
switch self.shape {
84+
case .circle:
85+
return 1.5 * .pi
86+
case .arc:
87+
return 2.25 * .pi
88+
}
89+
}
7190
var titleFont: UniversalFont {
7291
if let font {
7392
return font
@@ -81,44 +100,6 @@ extension CircularProgressVM {
81100
return .lgCaption
82101
}
83102
}
84-
var stripeWidth: CGFloat {
85-
return 0.5
86-
}
87-
private func stripesCGPath(in rect: CGRect) -> CGMutablePath {
88-
let stripeSpacing: CGFloat = 3
89-
let stripeAngle: Angle = .degrees(135)
90-
91-
let path = CGMutablePath()
92-
let step = stripeWidth + stripeSpacing
93-
let radians = stripeAngle.radians
94-
95-
let dx: CGFloat = rect.height * tan(radians)
96-
for x in stride(from: 0, through: rect.width + rect.height, by: step) {
97-
let topLeft = CGPoint(x: x, y: 0)
98-
let bottomRight = CGPoint(x: x + dx, y: rect.height)
99-
100-
path.move(to: topLeft)
101-
path.addLine(to: bottomRight)
102-
path.closeSubpath()
103-
}
104-
return path
105-
}
106-
}
107-
108-
extension CircularProgressVM {
109-
func gap(for normalized: CGFloat) -> CGFloat {
110-
return normalized > 0 ? 0.05 : 0
111-
}
112-
113-
func stripedArcStart(for normalized: CGFloat) -> CGFloat {
114-
let gapValue = self.gap(for: normalized)
115-
return max(0, min(1, normalized + gapValue))
116-
}
117-
118-
func stripedArcEnd(for normalized: CGFloat) -> CGFloat {
119-
let gapValue = self.gap(for: normalized)
120-
return 1 - gapValue
121-
}
122103
}
123104

124105
extension CircularProgressVM {
@@ -133,33 +114,6 @@ extension CircularProgressVM {
133114
// MARK: - UIKit Helpers
134115

135116
extension CircularProgressVM {
136-
var isStripesLayerHidden: Bool {
137-
switch self.style {
138-
case .light:
139-
return true
140-
case .striped:
141-
return false
142-
}
143-
}
144-
var isBackgroundLayerHidden: Bool {
145-
switch self.style {
146-
case .light:
147-
return false
148-
case .striped:
149-
return true
150-
}
151-
}
152-
func stripesBezierPath(in rect: CGRect) -> UIBezierPath {
153-
let center = CGPoint(x: rect.midX, y: rect.midY)
154-
let path = UIBezierPath(cgPath: self.stripesCGPath(in: rect))
155-
var transform = CGAffineTransform.identity
156-
transform = transform
157-
.translatedBy(x: center.x, y: center.y)
158-
.rotated(by: -CGFloat.pi / 2)
159-
.translatedBy(x: -center.x, y: -center.y)
160-
path.apply(transform)
161-
return path
162-
}
163117
func shouldInvalidateIntrinsicContentSize(_ oldModel: Self) -> Bool {
164118
return self.preferredSize != oldModel.preferredSize
165119
}
@@ -170,12 +124,7 @@ extension CircularProgressVM {
170124
return self.minValue != oldModel.minValue
171125
|| self.maxValue != oldModel.maxValue
172126
}
173-
}
174-
175-
// MARK: - SwiftUI Helpers
176-
177-
extension CircularProgressVM {
178-
func stripesPath(in rect: CGRect) -> Path {
179-
Path(self.stripesCGPath(in: rect))
127+
func shouldUpdateShape(_ oldModel: Self) -> Bool {
128+
return self.shape != oldModel.shape
180129
}
181130
}
Lines changed: 19 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import SwiftUI
22

3-
/// A SwiftUI component that displays a circular progress.
3+
/// A SwiftUI component that displays the progress of a task or operation in a circular form.
44
public struct SUCircularProgress: View {
55
// MARK: - Properties
66

@@ -33,14 +33,22 @@ public struct SUCircularProgress: View {
3333
public var body: some View {
3434
ZStack {
3535
// Background part
36-
Group {
37-
switch self.model.style {
38-
case .light:
39-
self.lightBackground
40-
case .striped:
41-
self.stripedBackground
42-
}
36+
Path { path in
37+
path.addArc(
38+
center: self.model.center,
39+
radius: self.model.radius,
40+
startAngle: .radians(self.model.startAngle),
41+
endAngle: .radians(self.model.endAngle),
42+
clockwise: false
43+
)
4344
}
45+
.stroke(
46+
self.model.color.background.color,
47+
style: StrokeStyle(
48+
lineWidth: self.model.circularLineWidth,
49+
lineCap: self.model.lineCap.cgLineCap
50+
)
51+
)
4452
.frame(
4553
width: self.model.preferredSize.width,
4654
height: self.model.preferredSize.height
@@ -51,8 +59,8 @@ public struct SUCircularProgress: View {
5159
path.addArc(
5260
center: self.model.center,
5361
radius: self.model.radius,
54-
startAngle: .radians(0),
55-
endAngle: .radians(2 * .pi),
62+
startAngle: .radians(self.model.startAngle),
63+
endAngle: .radians(self.model.endAngle),
5664
clockwise: false
5765
)
5866
}
@@ -61,10 +69,9 @@ public struct SUCircularProgress: View {
6169
self.model.color.main.color,
6270
style: StrokeStyle(
6371
lineWidth: self.model.circularLineWidth,
64-
lineCap: .round
72+
lineCap: self.model.lineCap.cgLineCap
6573
)
6674
)
67-
.rotationEffect(.degrees(-90))
6875
.frame(
6976
width: self.model.preferredSize.width,
7077
height: self.model.preferredSize.height
@@ -82,62 +89,4 @@ public struct SUCircularProgress: View {
8289
value: self.progress
8390
)
8491
}
85-
86-
// MARK: - Subviews
87-
88-
var lightBackground: some View {
89-
Path { path in
90-
path.addArc(
91-
center: self.model.center,
92-
radius: self.model.radius,
93-
startAngle: .radians(0),
94-
endAngle: .radians(2 * .pi),
95-
clockwise: false
96-
)
97-
}
98-
.stroke(
99-
self.model.color.background.color,
100-
lineWidth: self.model.circularLineWidth
101-
)
102-
}
103-
104-
var stripedBackground: some View {
105-
StripesShapeCircularProgress(model: self.model)
106-
.stroke(
107-
self.model.color.main.color,
108-
style: StrokeStyle(lineWidth: self.model.stripeWidth)
109-
)
110-
.mask {
111-
Path { maskPath in
112-
maskPath.addArc(
113-
center: self.model.center,
114-
radius: self.model.radius,
115-
startAngle: .radians(0),
116-
endAngle: .radians(2 * .pi),
117-
clockwise: false
118-
)
119-
}
120-
.trim(
121-
from: self.model.stripedArcStart(for: self.progress),
122-
to: self.model.stripedArcEnd(for: self.progress)
123-
)
124-
.stroke(
125-
style: StrokeStyle(
126-
lineWidth: self.model.circularLineWidth,
127-
lineCap: .round
128-
)
129-
)
130-
}
131-
.rotationEffect(.degrees(-90))
132-
}
133-
}
134-
135-
// MARK: - Helpers
136-
137-
struct StripesShapeCircularProgress: Shape, @unchecked Sendable {
138-
var model: CircularProgressVM
139-
140-
func path(in rect: CGRect) -> Path {
141-
self.model.stripesPath(in: rect)
142-
}
14392
}

0 commit comments

Comments
 (0)