SwiftUI view animates at unexpected path

Viewed 81

I made the following PulsatingView:

struct PulsatingView: View {
    
    @State private var opacity = 0.7
    @State private var scale: CGFloat = 0.5

    var body: some View {
        VStack {
            ZStack {
                Circle()
                    .fill(Color.black.opacity(opacity))
                    .animation(.easeInOut(duration: 2).delay(1.5).repeatForever(autoreverses: false))
                    .frame(width: 7, height: 7)
                    .scaleEffect(scale)
                    .animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false))
                Circle()
                    .fill(Color.black)
                    .frame(width: 7, height: 7)
            }
        }
        .onAppear {
            opacity = 0
            scale = 5
        }
    }
}

It produces the following result: (click to see animation; it's a .gif)

pulsating view screencast

However, when I add it to a view, inside a VStack for example, this happens: (click to see animation; it's a .gif)

pulsating view glitch

I have no idea why this happens or how I can fix it. I've already tried adding modifiers such as .fixedSize(horizontal: true, vertical: true) or .frame(width: 50, height: 50) to it but that doesn't help.

Does anyone know what's going on?

3 Answers

Try using animation with joined value, like

Circle()
    .fill(Color.black.opacity(opacity))
    .animation(.easeInOut(duration: 2).delay(1.5).repeatForever(autoreverses: false), value: opacity) // << here !!
    .frame(width: 7, height: 7)
    .scaleEffect(scale)
    .animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false), value: scale)    // << here !!

Important: if it is started in NavigationView then animatable state should be activated postponed. The reason and investigation details was provided in https://stackoverflow.com/a/66643096/12299030.

This issue is not about your code it is because NavigationView to fix it we should use DispatchQueue.main.async I wanted refactor your code as well, hope help you, also you can avoid hard coded value as much as possible, for example you can apply the frame in outside of view:

struct ContentView: View {
    
    var body: some View {
        
        CircleView(lineWidth: 0.5, color: .red, scale: 5.0)
            .frame(width: 7, height: 7)
        
    }
    
}




struct CircleView: View {

    let lineWidth: CGFloat
    let color: Color
    let scale: CGFloat
    
    @State private var startAnimation: Bool = Bool()
    
    var body: some View {
        
        Circle()
            .strokeBorder(style: .init(lineWidth: startAnimation ? .zero : lineWidth)).opacity(startAnimation ? .zero : 1.0)
            .scaleEffect(startAnimation ? scale : 1.0)
            .overlay( Circle() )
            .foregroundColor(color)
            .onAppear() { DispatchQueue.main.async { startAnimation.toggle() } }
            .animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false), value: startAnimation)
        
    }
}

I finally managed to find a solution to this on my own project - I found the only solution to get the animated view working within a NavigationView was to move the animation inside the onAppear call, like this:

Circle()
    .fill(Color.black.opacity(opacity))
    .onAppear {
        withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: false)) {
            opacity = 0
        }
    }

Hope this can help someone else if they get stuck on the same issue I ended up having!

Related