Skip to content

Commit 6702d10

Browse files
UNT-T21025 Stick Animations
1 parent f2d8c97 commit 6702d10

File tree

10 files changed

+959
-9
lines changed

10 files changed

+959
-9
lines changed

SSSwiftUIAnimations.xcodeproj/project.pbxproj

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
223F502E2C2E95F3006C68CE /* CircularLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F502D2C2E95F3006C68CE /* CircularLoading.swift */; };
11+
223F50342C2E9636006C68CE /* LinearLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F502F2C2E9636006C68CE /* LinearLoading.swift */; };
12+
223F50352C2E9636006C68CE /* LinearProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50302C2E9636006C68CE /* LinearProgress.swift */; };
13+
223F50362C2E9636006C68CE /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50312C2E9636006C68CE /* CircularProgress.swift */; };
14+
223F50372C2E9636006C68CE /* CircularReverseProgreessBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50322C2E9636006C68CE /* CircularReverseProgreessBar.swift */; };
15+
223F50382C2E9636006C68CE /* Stick.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50332C2E9636006C68CE /* Stick.swift */; };
16+
223F503A2C2E991B006C68CE /* StickAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50392C2E991B006C68CE /* StickAnimations.swift */; };
17+
223F503C2C2EACCD006C68CE /* StickAnimationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F503B2C2EACCD006C68CE /* StickAnimationType.swift */; };
1018
2BC2D8F328CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F228CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift */; };
1119
2BC2D8F528CF3A6F00CAB302 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */; };
1220
2BC2D8F728CF3A7000CAB302 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F628CF3A7000CAB302 /* Assets.xcassets */; };
@@ -43,6 +51,14 @@
4351
/* End PBXBuildFile section */
4452

4553
/* Begin PBXFileReference section */
54+
223F502D2C2E95F3006C68CE /* CircularLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularLoading.swift; sourceTree = "<group>"; };
55+
223F502F2C2E9636006C68CE /* LinearLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearLoading.swift; sourceTree = "<group>"; };
56+
223F50302C2E9636006C68CE /* LinearProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearProgress.swift; sourceTree = "<group>"; };
57+
223F50312C2E9636006C68CE /* CircularProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = "<group>"; };
58+
223F50322C2E9636006C68CE /* CircularReverseProgreessBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularReverseProgreessBar.swift; sourceTree = "<group>"; };
59+
223F50332C2E9636006C68CE /* Stick.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stick.swift; sourceTree = "<group>"; };
60+
223F50392C2E991B006C68CE /* StickAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickAnimations.swift; sourceTree = "<group>"; };
61+
223F503B2C2EACCD006C68CE /* StickAnimationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickAnimationType.swift; sourceTree = "<group>"; };
4662
2BC2D8EF28CF3A6F00CAB302 /* SSSwiftUIAnimations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SSSwiftUIAnimations.app; sourceTree = BUILT_PRODUCTS_DIR; };
4763
2BC2D8F228CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSSwiftUIAnimationsApp.swift; sourceTree = "<group>"; };
4864
2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -90,6 +106,21 @@
90106
/* End PBXFrameworksBuildPhase section */
91107

