Setting .background(Color.green) on Text() triggers additional 60 calls per second into animation

Hey all,

I have a very simple view that offsets some view based on the value of a binding. To calculate that offset, I also need to have the previous value of the binding. To have that, am using withAnimation(). I am also using a custom animation. To keep things simple, my custom animation right now is just a linear progression. I added some code to keep track of the number of times animate<>(:::) gets called.

Turns out, when I add .background(Color.green) to my Text(), the number of calls gets increased by 60 (per second of animation). If .background(Color.green) gets added last (more precisely, after .offset(x: newOffset)), the background is not animated and the extra calls do not happen.

After reading the documentation and watching 'Demystifying SwiftUI', 'Demystifying SwiftUI performance' and various general SwiftUI and SwiftUI animation related WWDC sessions I am still feeling like I miss some basic understanding of SwiftUI animations.

Who can explain to me what is happening here and why? Or is the fact that animate<>(:::) gets called a number of times that is increasing linearly with the number of modifiers and number of subviews OK, and I should not be worried at all?

Relevant code below:

View:

struct TestView: View 
{
    @Binding
    var offset: Double

    @State
    private var previousOffset: Double = 0
    
    @State
    private var isAnimatingToNewOffset = false
    
    private let valuesInView = 20
    
    var body: some View
    {
        GeometryReader { geometry in
            let headingHeight: CGFloat = 80
            let newOffset = isAnimatingToNewOffset ?
            -(offset - previousOffset) / CGFloat(valuesInView) * geometry.size.width :
            0
            
            Text("Some text")
                .frame(width: geometry.size.width)
                .frame(height: headingHeight)
                .offset(y: (geometry.size.height - headingHeight) / 2)
                .background(Color.green) // moving this around, or removing it will cause the animate<>(:::) to be called a different number if times
                .offset(x: newOffset)
                
        }
        .background(Color.gray)
        .onChange(of: offset) { (oldValue, newValue) in
            /// entering an animated change
            isAnimatingToNewOffset = true
            withAnimation(Animation(MyAnimation(duration: 1)))
            {
                // before updating, keep the value of the current offset
                previousOffset = offSet
            } completion:
            {
                // now update the previous offset to be ready for a new animation
                previousOffset = offset

                // trigger another update of the body, but now without animation
                isAnimatingToNewOffset = false
            }
        }
    }
}

My custom animation:

struct MyAnimation: CustomAnimation
{
    
    /// just for debugging to understand how often this method gets called
    private static var count = 0
    let duration: TimeInterval
    
    func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic
    {
        let relativeProgress = CGFloat(time / duration)

        // print out the number of times this function is called, and with what value
        print("\(String(format: "%02i", MyAnimation.count)) - \(String(format: "%1.2f", relativeProgress))")
        MyAnimation.count += 1
        
        guard time < duration else { return nil }
     
        // keeping things simple for now, returning linear progress
        return value.scaled(by: relativeProgress)
    }
}

Replies

I can't figure out what you're trying to do here. I tried to put your TestView() into a template project's ContentView, but nothing animated at all and your print statements were not called. Your code didn't compile because previousOffset = offSet should read previousOffset = oldValue

could you post or provide a link to complete code which works, and provide an explanation of what you're actually trying to do? There would appear to be much simpler ways to offset a view with an animation.