Can UISpringTimingParameters have an inverse spring effect? - ios

After dabbling with UIViewPropertyAnimator, I've found that .isReversed simply plays the animation backwards like a movie in reverse. Is it possible to apply "springiness" to the reverse animation through a custom Cubic Timing Parameter that only applies to the animation if it's reversed?

When you want to reverse the animation and go back with spring effect, you can simply use the following code snippet:
// prepare new timing parameters with the desired springiness
let newTimingParameters
= UISpringTimingParameters(dampingRatio: 0.5, initialVelocity: CGVector(dx: 2, dy: 0))
// pause the animator
propertyAnimator.pauseAnimation()
// reverse it
propertyAnimator.isReversed = true
// and restart it using new timing parameters with springiness
propertyAnimator.continueAnimation(withTimingParameters: newTimingParameters, durationFactor: propertyAnimator.fractionComplete)

Related

How can I set the old transformation after the animation has finished?

I have a scene with several triangles on it with different colors and so on which the users can create when they touch the view. The triangle is placed where the user touched the view and is added to a topView (which is linked to a context menu but this is something different).
I want to apply the current transformation of my triangle after the animation has finished.
Right now it animates it 4 times and when the animation has finished it shrinks abrupt without an animation and places it not at the old position.
let invertedTransform = topView.transform.inverted()
let triangles = TriangleView()
triangles.transform = invertedTransform
topView.addsubview(triangles)
triangleArray.append(triangles)
for triangle in triangleArray {
UIView.animate(withDuration: 1, delay: 0, options: [.repeat,.autoreverse], animations: {
UIView.setAnimationRepeatCount(4)
triangle.transform = CGAffineTransform(scaleX: 2.4, y: 2.4) }, completion: { done in
if done {
triangle.transform = CGAffineTransform(scaleX: 1, y: 1)
}
})
}
I think the mistake is in the completion part.The .autoreverse option is doing the reverse animation fine but when it finishes it doesn't shrink back.
How can I save my the current transformation of my triangle and set a pulsating animation for it?
UPDATE
I forgot to mention that the every triangle has a rotation value which is stored in a database:
triangle.rotation.value
The default transformation which is indeed given through the .identity method is the rotation value 0. When a rotation is applied on a triangle object it should set it back to the original value which is stored in the triangle object above:
let triangles = TriangleView(x: touchpoint.x, y: touchpoint.y, rotation: triangle.rotation.value)
The completion part should probably be (not enough code provided to test it):
if done {
triangle.transform = CGAffineTransform.identity // or just .identity
}
That sets the transform state back to "original" / "no transform".
Edit
If your object already has a transform, you'll need to either save it or re-create it.
So, for example, add a property to your TriangleView that saves its "initial" state:
var initialTransform: CGAffineTransform!
When you init the view and setup the rotation transform, save that:
self.initialTransform = self.transform
Your animation completion then becomes:
if done {
triangle.transform = triangle.initialTransform
}

Custom animation: easing out

I have successfully created a custom animation in a drawing in a UIView by using a repeating Timer to increment the alpha of the drawing every few milliseconds.
Now, I want to achieve an easing out animation (deceleration) with my drawing. I would like to do this by firing a new timer with a longer interval every time the Timer is called, so that the alpha increments slower, resulting in deceleration.
I know that there is an easeOut animation from CAMediaTiming, but I would like to know if there is any built in function to get the decelerating numbers. For example, if I pass in a constant of 10, every time I call the function I can get decelerating numbers like 15, 18, 20, 21, 21.5, etc.
There is a built-in way to use various sorts of animation curves in standard UIView animations.
var yourView = UIView() // Or whatever your view might be
var yourView.alpha = 0 // Initial alpha value
// Call the animation method. Note options (which can be an array) which applies an ease-out curve to the animation
UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseOut, animations: {
yourView.alpha = 1.0 // Final value of alpha
}, completion: nil)
Other animation curves are available, such as .curveEaseIn and .curveEaseInOut as well as other options. You can read more about animation options here.
You can use the completion handler closure to chain animations too.
If you are insisting on implementing your own means of animation using a timer (which I would not recommend), post some code so that I can consider a way to apply a curve to the changing values.