92108
/* Begin PBXGroup section */
109+
223F502C2C2E95D6006C68CE /* SticksAnimations */ = {
110+
isa = PBXGroup;
111+
children = (
112+
223F502D2C2E95F3006C68CE /* CircularLoading.swift */,
113+
223F50312C2E9636006C68CE /* CircularProgress.swift */,
114+
223F50322C2E9636006C68CE /* CircularReverseProgreessBar.swift */,
115+
223F502F2C2E9636006C68CE /* LinearLoading.swift */,
116+
223F50302C2E9636006C68CE /* LinearProgress.swift */,
117+
223F50332C2E9636006C68CE /* Stick.swift */,
118+
223F50392C2E991B006C68CE /* StickAnimations.swift */,
119+
223F503B2C2EACCD006C68CE /* StickAnimationType.swift */,
120+
);
121+
path = SticksAnimations;
122+
sourceTree = "<group>";
123+
};
93124
2BC2D8E628CF3A6F00CAB302 = {
94125
isa = PBXGroup;
95126
children = (
@@ -180,6 +211,7 @@
180211
isa = PBXGroup;
181212
children = (
182213
B780A9162C3D05CD00342512 /* WaterProgressAnimation */,
214+
223F502C2C2E95D6006C68CE /* SticksAnimations */,
183215
B14AB36A2BC40286004B09C4 /* ProgressAnimation */,
184216
469963A3290FCE1900DC01AD /* ArrowLeftRightAnimation */,
185217
);
@@ -297,6 +329,8 @@
297329
B177713F2BF39A60001723EC /* ModelClass.swift in Sources */,
298330
B10677FE2BE8D0D400957B4E /* DownArrow.swift in Sources */,
299331
B1098E7D2BD94ED900BC19DD /* WaveView.swift in Sources */,
332+
223F50372C2E9636006C68CE /* CircularReverseProgreessBar.swift in Sources */,
333+
223F503C2C2EACCD006C68CE /* StickAnimationType.swift in Sources */,
300334
B19E0B662BF7498700E65974 /* ExampleProgressView.swift in Sources */,
301335
B18792612AA5A0D2006F2CC9 /* CircularView.swift in Sources */,
302336
B153FD152BFB7A7900AEFE83 /* ExampleLRArrowView.swift in Sources */,
@@ -308,16 +342,22 @@
308342
B717EC9D2C45488100555F90 /* CheckmarkView.swift in Sources */,
309343
B780A9282C3D851700342512 /* WaterCircleView.swift in Sources */,
310344
B153FD132BFB71F500AEFE83 /* FilledStrokeCircle.swift in Sources */,
345+
223F50362C2E9636006C68CE /* CircularProgress.swift in Sources */,
346+
223F50352C2E9636006C68CE /* LinearProgress.swift in Sources */,
347+
223F502E2C2E95F3006C68CE /* CircularLoading.swift in Sources */,
311348
2BC2D8F328CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift in Sources */,
312349
B741D9A62C46448200ABFCB4 /* WaterProgressTextView.swift in Sources */,
313350
B780A9242C3D7A7C00342512 /* WaterCircleOutlineView.swift in Sources */,
314351
B1DFCA532BF4FC7900F01505 /* ArrowView.swift in Sources */,
315352
B780A9182C3D063500342512 /* WaterProgressView.swift in Sources */,
353+
223F50382C2E9636006C68CE /* Stick.swift in Sources */,
354+
223F50342C2E9636006C68CE /* LinearLoading.swift in Sources */,
316355
B11B983A2BCE9C3F00D76016 /* CheckView.swift in Sources */,
317356
B7ECD58D2C452D8100B6A703 /* BubbleView.swift in Sources */,
318357
B780A9262C3D806300342512 /* ExampleWaterProgressView.swift in Sources */,
319358
B780A91A2C3D0BCB00342512 /* WaterProgressViewStyle.swift in Sources */,
320359
B1DFCA512BF4FA3D00F01505 /* ProgressCircle.swift in Sources */,
360+
223F503A2C2E991B006C68CE /* StickAnimations.swift in Sources */,
321361
B14AB36C2BC41B05004B09C4 /* ProgressView.swift in Sources */,
322362
B1FE861E2BFF6BC000FB111C /* ViewExtension.swift in Sources */,
323363
);

SSSwiftUIAnimations/ContentView.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@
88
import SwiftUI
99

1010
struct ContentView: View {
11+
@State private var percentage: Double = 0
1112
private var exampleList = ExampleListModel.exampleList
1213

1314
var body: some View {
1415
NavigationView {
15-
ZStack {
16-
List{
17-
ForEach(exampleList) {item in
18-
ExampleListRow(exampleListItem: item)
19-
}
20-
}.listStyle(.insetGrouped)
21-
.listRowSpacing(10)
22-
.listRowSeparator(.hidden)
23-
.customToolbar(title: "Examples", fontSize: 30, displayMode: .inline)
16+
VStack {
17+
StickAnimations(
18+
type: .linearLoading(),
19+
duration: 3
20+
)
21+
.frame(width: 150, height: 50)
22+
Text(String(percentage))
23+
Slider(value: $percentage, in: 0...100, step: 1)
24+
.padding()
2425
}
2526
}
2627
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// CircularLoading.swift
3+
// SSSwiftUIAnimations
4+
//
5+
// Created by Brijesh Barasiya on 29/01/24.
6+
//
7+
8+
import SwiftUI
9+
10+
struct CircularLoading: View {
11+
/// The sticks to be displayed in the loading indicator.
12+
@State private var sticks: [Stick]
13+
/// The size of the circle.
14+
private let circleSize: CGFloat
15+
/// The width of each stick.
16+
private let stickWidth: CGFloat
17+
/// The color of a filled stick.
18+
private let filledColor: Color
19+
/// The color of an unfilled stick.
20+
private let unFilledColor: Color
21+
/// The duration of the animation for each stick.
22+
private let perStickDuration: Double
23+
24+
/// Initializes the `CircularLoading` view.
25+
/// - Parameters:
26+
/// - size: The size of the view.
27+
/// - filledColor: The color of a filled stick.
28+
/// - unFilledColor: The color of an unfilled stick.
29+
/// - duration: The total duration of the animation.
30+
init(
31+
size: CGSize,
32+
filledColor: Color,
33+
unFilledColor: Color,
34+
duration: Double
35+
) {
36+
let adjustedSize = min(size.width, size.height)
37+
let adjustedStickWidth = adjustedSize * 0.05
38+
let totalStickCount: Int = Int(adjustedSize / (adjustedStickWidth * 0.75))
39+
self.sticks = Array(
40+
repeating: Stick(xAxis: 0, stickHeight: (adjustedSize * 0.20), color: unFilledColor),
41+
count: totalStickCount
42+
)
43+
self.circleSize = CGFloat(adjustedSize)
44+
self.stickWidth = CGFloat(adjustedStickWidth)
45+
self.filledColor = filledColor
46+
self.unFilledColor = unFilledColor
47+
self.perStickDuration = duration / Double(totalStickCount)
48+
}
49+
50+
var body: some View {
51+
Circle()
52+
.frame(width: circleSize)
53+
.foregroundColor(Color.clear)
54+
.overlay {
55+
ForEach(0..<sticks.count, id: \.self) { index in
56+
Rectangle()
57+
.frame(width: stickWidth, height: sticks[index].stickHeight)
58+
.foregroundColor(sticks[index].color)
59+
.offset(y: (circleSize - sticks[index].stickHeight) / 2)
60+
.rotationEffect(
61+
.degrees(Double((CGFloat(index) + sticks[index].xAxis) * 360) / Double(sticks.count))
62+
)
63+
}
64+
}
65+
.onAppear {
66+
animateStickView(index: 0, color: filledColor)
67+
}
68+
}
69+
70+
/// Starts and resets the animation on a particular stick view.
71+
/// - Parameters:
72+
/// - index: The index of the current stick.
73+
/// - color: The color to animate to.
74+
private func animateStickView(index: Int, color: Color) {
75+
if #available(iOS 17.0, *) {
76+
withAnimation(Animation.linear(duration: perStickDuration)) {
77+
updateStickViewProperties(index: index, color: color)
78+
} completion: {
79+
resetStickViewAnimation(index: index, color: color)
80+
}
81+
} else {
82+
withAnimation(Animation.linear(duration: perStickDuration)) {
83+
updateStickViewProperties(index: index, color: color)
84+
}
85+
DispatchQueue.main.asyncAfter(deadline: .now() + perStickDuration) {
86+
resetStickViewAnimation(index: index, color: color)
87+
}
88+
}
89+
}
90+
91+
/// Updates the properties of the stick view for animation.
92+
/// - Parameters:
93+
/// - index: The index of the current stick.
94+
/// - color: The color to animate to.
95+
private func updateStickViewProperties(index: Int, color: Color) {
96+
sticks[index].xAxis = 0.6
97+
sticks[index].color = color
98+
}
99+
100+
/// Resets the properties of the stick view after animation.
101+
/// - Parameters:
102+
/// - index: The index of the current stick.
103+
/// - color: The color to animate to.
104+
private func resetStickViewAnimation(index: Int, color: Color) {
105+
withAnimation(Animation.linear(duration: perStickDuration * 10)) {
106+
sticks[index].xAxis = 0
107+
}
108+
if index == sticks.indices.last {
109+
let newColor = switch color {
110+
case unFilledColor: filledColor
111+
case filledColor: unFilledColor
112+
default: filledColor
113+
}
114+
animateStickView(index: 0, color: newColor)
115+
} else {
116+
animateStickView(index: index + 1, color: color)
117+
}
118+
}
119+
}
120+
121+
#Preview {
122+
CircularLoading(
123+
size: CGSize(width: 150, height: 150),
124+
filledColor: .black,
125+
unFilledColor: .gray,
126+
duration: 1
127+
)
128+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//
2+
// CircularLoading.swift
3+
// SSSwiftUIAnimations
4+
//
5+
// Created by Brijesh Barasiya on 29/01/24.
6+
//
7+
8+
import SwiftUI
9+
10+
struct CircularProgress: View {
11+
/// The percentage of progress to be displayed.
12+
@Binding private var percentage: Double
13+
/// The sticks to be displayed in the loading indicator.
14+
@State private var sticks: [Stick]
15+
/// The size of the circle.
16+
private let circleSize: CGFloat
17+
/// The width of each stick.
18+
private let stickWidth: CGFloat
19+
/// The color of a filled stick.
20+
private let filledColor: Color
21+
/// The color of an unfilled stick.
22+
private let unFilledColor: Color
23+
/// The duration of the animation for each stick.
24+
private let perStickDuration: Double
25+
26+
/// Initializes the `CircularProgress` view.
27+
/// - Parameters:
28+
/// - percentage: The binding to the progress percentage.
29+
/// - size: The size of the view.
30+
/// - filledColor: The color of a filled stick.
31+
/// - unFilledColor: The color of an unfilled stick.
32+
/// - duration: The total duration of the animation.
33+
init(
34+
percentage: Binding<Double>,
35+
size: CGSize,
36+
filledColor: Color,
37+
unFilledColor: Color,
38+
duration: Double
39+
) {
40+
let adjustedSize = min(size.width, size.height)
41+
let adjustedStickWidth = adjustedSize * 0.05
42+
let totalStickCount: Int = Int(adjustedSize / (adjustedStickWidth * 0.75))
43+
self._percentage = percentage
44+
self.sticks = Array(
45+
repeating: Stick(xAxis: 0, stickHeight: (adjustedSize * 0.20), color: unFilledColor),
46+
count: totalStickCount
47+
)
48+
self.circleSize = CGFloat(adjustedSize)
49+
self.stickWidth = CGFloat(adjustedStickWidth)
50+
self.filledColor = filledColor
51+
self.unFilledColor = unFilledColor
52+
self.perStickDuration = duration / Double(totalStickCount)
53+
54+
}
55+
56+
var body: some View {
57+
Circle()
58+
.frame(width: circleSize)
59+
.foregroundColor(Color.clear)
60+
.overlay {
61+
ForEach(0..<sticks.count, id: \.self) { index in
62+
Rectangle()
63+
.frame(width: stickWidth, height: sticks[index].stickHeight)
64+
.foregroundColor(sticks[index].color)
65+
.offset(y: (circleSize - sticks[index].stickHeight) / 2)
66+
.rotationEffect(
67+
.degrees(Double((CGFloat(index) + sticks[index].xAxis) * 360) / Double(sticks.count))
68+
)
69+
}
70+
}
71+
.onAppear {
72+
animateStickView(index: 0, color: filledColor)
73+
}
74+
}
75+
76+
/// Starts and resets the animation on a particular stick view.
77+
/// - Parameters:
78+
/// - index: The index of the current stick.
79+
/// - color: The color to animate to.
80+
private func animateStickView(index: Int, color: Color) {
81+
if #available(iOS 17.0, *) {
82+
withAnimation(Animation.linear(duration: perStickDuration)) {
83+
updateStickViewProperties(index: index, color: color)
84+
} completion: {
85+
resertStickViewAnimation(index: index, color: color)
86+
}
87+
} else {
88+
withAnimation(Animation.linear(duration: perStickDuration)) {
89+
updateStickViewProperties(index: index, color: color)
90+
}
91+
DispatchQueue.main.asyncAfter(deadline: .now() + perStickDuration) {
92+
resertStickViewAnimation(index: index, color: color)
93+
}
94+
}
95+
}
96+
97+
/// Updates the properties of the stick view for animation.
98+
/// - Parameters:
99+
/// - index: The index of the current stick.
100+
/// - color: The color to animate to.
101+
private func updateStickViewProperties(index: Int, color: Color) {
102+
changeStictsColor(color: unFilledColor)
103+
let validatedPercentage = min(max(0, percentage), 100)
104+
let sticksAccordingToPercentage = Double(sticks.count) * Double(validatedPercentage / 200)
105+
let numberOfSticksToChange = max(Int(sticksAccordingToPercentage), 0)
106+
for stickIndex in 0..<Int(numberOfSticksToChange) {
107+
updateStickColor(at: index + stickIndex, color: filledColor)
108+
updateStickColor(at: (index - 1) - stickIndex, color: filledColor)
109+
}
110+
if Double(numberOfSticksToChange) != round(sticksAccordingToPercentage) {
111+
updateStickColor(at: index + (Int(numberOfSticksToChange)), color: filledColor)
112+
}
113+
}
114+
115+
/// Changes the color of all sticks.
116+
/// - Parameter color: The color to set for all sticks.
117+
private func changeStictsColor(color: Color) {
118+
sticks.indices.forEach { updateStickColor(at: $0, color: color) }
119+
}
120+
121+
/// Updates the color of a specific stick.
122+
/// - Parameters:
123+
/// - index: The index of the stick to update.
124+
/// - color: The color to set.
125+
private func updateStickColor(at index: Int, color: Color) {
126+
let adjustedIndex = (index + sticks.count) % sticks.count
127+
sticks[adjustedIndex].color = color
128+
}
129+
130+
/// Resets the properties of the stick view after animation.
131+
/// - Parameters:
132+
/// - index: The index of the current stick.
133+
/// - color: The color to animate to.
134+
private func resertStickViewAnimation(index: Int, color: Color) {
135+
let nextIndex = (index == sticks.indices.last) ? 0 : index + 1
136+
animateStickView(index: nextIndex, color: color)
137+
}
138+
}
139+
140+
#Preview {
141+
CircularProgress(
142+
percentage: .constant(75),
143+
size: CGSize(width: 150, height: 150),
144+
filledColor: .green,
145+
unFilledColor: .gray,
146+
duration: 1
147+
)
148+
}

0 commit comments

Comments
 (0)