Skip to content

Commit

Permalink
UNT-T21025 Stick Animations
Browse files Browse the repository at this point in the history
  • Loading branch information
brijeshbarasiya2022 committed Aug 2, 2024
1 parent f2d8c97 commit e22146a
Show file tree
Hide file tree
Showing 9 changed files with 967 additions and 0 deletions.
40 changes: 40 additions & 0 deletions SSSwiftUIAnimations.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
objects = {

/* Begin PBXBuildFile section */
223F502E2C2E95F3006C68CE /* CircularLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F502D2C2E95F3006C68CE /* CircularLoading.swift */; };
223F50342C2E9636006C68CE /* LinearLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F502F2C2E9636006C68CE /* LinearLoading.swift */; };
223F50352C2E9636006C68CE /* LinearProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50302C2E9636006C68CE /* LinearProgress.swift */; };
223F50362C2E9636006C68CE /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50312C2E9636006C68CE /* CircularProgress.swift */; };
223F50372C2E9636006C68CE /* CircularReverseProgreessBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50322C2E9636006C68CE /* CircularReverseProgreessBar.swift */; };
223F50382C2E9636006C68CE /* Stick.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50332C2E9636006C68CE /* Stick.swift */; };
223F503A2C2E991B006C68CE /* StickAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F50392C2E991B006C68CE /* StickAnimations.swift */; };
223F503C2C2EACCD006C68CE /* StickAnimationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223F503B2C2EACCD006C68CE /* StickAnimationType.swift */; };
2BC2D8F328CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F228CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift */; };
2BC2D8F528CF3A6F00CAB302 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */; };
2BC2D8F728CF3A7000CAB302 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F628CF3A7000CAB302 /* Assets.xcassets */; };
Expand Down Expand Up @@ -43,6 +51,14 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
223F502D2C2E95F3006C68CE /* CircularLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularLoading.swift; sourceTree = "<group>"; };
223F502F2C2E9636006C68CE /* LinearLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearLoading.swift; sourceTree = "<group>"; };
223F50302C2E9636006C68CE /* LinearProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearProgress.swift; sourceTree = "<group>"; };
223F50312C2E9636006C68CE /* CircularProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = "<group>"; };
223F50322C2E9636006C68CE /* CircularReverseProgreessBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularReverseProgreessBar.swift; sourceTree = "<group>"; };
223F50332C2E9636006C68CE /* Stick.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stick.swift; sourceTree = "<group>"; };
223F50392C2E991B006C68CE /* StickAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickAnimations.swift; sourceTree = "<group>"; };
223F503B2C2EACCD006C68CE /* StickAnimationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickAnimationType.swift; sourceTree = "<group>"; };
2BC2D8EF28CF3A6F00CAB302 /* SSSwiftUIAnimations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SSSwiftUIAnimations.app; sourceTree = BUILT_PRODUCTS_DIR; };
2BC2D8F228CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSSwiftUIAnimationsApp.swift; sourceTree = "<group>"; };
2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -90,6 +106,21 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
223F502C2C2E95D6006C68CE /* SticksAnimations */ = {
isa = PBXGroup;
children = (
223F502D2C2E95F3006C68CE /* CircularLoading.swift */,
223F50312C2E9636006C68CE /* CircularProgress.swift */,
223F50322C2E9636006C68CE /* CircularReverseProgreessBar.swift */,
223F502F2C2E9636006C68CE /* LinearLoading.swift */,
223F50302C2E9636006C68CE /* LinearProgress.swift */,
223F50332C2E9636006C68CE /* Stick.swift */,
223F50392C2E991B006C68CE /* StickAnimations.swift */,
223F503B2C2EACCD006C68CE /* StickAnimationType.swift */,
);
path = SticksAnimations;
sourceTree = "<group>";
};
2BC2D8E628CF3A6F00CAB302 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -180,6 +211,7 @@
isa = PBXGroup;
children = (
B780A9162C3D05CD00342512 /* WaterProgressAnimation */,
223F502C2C2E95D6006C68CE /* SticksAnimations */,
B14AB36A2BC40286004B09C4 /* ProgressAnimation */,
469963A3290FCE1900DC01AD /* ArrowLeftRightAnimation */,
);
Expand Down Expand Up @@ -297,6 +329,8 @@
B177713F2BF39A60001723EC /* ModelClass.swift in Sources */,
B10677FE2BE8D0D400957B4E /* DownArrow.swift in Sources */,
B1098E7D2BD94ED900BC19DD /* WaveView.swift in Sources */,
223F50372C2E9636006C68CE /* CircularReverseProgreessBar.swift in Sources */,
223F503C2C2EACCD006C68CE /* StickAnimationType.swift in Sources */,
B19E0B662BF7498700E65974 /* ExampleProgressView.swift in Sources */,
B18792612AA5A0D2006F2CC9 /* CircularView.swift in Sources */,
B153FD152BFB7A7900AEFE83 /* ExampleLRArrowView.swift in Sources */,
Expand All @@ -308,16 +342,22 @@
B717EC9D2C45488100555F90 /* CheckmarkView.swift in Sources */,
B780A9282C3D851700342512 /* WaterCircleView.swift in Sources */,
B153FD132BFB71F500AEFE83 /* FilledStrokeCircle.swift in Sources */,
223F50362C2E9636006C68CE /* CircularProgress.swift in Sources */,
223F50352C2E9636006C68CE /* LinearProgress.swift in Sources */,
223F502E2C2E95F3006C68CE /* CircularLoading.swift in Sources */,
2BC2D8F328CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift in Sources */,
B741D9A62C46448200ABFCB4 /* WaterProgressTextView.swift in Sources */,
B780A9242C3D7A7C00342512 /* WaterCircleOutlineView.swift in Sources */,
B1DFCA532BF4FC7900F01505 /* ArrowView.swift in Sources */,
B780A9182C3D063500342512 /* WaterProgressView.swift in Sources */,
223F50382C2E9636006C68CE /* Stick.swift in Sources */,
223F50342C2E9636006C68CE /* LinearLoading.swift in Sources */,
B11B983A2BCE9C3F00D76016 /* CheckView.swift in Sources */,
B7ECD58D2C452D8100B6A703 /* BubbleView.swift in Sources */,
B780A9262C3D806300342512 /* ExampleWaterProgressView.swift in Sources */,
B780A91A2C3D0BCB00342512 /* WaterProgressViewStyle.swift in Sources */,
B1DFCA512BF4FA3D00F01505 /* ProgressCircle.swift in Sources */,
223F503A2C2E991B006C68CE /* StickAnimations.swift in Sources */,
B14AB36C2BC41B05004B09C4 /* ProgressView.swift in Sources */,
B1FE861E2BFF6BC000FB111C /* ViewExtension.swift in Sources */,
);
Expand Down
128 changes: 128 additions & 0 deletions SSSwiftUIAnimations/Sources/SticksAnimations/CircularLoading.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// CircularLoading.swift
// SSSwiftUIAnimations
//
// Created by Brijesh Barasiya on 29/01/24.
//

