Sigh at SwiftUI... yeah, that's the problem with the current animation in SwiftUI, that it doesn't support keyframe animation, and so we somehow end up with Animatable shenanigan. Though I'm not even sure if keyframe would make sense in SwiftUI.
Anyhow, we need to split the animation to utilise the nontrivial curves they provide. If we're to split the animation anyway, might as well just go simple.
The solution is actually very simple with your code and combine with Amination.repeatCount(n, autoreverse: true): just make an AnimatableModifier to scale relative to its origin and fully to the end. autoreverse: true would take care of animating back. It works as long as the repeat count is an odd number. No sure why it has to be an odd number.
I put some print statements in trying to understand. Though the animation on screen look correct. The print out don't make sense to me. On every other run if the starting value was 0, instead of interpolate from 0, it starts at 1. Same with 1, it would start interpolate from 0. The animation looks correct. I don't know what's going on here.
import SwiftUI
import Combine
struct BarView: View {
let fooTrigger = ObservableObjectPublisher()
var body: some View {
VStack {
FooView(trigger: fooTrigger)
Button("Animate above...") {
self.fooTrigger.send()
}
}
}
}
struct FooView: View {
static var animationDuration = 1.0
let trigger: ObservableObjectPublisher
@State var animationFlag = false
func doFoo() {
animationFlag.toggle()
}
var body: some View {
VStack {
ZStack {
Text(verbatim: "🤔").font(.system(size: 150))
.modifier(UnidirectionalScalingModifier(animationFlag))
.animation(Animation.easeInOut(duration: Self.animationDuration).repeatCount(3, autoreverses: true))
}
.frame(width: 200, height: 200)
Text(verbatim: "self.flag = \(self.animationFlag)")
}
.onReceive(trigger, perform: self.doFoo)
}
}
struct UnidirectionalScalingModifier: AnimatableModifier {
var animatableData: CGFloat
let origin: CGFloat
init(_ value: Bool) {
animatableData = value ? 0 : 1
origin = animatableData
print("\n\nScalingModifier\n-=-=-=-=-=-=-\norigin: \(animatableData)")
}
func body(content: Content) -> some View {
content
.scaleEffect(currentScale)
}
private var currentScale: CGFloat {
print("ScalingModifier: animatableData = \(animatableData)")
let maxScale: CGFloat = 1.2, minScale: CGFloat = 1
let currentValue = abs(animatableData - origin) // scale relative to the origin
return minScale + (maxScale - minScale) * currentValue
}
}
struct BarView_Previews: PreviewProvider {
static var previews: some View {
BarView()
}
}