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