import SwiftUI

struct CircularLoading: View {
/// The sticks to be displayed in the loading indicator.
@State private var sticks: [Stick]
/// The size of the circle.
private let circleSize: CGFloat
/// The width of each stick.
private let stickWidth: CGFloat
/// The color of a filled stick.
private let filledColor: Color
/// The color of an unfilled stick.
private let unFilledColor: Color
/// The duration of the animation for each stick.
private let perStickDuration: Double

/// Initializes the `CircularLoading` view.
/// - Parameters:
/// - size: The size of the view.
/// - filledColor: The color of a filled stick.
/// - unFilledColor: The color of an unfilled stick.
/// - duration: The total duration of the animation.
init(
size: CGSize,
filledColor: Color,
unFilledColor: Color,
duration: Double
) {
let adjustedSize = min(size.width, size.height)
let adjustedStickWidth = adjustedSize * 0.05
let totalStickCount: Int = Int(adjustedSize / (adjustedStickWidth * 0.75))
self.sticks = Array(
repeating: Stick(xAxis: 0, stickHeight: (adjustedSize * 0.20), color: unFilledColor),
count: totalStickCount
)
self.circleSize = CGFloat(adjustedSize)
self.stickWidth = CGFloat(adjustedStickWidth)
self.filledColor = filledColor
self.unFilledColor = unFilledColor
self.perStickDuration = duration / Double(totalStickCount)
}

var body: some View {
Circle()
.frame(width: circleSize)
.foregroundColor(Color.clear)
.overlay {
ForEach(0..<sticks.count, id: \.self) { index in
Rectangle()
.frame(width: stickWidth, height: sticks[index].stickHeight)
.foregroundColor(sticks[index].color)
.offset(y: (circleSize - sticks[index].stickHeight) / 2)
.rotationEffect(
.degrees(Double((CGFloat(index) + sticks[index].xAxis) * 360) / Double(sticks.count))
)
}
}
.onAppear {
animateStickView(index: 0, color: filledColor)
}
}

/// Starts and resets the animation on a particular stick view.
/// - Parameters:
/// - index: The index of the current stick.
/// - color: The color to animate to.
private func animateStickView(index: Int, color: Color) {
if #available(iOS 17.0, *) {
withAnimation(Animation.linear(duration: perStickDuration)) {
updateStickViewProperties(index: index, color: color)
} completion: {
resetStickViewAnimation(index: index, color: color)
}
} else {
withAnimation(Animation.linear(duration: perStickDuration)) {
updateStickViewProperties(index: index, color: color)
}
DispatchQueue.main.asyncAfter(deadline: .now() + perStickDuration) {
resetStickViewAnimation(index: index, color: color)
}
}
}

