Skip to content

Commit

Permalink
Linear & Circular Loading Animations
Browse files Browse the repository at this point in the history
  • Loading branch information
brijeshbarasiya2022 committed Feb 1, 2024
1 parent 3c9f828 commit 1ce5968
Show file tree
Hide file tree
Showing 9 changed files with 708 additions and 0 deletions.
56 changes: 56 additions & 0 deletions SSSwiftUIAnimations/SSSwiftUIAnimations.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@
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 */; };
2BC2D8FA28CF3A7000CAB302 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BC2D8F928CF3A7000CAB302 /* Preview Assets.xcassets */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
22AA9A522B679F83002FC677 /* LoadingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingType.swift; sourceTree = "<group>"; };
22AA9A542B679FB0002FC677 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
22AA9A562B679FEC002FC677 /* LoadingObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingObservable.swift; sourceTree = "<group>"; };
22AA9A582B67A026002FC677 /* CircularLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularLoading.swift; sourceTree = "<group>"; };
22AA9A5A2B67A03B002FC677 /* LinearLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinearLoading.swift; sourceTree = "<group>"; };
22AA9A5C2B67A065002FC677 /* Stick.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stick.swift; sourceTree = "<group>"; };
22AA9A5E2B67A0DA002FC677 /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
22AA9A602B67A0EF002FC677 /* LinearProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinearProgressBar.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 All @@ -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 = "<group>";
};
22AA9A502B679F56002FC677 /* Loading */ = {
isa = PBXGroup;
children = (
22AA9A5A2B67A03B002FC677 /* LinearLoading.swift */,
22AA9A582B67A026002FC677 /* CircularLoading.swift */,
);
path = Loading;
sourceTree = "<group>";
};
22AA9A512B679F5F002FC677 /* ProgressBar */ = {
isa = PBXGroup;
children = (
22AA9A602B67A0EF002FC677 /* LinearProgressBar.swift */,
22AA9A5E2B67A0DA002FC677 /* CircularProgressBar.swift */,
);
path = ProgressBar;
sourceTree = "<group>";
};
2BC2D8E628CF3A6F00CAB302 = {
isa = PBXGroup;
children = (
Expand All @@ -53,6 +100,7 @@
children = (
2BC2D8F228CF3A6F00CAB302 /* SSSwiftUIAnimationsApp.swift */,
2BC2D8F428CF3A6F00CAB302 /* ContentView.swift */,
22AA9A4F2B679F24002FC677 /* LoadingAnimation */,
2BC2D8F628CF3A7000CAB302 /* Assets.xcassets */,
2BC2D8F828CF3A7000CAB302 /* Preview Content */,
);
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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..<sticks.count, id: \.self) { index in
Rectangle()
.frame(width: stickWidth, height: (stickWidth * 2))
.foregroundColor(sticks[index].color)
.offset(y: (circleRadius - stickWidth))
.rotationEffect(
.degrees(Double((CGFloat(index) + sticks[index].xAxis) * 360) / Double(sticks.count))
)
}
}
.onAppear {
animateStickView(index: 0, color: filledColor)
}
}

private func animateStickView(index: Int, color: Color) {
if #available(iOS 17.0, *) {
withAnimation(Animation.linear(duration: duration)) {
updateStickViewProperties(index: index, color: color)
} completion: {
resertStickViewAnimation(index: index, color: color)
}
} else {
withAnimation(Animation.linear(duration: duration)) {
updateStickViewProperties(index: index, color: color)
}
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
resertStickViewAnimation(index: index, color: color)
}
}
}

private func updateStickViewProperties(index: Int, color: Color) {
sticks[index].xAxis = 0.6
sticks[index].color = color
}

private func resertStickViewAnimation(index: Int, color: Color) {
withAnimation(Animation.linear(duration: duration * 12)) {
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 {
LoadingView(loaderType: .circularLoading())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// LinearLoading.swift
// SSSwiftUIAnimations
//
// Created by Brijesh Barasiya on 29/01/24.
//

import SwiftUI

struct LinearLoading: View {
@State private var sticks: [Stick]
private let spacing: CGFloat
private let stickWidth: CGFloat
private let stickHeight: CGFloat
private let filledColor: Color
private let unFilledColor: Color
private let perStickDuration: Double
private let allowStickHeightAnimation: Bool

init(
stickCount: Int,
spacing: Float,
stickWidth: Float,
stickHeight: Float,
filledColor: Color,
unFilledColor: Color,
duration: Double,
allowStickHeightAnimation: Bool
) {
let screenBounds = UIScreen.main.bounds
let screenWidth = Float(screenBounds.width)
let screenHeight = Float(screenBounds.height)

// Adjust stick width, ensuring a minimum value of 10
let adjustedStickWidth = max(stickWidth, 10)

// Adjust spacing, ensuring a space is not lowerthan 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), screenHeight - 50)

// Calculate the total number of sticks, considering screen width and spacing
let totalStickCount = min(Int(screenWidth / (adjustedStickWidth + adjustedSpacing)) - 1, max(stickCount, 3))

// 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.filledColor = filledColor
self.unFilledColor = unFilledColor
self.allowStickHeightAnimation = allowStickHeightAnimation
}

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 {
sticks[0].color = filledColor
animateStickView(index: 0, isReversing: false)
}
}

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)
}
}
}

private func updateStickViewProperties(index: Int, isReversing: Bool) {
sticks[index].xAxis = isReversing ? (spacing * -1) : spacing
sticks[index].color = getStickColor(forIndex: index, isReversing: isReversing)
}

private func getStickColor(forIndex index: Int, isReversing: Bool) -> 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())
}
Loading

0 comments on commit 1ce5968

Please sign in to comment.