diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj b/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj index 28688ed..b6c84e3 100644 --- a/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj @@ -7,6 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + 22AA9A532B679F83002FC677 /* LoadingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A522B679F83002FC677 /* LoadingType.swift */; }; + 22AA9A552B679FB0002FC677 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A542B679FB0002FC677 /* LoadingView.swift */; }; + 22AA9A572B679FEC002FC677 /* LoadingObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A562B679FEC002FC677 /* LoadingObservable.swift */; }; + 22AA9A592B67A026002FC677 /* CircularLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A582B67A026002FC677 /* CircularLoading.swift */; }; + 22AA9A5B2B67A03B002FC677 /* LinearLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A5A2B67A03B002FC677 /* LinearLoading.swift */; }; + 22AA9A5D2B67A065002FC677 /* Stick.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A5C2B67A065002FC677 /* Stick.swift */; }; + 22AA9A5F2B67A0DA002FC677 /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A5E2B67A0DA002FC677 /* CircularProgressBar.swift */; }; + 22AA9A612B67A0EF002FC677 /* LinearProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22AA9A602B67A0EF002FC677 /* LinearProgressBar.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 */; }; @@ -14,6 +22,14 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 22AA9A522B679F83002FC677 /* LoadingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingType.swift; sourceTree = ""; }; + 22AA9A542B679FB0002FC677 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + 22AA9A562B679FEC002FC677 /* LoadingObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingObservable.swift; sourceTree = ""; }; + 22AA9A582B67A026002FC677 /* CircularLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularLoading.swift; sourceTree = ""; }; + 22AA9A5A2B67A03B002FC677 /* LinearLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinearLoading.swift; sourceTree = ""; }; + 22AA9A5C2B67A065002FC677 /* Stick.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stick.swift; sourceTree = ""; }; + 22AA9A5E2B67A0DA002FC677 /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; + 22AA9A602B67A0EF002FC677 /* LinearProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinearProgressBar.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 = ""; }; @@ -32,6 +48,37 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 22AA9A4F2B679F24002FC677 /* LoadingAnimation */ = { + isa = PBXGroup; + children = ( + 22AA9A502B679F56002FC677 /* Loading */, + 22AA9A512B679F5F002FC677 /* ProgressBar */, + 22AA9A522B679F83002FC677 /* LoadingType.swift */, + 22AA9A542B679FB0002FC677 /* LoadingView.swift */, + 22AA9A562B679FEC002FC677 /* LoadingObservable.swift */, + 22AA9A5C2B67A065002FC677 /* Stick.swift */, + ); + path = LoadingAnimation; + sourceTree = ""; + }; + 22AA9A502B679F56002FC677 /* Loading */ = { + isa = PBXGroup; + children = ( + 22AA9A5A2B67A03B002FC677 /* LinearLoading.swift */, + 22AA9A582B67A026002FC677 /* CircularLoading.swift */, + ); + path = Loading; + sourceTree = ""; + }; + 22AA9A512B679F5F002FC677 /* ProgressBar */ = { + isa = PBXGroup; + children = ( + 22AA9A602B67A0EF002FC677 /* LinearProgressBar.swift */, + 22AA9A5E2B67A0DA002FC677 /* CircularProgressBar.swift */, + ); + path = ProgressBar; + sourceTree = ""; + }; 2BC2D8E628CF3A6F00CAB302 = { isa = PBXGroup; children = ( @@ -53,6 +100,7 @@ children = ( 2BC2D8F228CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift */, 2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */, + 22AA9A4F2B679F24002FC677 /* LoadingAnimation */, 2BC2D8F628CF3A7000CAB302 /* Assets.xcassets */, 2BC2D8F828CF3A7000CAB302 /* Preview Content */, ); @@ -138,7 +186,15 @@ buildActionMask = 2147483647; files = ( 2BC2D8F528CF3A6F00CAB302 /* ContentView.swift in Sources */, + 22AA9A612B67A0EF002FC677 /* LinearProgressBar.swift in Sources */, + 22AA9A572B679FEC002FC677 /* LoadingObservable.swift in Sources */, + 22AA9A5F2B67A0DA002FC677 /* CircularProgressBar.swift in Sources */, + 22AA9A5B2B67A03B002FC677 /* LinearLoading.swift in Sources */, + 22AA9A552B679FB0002FC677 /* LoadingView.swift in Sources */, + 22AA9A5D2B67A065002FC677 /* Stick.swift in Sources */, + 22AA9A592B67A026002FC677 /* CircularLoading.swift in Sources */, 2BC2D8F328CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift in Sources */, + 22AA9A532B679F83002FC677 /* LoadingType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/Loading/CircularLoading.swift b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/Loading/CircularLoading.swift new file mode 100644 index 0000000..993c291 --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/Loading/CircularLoading.swift @@ -0,0 +1,107 @@ +// +// CircularLoading.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 29/01/24. +// + +import SwiftUI + +struct CircularLoading: View { + @State private var sticks: [Stick] + private let circleRadius: CGFloat + private let stickWidth: CGFloat + private let filledColor: Color + private let unFilledColor: Color + private let duration: Double + + init( + size: Float, + stickWidth: Float, + filledColor: Color, + unFilledColor: Color, + duration: Double + ) { + let screenBounds = UIScreen.main.bounds + let screenWidth = Float(screenBounds.width) + let screenHeight = Float(screenBounds.height) + + // Adjust stick size, ensuring it doesn't exceed the screen size - 50 + let adjustedSize: Float = min(max(size, 50), min(screenWidth, screenHeight) - 50) + + // Adjust stick width, ensuring a minimum value of 9% of size. + let adjustedStickWidth = min(max((adjustedSize / 100) * 9, stickWidth), (adjustedSize / 100) * 20) + + let circumference = 2 * .pi * adjustedSize + let spacing = circumference / adjustedStickWidth + let totalStickCount: Int = Int((spacing * 25) / 100) + self.sticks = Array(repeating: Stick(xAxis: 0, color: unFilledColor), count: totalStickCount) + self.circleRadius = CGFloat(adjustedSize/2) + self.stickWidth = CGFloat(adjustedStickWidth) + self.filledColor = filledColor + self.unFilledColor = unFilledColor + self.duration = duration / Double(totalStickCount) + } + + var body: some View { + Circle() + .frame(width: circleRadius * 2) + .foregroundColor(Color.clear) + .overlay { + ForEach(0.. Color { + return switch (index) { + case 0 : filledColor + case sticks.indices.last : unFilledColor + default: isReversing ? unFilledColor : filledColor + } + } + + 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 == 0 && isReversing) || (index == sticks.indices.last && !isReversing) { + animateStickView(index: isReversing ? nextIndex + 1 : nextIndex - 1, isReversing: !isReversing) + } else { + animateStickView(index: nextIndex, isReversing: isReversing) + } + } +} + +#Preview { + LoadingView(loaderType: .linearLoading()) +} diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingObservable.swift b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingObservable.swift new file mode 100644 index 0000000..1826815 --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingObservable.swift @@ -0,0 +1,85 @@ +// +// LoadingObservable.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 29/01/24. +// + +import SwiftUI + +class LoadingObservable: ObservableObject { + + let view: any View + + init( + loaderType: LoaderType, + stickWidth: Float = 5, + duration: Double = 2, + filledColor: Color = .black, + unFilledColor: Color = .gray + ) { + self.view = switch loaderType { + case .linearLoading( + let stickCount, + let spacing, + let stickHeight, + let allowStickHeightAnimation + ): + LinearLoading( + stickCount: stickCount, + spacing: spacing, + stickWidth: stickWidth, + stickHeight: stickHeight, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration, + allowStickHeightAnimation: allowStickHeightAnimation + ) + + case .linearProgressBar ( + let percentange, + let stickCount, + let spacing, + let stickHeight, + let progressColor, + let allowHeightAnimation + ): + LinearProgressBar( + percentage: percentange, + stickCount: stickCount, + spacing: spacing, + stickWidth: stickWidth, + stickHeight: stickHeight, + progressColor: progressColor, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration, + allowStickHeightAnimation: allowHeightAnimation + ) + + case .circularLoading( + let size + ): + CircularLoading( + size: size, + stickWidth: stickWidth, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration + ) + + case .circularProgressBar( + let percentage, + let size + ): + CircularProgressBar( + percentage: percentage, + size: size, + stickWidth: stickWidth, + filledColor: filledColor, + unFilledColor: unFilledColor, + duration: duration + ) + } + } +} diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingType.swift b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingType.swift new file mode 100644 index 0000000..ad11633 --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingType.swift @@ -0,0 +1,31 @@ +// +// LoadingType.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 29/01/24. +// +import SwiftUI + +enum LoaderType { + case linearLoading( + stickCount: Int = 10, + spacing: Float = 5, + stickHeight: Float = 55, + allowStickHeightAnimation: Bool = true + ) + case linearProgressBar( + percentange: Binding, + stickCount: Int = 10, + spacing: Float = 7, + stickHeight: Float = 55, + progressColor: Color = .blue, + allowHeightAnimation: Bool = true + ) + case circularLoading( + size: Float = 50 + ) + case circularProgressBar( + percentage: Binding, + size: Float = 100 + ) +} diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingView.swift b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingView.swift new file mode 100644 index 0000000..3c435a0 --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/LoadingView.swift @@ -0,0 +1,37 @@ +// +// LoadingView.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 29/01/24. +// + +import SwiftUI + +struct LoadingView : View { + + @ObservedObject private var loaderObservable: LoadingObservable + + init ( + loaderType: LoaderType, + stickWidth: Float = 5, + duration: Double = 2, + filledColor: Color = .black, + unFilledColor: Color = .gray + ) { + loaderObservable = LoadingObservable( + loaderType: loaderType, + stickWidth: stickWidth, + duration: duration, + filledColor: filledColor, + unFilledColor: unFilledColor + ) + } + + var body: some View { + AnyView(loaderObservable.view) + } +} + +#Preview { + LoadingView(loaderType: .circularLoading()) +} diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/ProgressBar/CircularProgressBar.swift b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/ProgressBar/CircularProgressBar.swift new file mode 100644 index 0000000..7de6c4c --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/ProgressBar/CircularProgressBar.swift @@ -0,0 +1,127 @@ +// +// CircularProgressBar.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 29/01/24. +// + +import SwiftUI + +struct CircularProgressBar: View { + @Binding private var percentage: Float + @State private var sticks: [Color] + private let circleRadius: CGFloat + private let stickWidth: CGFloat + private let filledColor: Color + private let unFilledColor: Color + private let duration: Double + + init( + percentage: Binding, + size: Float, + stickWidth: Float, + filledColor: Color, + unFilledColor: Color, + duration: Double + ) { + let screenBounds = UIScreen.main.bounds + let screenWidth = Float(screenBounds.width) + let screenHeight = Float(screenBounds.height) + + // Adjust stick size, ensuring it doesn't exceed the screen size - 50 + let adjustedSize: Float = min(max(size, 50), min(screenWidth, screenHeight) - 50) + + // Adjust stick width, ensuring a minimum value of 5% of size and maximum value upto 10%. + let adjustedStickWidth = min(max(adjustedSize * 0.05, stickWidth), adjustedSize * 0.1) + + let circumference = 2 * .pi * adjustedSize + let totalStickCount: Int = Int((circumference / adjustedStickWidth) * 0.3) + self.sticks = Array(repeating: unFilledColor, count: totalStickCount) + self.circleRadius = CGFloat(adjustedSize / 2) + self.stickWidth = CGFloat(adjustedStickWidth) + self.filledColor = filledColor + self.unFilledColor = unFilledColor + self.duration = max(duration, 5) / Double(totalStickCount) + self._percentage = percentage + } + + var body: some View { + VStack { + Circle() + .frame(width: circleRadius * 2) + .foregroundColor(.clear) + .overlay { + ForEach(sticks.indices, id: \.self) { index in + Rectangle() + .frame( + width: stickWidth, + height: sticks[index] == filledColor ? (stickWidth * 3.5) : (stickWidth * 3) + ) + .foregroundColor(sticks[index]) + .offset(y: circleRadius - stickWidth * 1.5 + (sticks[index] == filledColor ? stickWidth * 0.25 : 0)) + .rotationEffect(.degrees(Double(index) * 360) / Double(sticks.count)) + } + } + .onAppear { + animateStickView(index: 0) + } + } + } + + private func animateStickView(index: Int) { + if percentage < 100 { + if #available(iOS 17.0, *) { + withAnimation(Animation.easeInOut(duration: duration)) { + changeStickColor(index: index) + } completion: { + changeStickIndex(index: index) + } + } else { + withAnimation(Animation.linear(duration: duration)) { + changeStickColor(index: index) + } + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + changeStickIndex(index: index) + } + } + } else { + sticks.indices.forEach { updateStickColor(at: $0, color: filledColor) } + } + } + + private func changeStickColor(index: Int) { + resetStickColors() + let validatedPercentage = min(max(0, percentage), 100) + let sticksAccordingToPercentage = Double(sticks.count) * Double(validatedPercentage / 200) + let numberOfSticksToChange = max(Int(sticksAccordingToPercentage), 1) + for stickIndex in 0.., + stickCount: Int, + spacing: Float, + stickWidth: Float, + stickHeight: Float, + progressColor: Color, + filledColor: Color, + unFilledColor: Color, + duration: Double, + allowStickHeightAnimation: Bool + ) { + let screenBounds = UIScreen.main.bounds + let screenWidth = screenBounds.width + let screenHeight = screenBounds.height + + // Adjust stick width, ensuring a minimum value of 10 + let adjustedStickWidth = max(stickWidth, 10) + + // Adjust spacing, ensuring a space is not lower than 0. + let adjustedSpacing = max(spacing, 0) + + // Adjust stick height, ensuring it doesn't exceed the screen height - 50 + let adjustedStickHeight = min(max(stickHeight, adjustedSpacing + 10), Float(screenHeight) - 50) + + // Calculate the total number of sticks, considering screen width and spacing + let totalStickCount = min(Int(Float(screenWidth) / (adjustedStickWidth + adjustedSpacing)) - 1, max(stickCount, 10)) + + // Calculate the animation duration per stick + self.perStickDuration = max(duration / Double(totalStickCount), 1 / Double(totalStickCount)) + + self.sticks = Array(repeating: Stick(xAxis: 0, color: unFilledColor), count: totalStickCount) + self.spacing = CGFloat(adjustedSpacing) + self.stickWidth = CGFloat(adjustedStickWidth) + self.stickHeight = CGFloat(adjustedStickHeight) + self.progressColor = progressColor + self.filledColor = filledColor + self.unFilledColor = unFilledColor + self.allowStickHeightAnimation = allowStickHeightAnimation + self._percentage = percentage + } + + var body: some View { + HStack(spacing: spacing) { + ForEach(sticks.indices, id: \.self) { index in + Rectangle() + .foregroundColor(sticks[index].color) + .frame(width: stickWidth, height: allowStickHeightAnimation ? abs(sticks[index].xAxis) + (stickHeight - spacing) : stickHeight) + .offset(x: sticks[index].xAxis) + } + } + .frame(height: stickHeight) + .onAppear { + animateStickView(index: 0, isReversing: false) + } + } + + private func animateStickView(index: Int, isReversing: Bool) { + if percentage < 100 { + if #available(iOS 17.0, *) { + withAnimation(Animation.linear(duration: perStickDuration + (Double(percentage) * 0.001))) { + updateStickViewProperties(index: index, isReversing: isReversing) + } completion: { + resetStickViewAnimation(index: index, isReversing: isReversing) + } + } else { + withAnimation(Animation.linear(duration: perStickDuration + (Double(percentage) * 0.001))) { + updateStickViewProperties(index: index, isReversing: isReversing) + } + DispatchQueue.main.asyncAfter(deadline: .now() + perStickDuration) { + resetStickViewAnimation(index: index, isReversing: isReversing) + } + } + } else { + sticks.indices.forEach { sticks[$0].color = progressColor } + } + } + + private func updateStickViewProperties(index: Int, isReversing: Bool) { + sticks[index].xAxis = isReversing ? (spacing * -1) : spacing + 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 + } + + private func resetStickViewAnimation(index: Int, isReversing: Bool) { + withAnimation(Animation.linear(duration: perStickDuration + (Double(percentage) * 0.001))) { + 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) + } + } + + private func getStickColor(forIndex index: Int, isReversing: Bool) -> Color { + return (index == sticks.indices.last) ? unFilledColor : (isReversing ? unFilledColor : filledColor) + } +} + +#Preview { + LoadingView(loaderType: .linearProgressBar(percentange: .constant(45))) +} diff --git a/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/Stick.swift b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/Stick.swift new file mode 100644 index 0000000..ce41c9f --- /dev/null +++ b/SSSwiftUIAnimations/SSSwiftUIAnimations/LoadingAnimation/Stick.swift @@ -0,0 +1,13 @@ +// +// Stick.swift +// SSSwiftUIAnimations +// +// Created by Brijesh Barasiya on 29/01/24. +// + +import SwiftUI + +struct Stick { + var xAxis: CGFloat + var color: Color +}