I've been looking to add a loading indicator to my project, and found a really cool animation here. To make it easier to use, I wanted to incorporate it into a view modifier to put it on top of the current view. However, when I do so, it doesn't animate when I first press the button. I have played around with it a little, and my hypothesis is that the View Modifier doesn't pass the initial isAnimating = false
, so only passes it isAnimating = true
when the button is pressed. Because the ArcsAnimationView
doesn't get the false value initially, it doesn't actually animate anything and just shows the static arcs. However, if I press the button a second time afterwards, it seems to be initialized and the view properly animates as desired.
Is there a better way to structure my code to avoid this issue? Am I missing something key? Any help is greatly appreciated.
Below is the complete code:
import SwiftUI
struct ArcsAnimationView: View {
@Binding var isAnimating: Bool
let count: UInt = 4
let width: CGFloat = 5
let spacing: CGFloat = 2
init(isAnimating: Binding<Bool>) {
self._isAnimating = isAnimating
}
var body: some View {
GeometryReader { geometry in
ForEach(0..<Int(count)) { index in
item(forIndex: index, in: geometry.size)
// the rotation below is what is animated ...
// I think the problem is that it just starts at .degrees(360), instead of
// .degrees(0) as expected, where it is then animated to .degrees(360)
.rotationEffect(isAnimating ? .degrees(360) : .degrees(0))
.animation(
Animation.default
.speed(Double.random(in: 0.05...0.25))
.repeatCount(isAnimating ? .max : 1, autoreverses: false)
, value: isAnimating
)
.foregroundColor(Color(hex: AppColors.darkBlue1.rawValue))
}
}
.aspectRatio(contentMode: .fit)
}
private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
Group { () -> Path in
var p = Path()
p.addArc(center: CGPoint(x: geometrySize.width/2, y: geometrySize.height/2),
radius: geometrySize.width/2 - width/2 - CGFloat(index) * (width + spacing),
startAngle: .degrees(0),
endAngle: .degrees(Double(Int.random(in: 120...300))),
clockwise: true)
return p.strokedPath(.init(lineWidth: width))
}
.frame(width: geometrySize.width, height: geometrySize.height)
}
}
struct ArcsAnimationModifier: ViewModifier {
@Binding var isAnimating: Bool
func body(content: Content) -> some View {
ZStack {
if isAnimating {
ArcsAnimationView(isAnimating: _isAnimating)
.frame(width: 150)
}
content
.disabled(isAnimating)
}
}
}
extension View {
func loadingAnimation(isAnimating: Binding<Bool>) -> some View {
self.modifier(ArcsAnimationModifier(isAnimating: isAnimating))
}
}
Here is where I actually call the function:
struct AnimationView: View {
@State var isAnimating = false
var body: some View {
VStack {
Button(action: {
self.isAnimating = true
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.isAnimating = false
}
}, label: {
Text("show animation")
})
}
.loadingAnimation(isAnimating: $isAnimating)
}
}
Note: I am fairly certain the issue is with View Modifier since if I call ArcsAnimationView
as a regular view in AnimationView
, it works as expected.