Skip to content

Commit bd37630

Browse files
Merge pull request #67 from componentskit/SUCircularProgress
SUCircularProgress
2 parents b30e434 + 41d4d84 commit bd37630

File tree

6 files changed

+394
-2
lines changed

6 files changed

+394
-2
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import ComponentsKit
2+
import SwiftUI
3+
import UIKit
4+
5+
struct CircularProgressPreview: View {
6+
@State private var model = Self.initialModel
7+
@State private var currentValue: CGFloat = Self.initialValue
8+
9+
private let timer = Timer
10+
.publish(every: 0.5, on: .main, in: .common)
11+
.autoconnect()
12+
13+
var body: some View {
14+
VStack {
15+
PreviewWrapper(title: "SwiftUI") {
16+
SUCircularProgress(currentValue: self.currentValue, model: self.model)
17+
}
18+
Form {
19+
ComponentColorPicker(selection: self.$model.color)
20+
Picker("Font", selection: self.$model.font) {
21+
Text("Default").tag(Optional<UniversalFont>.none)
22+
Text("Small").tag(UniversalFont.smButton)
23+
Text("Medium").tag(UniversalFont.mdButton)
24+
Text("Large").tag(UniversalFont.lgButton)
25+
Text("Custom: system bold of size 16").tag(UniversalFont.system(size: 16, weight: .bold))
26+
}
27+
Picker("Line Width", selection: self.$model.lineWidth) {
28+
Text("Default").tag(Optional<CGFloat>.none)
29+
Text("2").tag(Optional<CGFloat>.some(2))
30+
Text("4").tag(Optional<CGFloat>.some(4))
31+
Text("8").tag(Optional<CGFloat>.some(8))
32+
}
33+
SizePicker(selection: self.$model.size)
34+
Picker("Style", selection: self.$model.style) {
35+
Text("Light").tag(CircularProgressVM.Style.light)
36+
Text("Striped").tag(CircularProgressVM.Style.striped)
37+
}
38+
}
39+
.onReceive(self.timer) { _ in
40+
if self.currentValue < self.model.maxValue {
41+
let step = (self.model.maxValue - self.model.minValue) / 100
42+
self.currentValue = min(
43+
self.model.maxValue,
44+
self.currentValue + CGFloat(Int.random(in: 1...20)) * step
45+
)
46+
} else {
47+
self.currentValue = self.model.minValue
48+
}
49+
self.model.label = "\(Int(self.currentValue))%"
50+
}
51+
}
52+
}
53+
54+
// MARK: - Helpers
55+
56+
private static var initialValue: Double {
57+
return 0.0
58+
}
59+
private static var initialModel = CircularProgressVM {
60+
$0.label = "0"
61+
$0.style = .light
62+
}
63+
}
64+
65+
#Preview {
66+
CircularProgressPreview()
67+
}

Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ struct ProgressBarPreview: View {
99
private let progressBar = UKProgressBar(initialValue: Self.initialValue, model: Self.initialModel)
1010

1111
private let timer = Timer
12-
.publish(every: 0.1, on: .main, in: .common)
12+
.publish(every: 0.5, on: .main, in: .common)
1313
.autoconnect()
1414

1515
var body: some View {
@@ -43,7 +43,11 @@ struct ProgressBarPreview: View {
4343
}
4444
.onReceive(self.timer) { _ in
4545
if self.currentValue < self.model.maxValue {
46-
self.currentValue += (self.model.maxValue - self.model.minValue) / 100
46+
let step = (self.model.maxValue - self.model.minValue) / 100
47+
self.currentValue = min(
48+
self.model.maxValue,
49+
self.currentValue + CGFloat(Int.random(in: 1...20)) * step
50+
)
4751
} else {
4852
self.currentValue = self.model.minValue
4953
}

Examples/DemosApp/DemosApp/Core/App.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ struct App: View {
2626
NavigationLinkWithTitle("Checkbox") {
2727
CheckboxPreview()
2828
}
29+
NavigationLinkWithTitle("Circular Progress") {
30+
CircularProgressPreview()
31+
}
2932
NavigationLinkWithTitle("Countdown") {
3033
CountdownPreview()
3134
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
extension CircularProgressVM {
4+
public enum Style {
5+
/// Defines the visual styles for the circular progress component.
6+
case light
7+
case striped
8+
}
9+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import SwiftUI
2+
3+
/// A model that defines the appearance properties for a circular progress component.
4+
public struct CircularProgressVM: ComponentVM {
5+
/// The color of the circular progress.
6+
///
7+
/// Defaults to `.accent`.
8+
public var color: ComponentColor = .accent
9+
10+
/// The style of the circular progress indicator.
11+
///
12+
/// Defaults to `.light`.
13+
public var style: Style = .light
14+
15+
/// The size of the circular progress.
16+
///
17+
/// Defaults to `.medium`.
18+
public var size: ComponentSize = .medium
19+
20+
/// The minimum value of the circular progress.
21+
///
22+
/// Defaults to `0`.
23+
public var minValue: CGFloat = 0
24+
25+
/// The maximum value of the circular progress.
26+
///
27+
/// Defaults to `100`.
28+
public var maxValue: CGFloat = 100
29+
30+
/// The width of the circular progress stroke.
31+
public var lineWidth: CGFloat?
32+
33+
/// An optional label to display inside the circular progress.
34+
public var label: String?
35+
36+
/// The font used for the circular progress label text.
37+
public var font: UniversalFont?
38+
39+
/// Initializes a new instance of `CircularProgressVM` with default values.
40+
public init() {}
41+
}
42+
43+
// MARK: Shared Helpers
44+
45+
extension CircularProgressVM {
46+
var animationDuration: TimeInterval {
47+
return 0.2
48+
}
49+
var circularLineWidth: CGFloat {
50+
return self.lineWidth ?? max(self.preferredSize.width / 8, 2)
51+
}
52+
var preferredSize: CGSize {
53+
switch self.size {
54+
case .small:
55+
return CGSize(width: 48, height: 48)
56+
case .medium:
57+
return CGSize(width: 64, height: 64)
58+
case .large:
59+
return CGSize(width: 80, height: 80)
60+
}
61+
}
62+
var radius: CGFloat {
63+
return self.preferredSize.height / 2 - self.circularLineWidth / 2
64+
}
65+
var center: CGPoint {
66+
return .init(
67+
x: self.preferredSize.width / 2,
68+
y: self.preferredSize.height / 2
69+
)
70+
}
71+
var titleFont: UniversalFont {
72+
if let font {
73+
return font
74+
}
75+
switch self.size {
76+
case .small:
77+
return .smCaption
78+
case .medium:
79+
return .mdCaption
80+
case .large:
81+
return .lgCaption
82+
}
83+
}
84+
private func stripesCGPath(in rect: CGRect) -> CGMutablePath {
85+
let stripeWidth: CGFloat = 0.5
86+
let stripeSpacing: CGFloat = 3
87+
let stripeAngle: Angle = .degrees(135)
88+
89+
let path = CGMutablePath()
90+
let step = stripeWidth + stripeSpacing
91+
let radians = stripeAngle.radians
92+
let dx = rect.height * tan(radians)
93+
for x in stride(from: dx, through: rect.width + rect.height, by: step) {
94+
let topLeft = CGPoint(x: x, y: 0)
95+
let topRight = CGPoint(x: x + stripeWidth, y: 0)
96+
let bottomLeft = CGPoint(x: x + dx, y: rect.height)
97+
let bottomRight = CGPoint(x: x + stripeWidth + dx, y: rect.height)
98+
path.move(to: topLeft)
99+
path.addLine(to: topRight)
100+
path.addLine(to: bottomRight)
101+
path.addLine(to: bottomLeft)
102+
path.closeSubpath()
103+
}
104+
return path
105+
}
106+
}
107+
108+
extension CircularProgressVM {
109+
func gap(for normalized: CGFloat) -> CGFloat {
110+
normalized > 0 ? 0.05 : 0
111+
}
112+
113+
func progressArcStart(for normalized: CGFloat) -> CGFloat {
114+
return 0
115+
}
116+
117+
func progressArcEnd(for normalized: CGFloat) -> CGFloat {
118+
return max(0, min(1, normalized))
119+
}
120+
121+
func backgroundArcStart(for normalized: CGFloat) -> CGFloat {
122+
let gapValue = self.gap(for: normalized)
123+
return max(0, min(1, normalized + gapValue))
124+
}
125+
126+
func backgroundArcEnd(for normalized: CGFloat) -> CGFloat {
127+
let gapValue = self.gap(for: normalized)
128+
return 1 - gapValue
129+
}
130+
}
131+
132+
extension CircularProgressVM {
133+
public func progress(for currentValue: CGFloat) -> CGFloat {
134+
let range = self.maxValue - self.minValue
135+
guard range > 0 else { return 0 }
136+
let normalized = (currentValue - self.minValue) / range
137+
return max(0, min(1, normalized))
138+
}
139+
}
140+
141+
// MARK: - SwiftUI Helpers
142+
143+
extension CircularProgressVM {
144+
func stripesPath(in rect: CGRect) -> Path {
145+
Path(self.stripesCGPath(in: rect))
146+
}
147+
}

0 commit comments

Comments
 (0)