/// Updates the properties of the stick view for animation.
/// - Parameters:
/// - index: The index of the current stick.
/// - color: The color to animate to.
private func updateStickViewProperties(index: Int, color: Color) {
sticks[index].xAxis = 0.6
sticks[index].color = color
}

/// Resets the properties of the stick view after animation.
/// - Parameters:
/// - index: The index of the current stick.
/// - color: The color to animate to.
private func resetStickViewAnimation(index: Int, color: Color) {
withAnimation(Animation.linear(duration: perStickDuration * 10)) {
sticks[index].xAxis = 0
}
if index == sticks.indices.last {
let newColor = switch color {
case unFilledColor: filledColor
case filledColor: unFilledColor
default: filledColor
}
animateStickView(index: 0, color: newColor)
} else {
animateStickView(index: index + 1, color: color)
}
}
}

#Preview {
CircularLoading(
size: CGSize(width: 150, height: 150),
filledColor: .black,
unFilledColor: .gray,
duration: 1
)
}
154 changes: 154 additions & 0 deletions SSSwiftUIAnimations/Sources/SticksAnimations/CircularProgress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// CircularLoading.swift
// SSSwiftUIAnimations
//
// Created by Brijesh Barasiya on 29/01/24.
//

import SwiftUI

struct CircularProgress: View {
/// The percentage of progress to be displayed.
@Binding private var percentage: Double
/// The sticks to be displayed in the loading indicator.
@State private var sticks: [Stick]
/// The size of the circle.
private let circleSize: CGFloat
/// The width of each stick.
private let stickWidth: CGFloat
/// The color of a filled stick.
private let filledColor: Color
/// The color of an unfilled stick.
private let unFilledColor: Color
/// The duration of the animation for each stick.
private let perStickDuration: Double

/// Initializes the `CircularProgress` view.
/// - Parameters:
/// - percentage: The binding to the progress percentage.
/// - size: The size of the view.
/// - filledColor: The color of a filled stick.
/// - unFilledColor: The color of an unfilled stick.
/// - duration: The total duration of the animation.
init(
percentage: Binding<Double>,
size: CGSize,
filledColor: Color,
unFilledColor: Color,
duration: Double
) {
let adjustedSize = min(size.width, size.height)
let adjustedStickWidth = adjustedSize * 0.05
let totalStickCount: Int = Int(adjustedSize / (adjustedStickWidth * 0.75))
self._percentage = percentage
self.sticks = Array(
repeating: Stick(xAxis: 0, stickHeight: (adjustedSize * 0.20), color: unFilledColor),
count: totalStickCount
)
self.circleSize = CGFloat(adjustedSize)
self.stickWidth = CGFloat(adjustedStickWidth)
self.filledColor = filledColor
self.unFilledColor = unFilledColor
self.perStickDuration = duration / Double(totalStickCount)

}

var body: some View {
Circle()
.frame(width: circleSize)
.foregroundColor(Color.clear)
.overlay {
ForEach(0..<sticks.count, id: \.self) { index in
Rectangle()
.frame(width: stickWidth, height: sticks[index].stickHeight)
.foregroundColor(sticks[index].color)
.offset(y: (circleSize - sticks[index].stickHeight) / 2)
.rotationEffect(
.degrees(Double((CGFloat(index) + sticks[index].xAxis) * 360) / Double(sticks.count))
)
}
}
.onAppear {
animateStickView(index: 0, color: filledColor)
}
}

/// Starts and resets the animation on a particular stick view.
/// - Parameters:
/// - index: The index of the current stick.
/// - color: The color to animate to.
private func animateStickView(index: Int, color: Color) {
if percentage < 100 {
if #available(iOS 17.0, *) {
withAnimation(Animation.linear(duration: perStickDuration)) {
updateStickViewProperties(index: index, color: color)
} completion: {
resertStickViewAnimation(index: index, color: color)
}
} else {
withAnimation(Animation.linear(duration: perStickDuration)) {
updateStickViewProperties(index: index, color: color)
}
DispatchQueue.main.asyncAfter(deadline: .now() + perStickDuration) {
resertStickViewAnimation(index: index, color: color)
}
}
} else {
changeStictsColor(color: filledColor)
}
}