UIViewPropertyAnimator different behaviour on iOS10 and iOS11 reversing an animation. Bug on `isReversed` and `fractionComplete` properties?

THE PROBLEM
Running the same code on iOS10 and iOS11 my UIViewPropertyAnimator has a different behaviour just after changing of his .isReversed property.
Everything is ok on iOS10. The animation problem happens on iOS11
CONDITIONS
It's true for any animations, not only for a particular one, and it is verifiable both by watching the animation and within the code.
It happens both on simulators and real devices.
DETAILS
Once created a UIViewPropertyAnimator with his animation, during its running I just call .pauseAnimation() and change the .isReversed property to true. After that I resume the animation calling:
continueAnimation(withTimingParameters parameters: UITimingCurveProvider?, durationFactor: CGFloat)
at this point on iOS10 the animation smoothly changes his verse, on iOS11 it stops immediately and reverses itself with a bit frames lag.
If in code I check the value of .fractionComplete (called on my UIViewPropertyAnimator object it gives me back the completion of the animation in his percent value, starting from 0.0 and ending at 1.0)
just after .continueAnimation(...
- On iOS 10 it remains for a few moments like if the animation is continuing and only after some fractions of time jumps to his complementary.
- On iOS 11 it jumps suddenly on his complementary
On the documentation there are non updates related to this, just a couple of new properties for the UIViewPropertyAnimator but not used because I'm targeting iOS10
Could be a bug or I'm missing something!?
Little update: just tested, same behaviour on iOS 11.0.1 and on iOS 11.1 beta1
As linked in the comment, this happens only with a non-linear curve!
I have been fighting this for quite a while as well, but then I noticed the scrubsLinearly property that was added to UIViewPropertyAnimator in iOS 11:
Defaults to true. Provides the ability for an animator to pause and scrub either linearly or using the animator’s current timing.
Note that the default of this property is true, which seems to cause a conflict with using a non-linear animation curve. That might also explain why the issue is not present when using a linear timing function.
Setting scrubsLinearly to false, the animator seems to work as expected:
let animator = UIViewPropertyAnimator(duration: 0.25, curve: .easeOut) {
...
}
animator.scrubsLinearly = false
On iOS 11, fractionComplete will be reversed (that is, 1 - originalFractionComplete) after you reverse animation by animator.isReversed = true.
Spring animation that have less than 0.1s duration will complete instantly.
So you may originally want the reversed animation runs 90% of the entire animation duration, but on iOS 11, the reversed animation actually runs 10% duration because isReversed changed, and that 10% duration is less than 0.1s, so the animation will be completed instantly and looks like no animation happened.
How to fix?
For iOS 10 backward compatibility, copy the fractionComplete value before you reverse animation and use it for continueAnimation.
e.g.
let fraction = animator.fractionComplete
animator.isReversed = true
animator.continueAnimation(...*fraction*...)
I have tried many solutions, but no one didn't work for me. I wrote my solution and everything is fine now. My solution:
take an image of the screen and show it
finish animation
start new animation for old state
pause the animation and set progress (1 - original progress)
remove screen image and continue animation
switch pan.state {
...
case .ended, .cancelled, .failed:
let velocity = pan.velocity(in: view)
let reversed: Bool
if abs(velocity.y) < 200 {
reversed = progress < 0.5
} else {
switch state {
case .shown:
reversed = velocity.y < 0
case .minimized:
reversed = velocity.y > 0
}
}
if reversed {
let overlayView = UIScreen.main.snapshotView(afterScreenUpdates: false)
view.addSubview(overlayView)
animator?.stopAnimation(false)
animator?.finishAnimation(at: .end)
startAnimation(state: state.opposite)
animator?.pauseAnimation()
animator?.fractionComplete = 1 - progress
overlayView.removeFromSuperview()
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0.5)
} else {
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
}
And the animation curve option must be linear.
animator = UIViewPropertyAnimator(duration: 0.3, curve: .linear) {
startAnimation()
}

How can I stop a SKAction.repeatActionForever action at a action boundary?

I have a sequence of SpriteKit actions that I create and then repeat forever on a node, but that I want to stop eventually. My sequence rotates a disk left, right, and left returning to the original rotation before starting again. However, when I remove the action, it stops without completing and so the original rotation is not restored.
I could save the original rotation state and restore it, but I want to know is there is a way to tell SpriteKit to only interrupt the action at the sequence boundary?
func wiggle() -> SKAction {
let wiggleLeft = SKAction.rotateByAngle(+0.04, duration: 0.1)
let wiggleRight = SKAction.rotateByAngle(-0.08, duration: 0.2)
let wiggleBack = SKAction.rotateByAngle(+0.04, duration: 0.1)
let wiggle = SKAction.sequence([wiggleLeft, wiggleRight, wiggleBack])
let wiggleForever = SKAction.repeatActionForever(wiggle)
return wiggleForever
}
disk.runAction(wiggle(), withKey: "wiggle")
...
disk.removeActionForKey("wiggle") // unfortunately stops mid-wiggle
Add the following code after disk.removeActionForKey("wiggle"):
disk.runAction.rotateToAngle(/*desired final angle of rotation*/)

SCNTransaction AnimationOptions

I have an animation in my SceneKit project. It animates the node that is pressed. Now, the animation works with this code:
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(0.5)
SCNTransaction.setCompletionBlock {
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(0.5)
result.node!.position.y -= 1
SCNTransaction.commit()
}
result.node!.position.y += 1
SCNTransaction.commit()
}
I want to make the node seem like it is jumping, therefore I would like to use some animation options, such as you can use with a UIView: CurveEaseIn etc.. (I want it to start slowly, end abrupt. The second should be abrupt first, then slowly.)
Is there a way to use these for a SCNTransaction? Or is there maybe a better way to make it 'bounce'?
Thanks in advance :)
Changing the timing function
Yes, you can change the timing function with SCNTransaction.
You do that by creating a CAMediaTimingFunction object and assigning it using setAnimationTimingFunction(). There are a couple of named timing functions (for example "ease in"), or you can create one using two set of control points.
let easeIn = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
SCNTransaction.setAnimationTimingFunction(easeIn)
Using a key-frame animation
Another alternative is to create a more custom key-frame animation for the node's y-position. I did a bounce looking key-frame animation in the sample code for Chapter 5 of my book about Scene Kit:
var jump = CAKeyframeAnimation(keyPath: "position.y")
let easeIn = CAMediaTimingFunction(controlPoints: 0.35, 0.0, 1.0, 1.0)
let easeOut = CAMediaTimingFunction(controlPoints: 0.0, 1.0, 0.65, 1.0)
jump.values = [0.000000, 0.433333, 0.000000, 0.124444, 0.000000, 0.035111, 0.000000];
jump.keyTimes = [0.000000, 0.255319, 0.531915, 0.680851, 0.829788, 0.914894, 1.000000];
jump.timingFunctions = [easeOut, easeIn, easeOut, easeIn, easeOut, easeIn ];
jump.duration = 0.783333;
yourNodeThatWasClicked.addAnimation(jump, forKey: "jump and bounce")
If you go down this path, I would recommend finding somewhere that you can experiment, tweak, and play with the key times and values of the key-frame animation. Perhaps a playground or a small custom app with sliders where you can get fast iterations.
you can use a CABasicAnimation with byValue of 1 and set autoreverses to YES. Finally set its timingFunction to kCAMediaTimingFunctionEaseIn (or, more specifically, a timing function initialized with this curve name).

Resources