If you use the
UISpringTimingParameters.init(dampingRatio: CGFloat), everything is fine. This implies the mass, stiffness and damping are automatically set to some values to match the provided
damping / (2 * sqrt(mass * stiffness)).
The documentation, however, doesn’t reveal the way the values are picked.
The thing is, when you take more control over the curve and use
UISpringTimingParameters.init(mass: CGFloat, stiffness: CGFloat, damping: CGFloat, initialVelocity: CGVector)
animations ignore their duration and, apparently, run as long as a string with such a curve would oscillate in a real-time ideal physics model (the time axis isn’t mapped). The animation doesn’t run forever though, it stops once the amplitude is too small for the screen to display (checked with a completion).
You can paste and run this code in an Xcode playground for visualization:
let v = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 500, height: 500))
let v1 = UIView()
v.backgroundColor = .white
v1.backgroundColor = .blue
v1.translatesAutoresizingMaskIntoConstraints = false
v1.heightAnchor.constraint(equalTo: v1.widthAnchor).isActive = true
v1.widthAnchor.constraint(equalToConstant: 90).isActive = true
v1.bottomAnchor.constraint(equalTo: v.bottomAnchor).isActive = true
v1.centerXAnchor.constraint(equalTo: v.centerXAnchor).isActive = true
let string1 = UISpringTimingParameters.init(mass: 1, stiffness: 16, damping: 0.1 * 8, initialVelocity: CGVector.zero)
let animator1 = UIViewPropertyAnimator.init(duration: 1.0, timingParameters: string1)
v1.transform = CGAffineTransform.init(translationX: 0, y: -220)
PlaygroundPage.current.liveView = v
This is another one that really deserves to be in a Radar. The UIKit team may not be lurking here on the Swift language forums for bug reports. :)
Looks like I’ll have to point out my own mistake. Apparently, this is deliberately done, though I have no idea why.
After revising the docs on the underlying
CASpringAnimation I stumbled upon the
settlingDuration property and found it rather suspicious: CASpringAnimation settlingDuration
In case the definition is confusing, it is the estimated time that has to pass for the spring system to be considered at rest.
Apparently it is actually the
settlingDuration that determines the animation duration. Which is very misleading in my opinion.
It looks like in the case of initializing
UISpringTimingParameters with simply a
dampingRatio, the other parameters are picked up in a manner that the
settlingDuration approximately equals the
duration of the animation .
When only a
dampingRatio is specified, as long as it remains constant and the
initialVelocity = 0, it is safe to modify the other properties, since mathematically they only determine how stretched then curve is by axis t (this is how the
settlingDuration is adjusted to equal the
It is important to note that wit the above conditions it doesn’t bother us how stretched the curve is, since we have to start at t = 0 and finish after
duration at the point where the amplitude is too small to display (the
settlingDuration). So a very stretched curve and a very shrunk curve with equal
initialVelocity = 0 are equal to us. We can just shrink or stretch it to match the
settlingDuration. I hope this is understandable.
However, when you specify all the parameters yourself, the
settlingDuration can far exceed or fall behind the
duration. For some reason, the curve isn’t stretched or shrunk in time as it could be for the
settlingVelocity to match the
This isn’t a bug as it appears, but isn’t this messed up? What are these mysterious cases when we need an animation that ignores its
duration and instead lasts for exactly the “estimated time required for the spring system to be considered at rest” in a real-time space? Can’t we be and don’t we have to be strict about the
duration of an animation when we can? I can simply assign
duration = settlingVelocity for the same effect…
@Tony_Parker will still report this, but as a suggestion.
Well, you’re explicitly specifying two things that are in conflict with each other (the physical parameters and the animation duration) so it has to pick one of them, and in this case I guess it’s not the one that you expected. It’s inherently ambiguous though, so I think if they were picking the duration instead then people would be asking the opposite question about why the animation isn’t reflecting the physical parameters they set.
The iOS spring animation APIs are known to be a bit odd. Here’s an interesting read if you’re curious for a bit more information.
I will try to clarify myself, though it will be obvious in some places
The animation duration and a function don’t conflict in a way you have to choose either of them and lose the other. Time can be mapped to suit you animation. The behavior of a function and its properties don’t change if time is linearly mapped. It’s simply a matter of speed.
Any basic time-based animation has a timing function or an array of them.
A timing function defines how an animated property changes from startValue to endValue throughout the animation duration. It is simply a function f(t) with the conditions f(0) = startValue, f(duration) = endValue that defines the pacing of your animation, where
duration is the duration of your animation. Not any other value, obviously.
A spring animation is an animation with a timing function of a damped oscillating spring. It has nothing to do with real-time physics at this point, although the function indeed possesses the properties of a spring. The fact that a damped oscillating spring is a physics model doesn’t mean the animation has to behave as if it is a physics body. It is the curve that matters. The function, and how it behaves. The dependence of progress on time.
The fact that a particular timing function is closely related to a physics model shouldn’t matter whatsoever.
Whether or not the object behaves as a real-time physics model is up to you to configure.
Imagine if easeInOut would be somehow related to physics, and animations because of this would ignore their
duration and act as physics bodies instead of scaling the function to match the
duration property, which actually is supposed to be the duration of your animation. You can easily set it to the appropriate value if you want a real-time physics model motion.
Concerning the spring curve and why the duration is approximate:
The spring curve is a harmonic function. It will oscillate forever in an ideal physics model (0 < dampingRatio < 1). This is why the curve is cropped from the right side at the moment t when the amplitude is too small for the screen to display. Given a
dampingRatio = damping / (2 * sqrt(mass * stiffness)), we have to pick
damping so that we don’t break the relation and
duration. This is why we approximate
duration. We can’t perfectly (well, float-presice) match
duration. And that is OK, but the animation duration absolutely has to at least approximate
duration. Because it is the animation duration.
I’ve seen that article a couple of times. Personally I see nothing odd in the plane old
animate method with spring parameters. Actually, the fact that you can set .curveEaseWhatever as an option is a mess. A bit long and outdated too. But that’s pretty much it. The fact that you had to calculate the
settlingDuration back then if you wanted the animation to look natural is more like an imperfection (already fixed) rather than an oddity.