/// Updates the properties of the stick view for animation.
/// - Parameters:
/// - index: The index of the current stick.
/// - color: The color to animate to.
private func updateStickViewProperties(index: Int, color: Color) {
changeStictsColor(color: unFilledColor)
let validatedPercentage = min(max(0, percentage), 100)
let sticksAccordingToPercentage = Double(sticks.count) * Double(validatedPercentage / 200)
let numberOfSticksToChange = max(Int(sticksAccordingToPercentage), 0)
for stickIndex in 0..<Int(numberOfSticksToChange) {
updateStickColor(at: index + stickIndex, color: filledColor)
updateStickColor(at: (index - 1) - stickIndex, color: filledColor)
}
if Double(numberOfSticksToChange) != round(sticksAccordingToPercentage) {
updateStickColor(at: index + (Int(numberOfSticksToChange)), color: filledColor)
} else if (numberOfSticksToChange == 0) {
updateStickColor(at: index, color: color)
}
}

/// Changes the color of all sticks.
/// - Parameter color: The color to set for all sticks.
private func changeStictsColor(color: Color) {
sticks.indices.forEach { updateStickColor(at: $0, color: color) }
}

/// Updates the color of a specific stick.
/// - Parameters:
/// - index: The index of the stick to update.
/// - color: The color to set.
private func updateStickColor(at index: Int, color: Color) {
let adjustedIndex = (index + sticks.count) % sticks.count
sticks[adjustedIndex].color = color
}

/// Resets the properties of the stick view after animation.
/// - Parameters:
/// - index: The index of the current stick.
/// - color: The color to animate to.
private func resertStickViewAnimation(index: Int, color: Color) {
let nextIndex = (index == sticks.indices.last) ? 0 : index + 1
animateStickView(index: nextIndex, color: color)
}
}

#Preview {
CircularProgress(
percentage: .constant(75),
size: CGSize(width: 150, height: 150),
filledColor: .green,
unFilledColor: .gray,
duration: 1
)
}
Loading

0 comments on commit e22146a

Please sign in to comment.