From e22146a73653013a98b7361a7c536069db616e24 Mon Sep 17 00:00:00 2001 From: Brijesh Barasiya Date: Fri, 2 Aug 2024 14:53:54 +0530 Subject: [PATCH] UNT-T21025 Stick Animations --- SSSwiftUIAnimations.xcodeproj/project.pbxproj | 40 ++++ .../SticksAnimations/CircularLoading.swift | 128 +++++++++++++ .../SticksAnimations/CircularProgress.swift | 154 +++++++++++++++ .../CircularReverseProgreessBar.swift | 167 ++++++++++++++++ .../SticksAnimations/LinearLoading.swift | 150 +++++++++++++++ .../SticksAnimations/LinearProgress.swift | 180 ++++++++++++++++++ .../Sources/SticksAnimations/Stick.swift | 14 ++ .../SticksAnimations/StickAnimationType.swift | 29 +++ .../SticksAnimations/StickAnimations.swift | 105 ++++++++++ 9 files changed, 967 insertions(+) create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/CircularLoading.swift create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/CircularProgress.swift create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/CircularReverseProgreessBar.swift create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/LinearLoading.swift create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/LinearProgress.swift create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/Stick.swift create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimationType.swift create mode 100644 SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimations.swift diff --git a/SSSwiftUIAnimations.xcodeproj/project.pbxproj b/SSSwiftUIAnimations.xcodeproj/project.pbxproj index 81d6757..ada29fd 100644 --- a/SSSwiftUIAnimations.xcodeproj/project.pbxproj +++ b/SSSwiftUIAnimations.xcodeproj/project.pbxproj @@ -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 */; }; @@ -43,6 +51,14 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 223F502D2C2E95F3006C68CE /* CircularLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularLoading.swift; sourceTree = ""; }; + 223F502F2C2E9636006C68CE /* LinearLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearLoading.swift; sourceTree = ""; }; + 223F50302C2E9636006C68CE /* LinearProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearProgress.swift; sourceTree = ""; }; + 223F50312C2E9636006C68CE /* CircularProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = ""; }; + 223F50322C2E9636006C68CE /* CircularReverseProgreessBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularReverseProgreessBar.swift; sourceTree = ""; }; + 223F50332C2E9636006C68CE /* Stick.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stick.swift; sourceTree = ""; }; + 223F50392C2E991B006C68CE /* StickAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickAnimations.swift; sourceTree = ""; }; + 223F503B2C2EACCD006C68CE /* StickAnimationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickAnimationType.swift; sourceTree = ""; }; 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 = ""; }; 2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -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 = ""; + }; 2BC2D8E628CF3A6F00CAB302 = { isa = PBXGroup; children = ( @@ -180,6 +211,7 @@ isa = PBXGroup; children = ( B780A9162C3D05CD00342512 /* WaterProgressAnimation */, + 223F502C2C2E95D6006C68CE /* SticksAnimations */, B14AB36A2BC40286004B09C4 /* ProgressAnimation */, 469963A3290FCE1900DC01AD /* ArrowLeftRightAnimation */, ); @@ -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 */, @@ -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 */, ); diff --git a/SSSwiftUIAnimations/Sources/SticksAnimations/CircularLoading.swift b/SSSwiftUIAnimations/Sources/SticksAnimations/CircularLoading.swift new file mode 100644 index 0000000..b2ea7dc --- /dev/null +++ b/SSSwiftUIAnimations/Sources/SticksAnimations/CircularLoading.swift @@ -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.., + 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.., + size: CGSize, + progressColor: Color, + 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.progressColor = progressColor + 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..= numberOfSticksToChange ? numberOfSticksToChange : lastStickIndex + lastStickIndex = finalIndex + } + + /// Resets the properties of the stick view after animation. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A boolean indicating whether the animation is in reverse mode. + private func resertStickViewAnimation(index: Int, isReversing: Bool) { + withAnimation(Animation.linear(duration: perStickDuration)) { + sticks[index].xAxis = 0 + } + let nextIndex = isReversing ? index - 1 : index + 1 + if (index == (lastStickIndex) && isReversing) || (index == sticks.indices.last && !isReversing) { + animateStickView(index: isReversing ? nextIndex + 1 : nextIndex - 1, isReversing: !isReversing) + } else { + animateStickView(index: nextIndex ,isReversing: isReversing) + } + } + + /// Gets the color for the stick at a specific index. + /// - Parameters: + /// - index: The index of the stick. + /// - isReversing: A boolean indicating whether the animation is in reverse mode. + /// - Returns: The color for the stick. + private func getStickColor(forIndex index: Int, isReversing: Bool) -> Color { + return if (index == lastStickIndex) { + filledColor + } else if (index == sticks.indices.last) { + unFilledColor + } else { + (isReversing ? unFilledColor : filledColor) + } + } +} + +#Preview { + CircularReverseProgressBar( + percentage: .constant(100), + size: CGSize(width: 150, height: 150), + progressColor: .red, + filledColor: .green, + unFilledColor: .gray, + duration: 3 + ) +} diff --git a/SSSwiftUIAnimations/Sources/SticksAnimations/LinearLoading.swift b/SSSwiftUIAnimations/Sources/SticksAnimations/LinearLoading.swift new file mode 100644 index 0000000..0006a78 --- /dev/null +++ b/SSSwiftUIAnimations/Sources/SticksAnimations/LinearLoading.swift @@ -0,0 +1,150 @@ +// +// LinearLoading.swift +// SwiftUIAnimation +// +// Created by Brijesh Barasiya on 17/05/24. +// + +import SwiftUI + +struct LinearLoading: View { + /// The sticks to be displayed in the loading indicator. + @State private var sticks: [Stick] + /// The width of each stick. + private let stickWidth: CGFloat + /// The height of each stick. + private let stickHeight: CGFloat + /// The spacing between each stick. + private let spacing: CGFloat + /// The color of a filled stick which indicate as Progressed. + 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 + /// A flag to allow height animation for the sticks. + private let allowHeightAnimation: Bool + + /// Initializes the `LinearLoading` view. + /// - Parameters: + /// - size: The size of the view. + /// - stickWidth: The width of each stick. + /// - spacing: The spacing between each stick. + /// - filledColor: The color of a filled stick. + /// - unFilledColor: The color of an unfilled stick. + /// - duration: The total duration of the animation. + /// - allowHeightAnimation: A flag to allow height animation for the sticks. + init( + size: CGSize, + stickWidth: Float, + spacing: Float, + filledColor: Color, + unFilledColor: Color, + duration: Double, + allowHeightAnimation: Bool + ) { + let screenHeight = Float(size.height) + let adjustedStickHeight: Float = allowHeightAnimation ? (screenHeight * 0.80) : screenHeight + let totalStickCount: Int = max(Int(Float(size.width) / (stickWidth + spacing)), 3) + self.sticks = Array( + repeating: Stick(xAxis: 0, stickHeight: CGFloat(adjustedStickHeight), color: unFilledColor), + count: totalStickCount + ) + self.stickWidth = CGFloat(max(stickWidth, 5)) + self.stickHeight = CGFloat(screenHeight) + self.spacing = CGFloat(max(min(spacing, stickWidth), 0)) + self.filledColor = filledColor + self.unFilledColor = unFilledColor + self.perStickDuration = max(duration, 0.2) / Double(totalStickCount * 2) + self.allowHeightAnimation = allowHeightAnimation + } + + var body: some View { + HStack(spacing: spacing) { + ForEach(sticks.indices, id: \.self) { index in + Rectangle() + .foregroundColor(sticks[index].color) + .frame(width: stickWidth, height: sticks[index].stickHeight) + .offset(x: sticks[index].xAxis) + } + } + .frame(height: stickHeight) + .onAppear { + sticks[0].color = filledColor + animateStickView(index: 0, isReversing: false) + } + } + + /// Starts and resets the animation on a particular stick view. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A flag to indicate if the animation is reversing. + private func animateStickView(index: Int, isReversing: Bool) { + if #available(iOS 17.0, *) { + withAnimation(Animation.linear(duration: perStickDuration)) { + updateStickViewProperties(index: index, isReversing: isReversing) + } completion: { + resertStickViewAnimation(index: index, isReversing: isReversing) + } + } else { + withAnimation(Animation.linear(duration: perStickDuration)) { + updateStickViewProperties(index: index, isReversing: isReversing) + } + DispatchQueue.main.asyncAfter(deadline: .now() + perStickDuration) { + resertStickViewAnimation(index: index, isReversing: isReversing) + } + } + } + + /// Updates the properties of the stick view for animation. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A flag to indicate if the animation is reversing. + private func updateStickViewProperties(index: Int, isReversing: Bool) { + sticks[index].xAxis = isReversing ? (spacing * -1) : spacing + sticks[index].stickHeight = allowHeightAnimation ? stickHeight * 1.25 : stickHeight + sticks[index].color = getStickColor(forIndex: index, isReversing: isReversing) + } + + /// Gets the color of the stick. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A flag to indicate if the animation is reversing. + /// - Returns: The color of the stick. + private func getStickColor(forIndex index: Int, isReversing: Bool) -> Color { + return switch (index) { + case 0 : filledColor + case sticks.indices.last : unFilledColor + default: isReversing ? unFilledColor : filledColor + } + } + + /// Resets the properties of the stick view after animation. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A flag to indicate if the animation is reversing. + private func resertStickViewAnimation(index: Int, isReversing: Bool) { + withAnimation(Animation.linear(duration: perStickDuration)) { + sticks[index].xAxis = 0 + sticks[index].stickHeight = stickHeight + } + let nextIndex = isReversing ? index - 1 : index + 1 + if (index == 0 && isReversing) || (index == sticks.indices.last && !isReversing) { + animateStickView(index: isReversing ? nextIndex + 1 : nextIndex - 1, isReversing: !isReversing) + } else { + animateStickView(index: nextIndex, isReversing: isReversing) + } + } +} + +#Preview { + LinearLoading( + size: CGSize(width: 70, height: 30), + stickWidth: 10, + spacing: 6, + filledColor: .black, + unFilledColor: .gray, + duration: 1, + allowHeightAnimation: true + ) +} diff --git a/SSSwiftUIAnimations/Sources/SticksAnimations/LinearProgress.swift b/SSSwiftUIAnimations/Sources/SticksAnimations/LinearProgress.swift new file mode 100644 index 0000000..383ebad --- /dev/null +++ b/SSSwiftUIAnimations/Sources/SticksAnimations/LinearProgress.swift @@ -0,0 +1,180 @@ +// +// LinearLoading.swift +// SwiftUIAnimation +// +// Created by Brijesh Barasiya on 17/05/24. +// + +import SwiftUI + +struct LinearProgress: View { + /// The last index of the stick that was updated. + @State private var lastStickIndex: Int = 0 + /// The percentage of progress to be displayed. + @Binding private var percentage: Double + /// The sticks to be displayed in the progress bar. + @State private var sticks: [Stick] + /// The width of each stick. + private let stickWidth: CGFloat + /// The height of each stick. + private let stickHeight: CGFloat + /// The spacing between each stick. + private let spacing: CGFloat + /// The color of the progress. + private let progressColor: Color + /// 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 + /// Whether to allow height animation for the sticks. + private let allowHeightAnimation: Bool + + /// Initializes the `LinearProgress` view. + /// - Parameters: + /// - percentage: The binding to the progress percentage. + /// - size: The size of the view. + /// - stickWidth: The width of each stick. + /// - spacing: The spacing between each stick. + /// - progressColor: The color of the progress. + /// - filledColor: The color of a filled stick. + /// - unFilledColor: The color of an unfilled stick. + /// - duration: The total duration of the animation. + /// - allowHeightAnimation: Whether to allow height animation for the sticks. + init( + percentage: Binding, + size: CGSize, + stickWidth: Float, + spacing: Float, + progressColor: Color, + filledColor: Color, + unFilledColor: Color, + duration: Double, + allowHeightAnimation: Bool + ) { + let screenHeight = Float(size.height) + let adjustedStickHeight: Float = allowHeightAnimation ? (screenHeight * 0.80) : screenHeight + let totalStickCount: Int = max(Int(Float(size.width) / (stickWidth + spacing)), 3) + self._percentage = percentage + self.sticks = Array( + repeating: Stick(xAxis: 0, stickHeight: CGFloat(adjustedStickHeight), color: unFilledColor), + count: totalStickCount + ) + self.stickWidth = CGFloat(max(stickWidth, 5)) + self.stickHeight = CGFloat(screenHeight) + self.spacing = CGFloat(max(min(spacing, stickWidth), 0)) + self.progressColor = progressColor + self.filledColor = filledColor + self.unFilledColor = unFilledColor + self.perStickDuration = max(duration, 0.2) / Double(totalStickCount * 2) + self.allowHeightAnimation = allowHeightAnimation + } + + var body: some View { + HStack(spacing: spacing) { + ForEach(sticks.indices, id: \.self) { index in + Rectangle() + .foregroundColor(sticks[index].color) + .frame(width: stickWidth, height: sticks[index].stickHeight) + .offset(x: sticks[index].xAxis) + } + } + .onAppear { + sticks[0].color = filledColor + animateStickView(index: 0, isReversing: false) + } + } + + /// Starts and resets the animation on a particular stick view. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A boolean indicating whether the animation is in reverse mode. + private func animateStickView(index: Int, isReversing: Bool) { + if percentage < 100 { + if #available(iOS 17.0, *) { + withAnimation(Animation.linear(duration: perStickDuration)) { + updateStickViewProperties(index: index, isReversing: isReversing) + } completion: { + resertStickViewAnimation(index: index, isReversing: isReversing) + } + } else { + withAnimation(Animation.linear(duration: perStickDuration)) { + updateStickViewProperties(index: index, isReversing: isReversing) + } + DispatchQueue.main.asyncAfter(deadline: .now() + perStickDuration) { + resertStickViewAnimation(index: index, isReversing: isReversing) + } + } + } else { + sticks.indices.forEach { index in + sticks[index].color = progressColor + } + } + } + + /// Updates the properties of the stick view for animation. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A boolean indicating whether the animation is in reverse mode. + private func updateStickViewProperties(index: Int, isReversing: Bool) { + sticks[index].xAxis = isReversing ? (spacing * -1) : spacing + sticks[index].stickHeight = allowHeightAnimation ? stickHeight * 1.25 : stickHeight + sticks[index].color = getStickColor(forIndex: index, isReversing: isReversing) + let validatedPercentage = min(max(0, percentage), 100) + let sticksAccordingToPercentage = Double(sticks.count) * Double(validatedPercentage / 100) + let numberOfSticksToChange = max(Int(sticksAccordingToPercentage), 0) + for stickIndex in 0..= numberOfSticksToChange ? numberOfSticksToChange : lastStickIndex + lastStickIndex = finalIndex + } + + /// Gets the color for the stick at a specific index. + /// - Parameters: + /// - index: The index of the stick. + /// - isReversing: A boolean indicating whether the animation is in reverse mode. + /// - Returns: The color for the stick. + private func getStickColor(forIndex index: Int, isReversing: Bool) -> Color { + switch index { + case sticks.indices.last: + return unFilledColor + case lastStickIndex: + return filledColor + default: + return isReversing ? unFilledColor : filledColor + } + } + + /// Resets the properties of the stick view after animation. + /// - Parameters: + /// - index: The index of the current stick. + /// - isReversing: A boolean indicating whether the animation is in reverse mode. + private func resertStickViewAnimation(index: Int, isReversing: Bool) { + withAnimation(Animation.linear(duration: perStickDuration)) { + sticks[index].xAxis = 0 + sticks[index].stickHeight = stickHeight + } + let nextIndex = isReversing ? index - 1 : index + 1 + if (index == lastStickIndex && isReversing) || (index == sticks.indices.last && !isReversing) { + animateStickView(index: isReversing ? nextIndex + 1 : nextIndex - 1, isReversing: !isReversing) + } else { + animateStickView(index: nextIndex, isReversing: isReversing) + } + } +} + +#Preview { + LinearProgress( + percentage: .constant(75), + size: CGSize(width: 70, height: 30), + stickWidth: 10, + spacing: 6, + progressColor: .green, + filledColor: .black, + unFilledColor: .gray, + duration: 1, + allowHeightAnimation: true + ) +} diff --git a/SSSwiftUIAnimations/Sources/SticksAnimations/Stick.swift b/SSSwiftUIAnimations/Sources/SticksAnimations/Stick.swift new file mode 100644 index 0000000..b84754e --- /dev/null +++ b/SSSwiftUIAnimations/Sources/SticksAnimations/Stick.swift @@ -0,0 +1,14 @@ +// +// Stick.swift +// SwiftUIAnimation +// +// Created by Brijesh Barasiya on 17/05/24. +// + +import SwiftUI + +struct Stick { + var xAxis: CGFloat + var stickHeight: CGFloat + var color: Color +} diff --git a/SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimationType.swift b/SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimationType.swift new file mode 100644 index 0000000..6093663 --- /dev/null +++ b/SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimationType.swift @@ -0,0 +1,29 @@ +// +// StickAnimationType.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 28/06/24. +// + +import SwiftUI + +enum StickAnimationType { + case linearLoading( + stickWidth: Float = 10, + spacing: Float = 6, + allowHeightAnimation: Bool = true + ) + case linearProgressBar( + percentage: Binding, + stickWidth: Float = 10, + spacing: Float = 6, + progressColor: Color = .green, + allowHeightAnimation: Bool = true + ) + case circularLoading + case circularProgressBar(percentage: Binding) + case circularReversableProgressBar( + percentage: Binding, + progressColor: Color = .green + ) +} diff --git a/SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimations.swift b/SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimations.swift new file mode 100644 index 0000000..b47be22 --- /dev/null +++ b/SSSwiftUIAnimations/Sources/SticksAnimations/StickAnimations.swift @@ -0,0 +1,105 @@ +// +// StickAnimations.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 28/06/24. +// + +import SwiftUI + +/// A view that displays various types of stick animations based on the provided `StickAnimationType`. +struct StickAnimations: View { + + /// The type of stick animation to display. + let type: StickAnimationType + /// The color for filled sticks. + let filledColor: Color = .black + /// The color for unfilled sticks. + let unFilledColor: Color = .gray + /// The duration of the animation. + var duration: Double = 1 + + /// A view builder that returns the appropriate animation view based on the `type`. + @ViewBuilder + var contentView: some View { + GeometryReader { geometry in + switch type { + case .linearLoading( + stickWidth: let stickWidth, + spacing: let spacing, + allowHeightAnimation: let allowHeightAnimation): + LinearLoading( + size: geometry.size, + stickWidth: stickWidth, + spacing: spacing, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration, + allowHeightAnimation: allowHeightAnimation + ) + .frame(width: geometry.size.width, height: geometry.size.height) + + case .linearProgressBar( + percentage: let percentage, + stickWidth: let stickWidth, + spacing: let spacing, + progressColor: let progressColor, + allowHeightAnimation: let allowHeightAnimation + ): + LinearProgress( + percentage: percentage, + size: geometry.size, + stickWidth: stickWidth, + spacing: spacing, + progressColor: progressColor, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration, + allowHeightAnimation: allowHeightAnimation + ) + .frame(width: geometry.size.width, height: geometry.size.height) + + case .circularLoading: + CircularLoading( + size: geometry.size, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration + ) + .frame(width: geometry.size.width, height: geometry.size.height) + + case .circularProgressBar(percentage: let percentage): + CircularProgress( + percentage: percentage, + size: geometry.size, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration + ) + .frame(width: geometry.size.width, height: geometry.size.height) + + case .circularReversableProgressBar( + percentage: let percentage, + progressColor: let progressColor + ): + CircularReverseProgressBar( + percentage: percentage, + size: geometry.size, + progressColor: progressColor, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration + ) + .frame(width: geometry.size.width, height: geometry.size.height) + } + } + } + + var body: some View { + contentView + } +} + +#Preview { + StickAnimations(type: .circularLoading) +}