DevB1
DevB1

Reputation: 1575

SwiftUI animation - toggled Boolean always ends up as true

I'm trying to create an animation in my app when a particular action happens which will essentially make the background of a given element change colour and back x number of times to create a kind of 'pulse' effect. The application itself is quite large, but I've managed to re-create the issue in a very basic app.

So the ContentView is as follows:

struct ContentView: View {
    struct Constants {
        static let animationDuration = 1.0
        static let backgroundAlpha: CGFloat = 0.6
    }
    
    @State var isAnimating = false
    @ObservedObject var viewModel = ContentViewViewModel()
    private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(6, autoreverses: false)
    
    var body: some View {
        VStack {
            Text("Hello, world!")
                
                .padding()
            Button(action: {
                animate()
            }) {
                Text("Button")
                    .foregroundColor(Color.white)
            }
        }
        .background(isAnimating ? Color.red : Color.blue)
        .onReceive(viewModel.$shouldAnimate, perform: { _ in
            if viewModel.shouldAnimate {
                withAnimation(self.animation, {
                    self.isAnimating.toggle()
                })
            }
        })
    }
    
    func animate() {
        self.viewModel.isNew = true
    }
}

And then my viewModel is:

import Combine
import SwiftUI

class ContentViewViewModel: ObservableObject {
    @Published var shouldAnimate = false
    @Published var isNew = false
    var cancellables = Set<AnyCancellable>()
    
    init() {
        $isNew
            .sink { result in
                if result {
                    self.shouldAnimate = true
                }
            }
            .store(in: &cancellables)
    }
}

So the logic I am following is that when the button is tapped, we set 'isNew' to true. This in turn is a publisher which, when set to true, sets 'shouldAnimate' to true. In the ContentView, when shouldAnimate is received and is true, we toggle the background colour of the VStack x number of times.

The reason I am using this 'shouldAnimate' published property is because in the actual app, there are several different actions which may need to trigger the animation, and so it feels simpler to have this tied to one variable which we can listen for in the ContentView.

So in the code above, we should be toggling the isAnimating bool 6 times. So, we start with false then toggle as follows:

1: true, 2: false, 3: true, 4: false, 5: true, 6: false

So I would expect to end up on false and therefore have the background white. However, this is what I am getting:

enter image description here

I tried changing the repeatCount (in case I was misunderstanding how the count works):

private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(7, autoreverses: false)

And I get the following:

enter image description here

No matter the count, I always end on true.

Update:

I have now managed to get the effect I am looking for by using the following loop:

for i in 0...5 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + Double(i), execute: {
                        withAnimation(self.animation, {
                            self.isAnimating.toggle()
                        })
                    })
                }

Not sure this is the best way to go though....

Upvotes: 0

Views: 818

Answers (1)

Scott Thompson
Scott Thompson

Reputation: 23711

To understand what is going on, it would help to understand CALayer property animations.

When you define an animation the system captures the state of a Layer and watches for changes in the animatable properties of that layer. It records property changes for playback during the animation. To present the animation, it create a copy of the layer in its initial state (the presentationLayer). It then substitutes the copy in place of the actual layers on screen and runs the animation by manipulating the animatable properties of the presentation layer.

I this case, when you begin the animation, the system watches what happens to the CALayer that backs your view and captures the changes to any animatable properties (in this case the background color). It then creates a presentationLayer and replays those property changes repeatedly. It's not running your code repeatedly - it's changing the properties of the presentation Layer.

In other words the animation the system knows the layer's background color property should toggle back and forth because of the example you set in your animation block, but the animation toggles the background color back and forth without running your code again.

Upvotes: 1

Related Questions