How to synchronise UIViewControllerInteractiveTransitioning and CABasicAnimation - ios

I have a button in a view controller that has a shadow. This shadow is applied with an animation, in viewWillAppear.
The button is inside an empty view called 'buttonContainer'.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
CATransaction.begin()
CATransaction.setCompletionBlock {
self.buttonContainer.layer.shadowOpacity = 1
}
let animation = CABasicAnimation(keyPath: "shadowOpacity")
animation.fromValue = buttonContainer.layer.shadowOpacity
animation.toValue = 1
animation.duration = 0.6
buttonContainer.layer.add(animation, forKey: animation.keyPath)
CATransaction.commit()
}
I want the shadow to animate with the push animation of the view controller. I started implementing 'UIViewControllerAnimatedTransitioning' and I couldn't find a way to tie in the animation duration with CABasicAnimation.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let button = containerView.viewWithTag(68) as! UIButton
let animation = CABasicAnimation(keyPath: "shadowOpacity")
animation.fromValue = ?
animation.toValue = ?
}
And I suppose I would need to implement UIViewControllerInteractiveTransitioning but I can't think of a way to tie in CABasicAnimation. Any help appreciated

After doing some reading around it looks like someone has implemented a bridge between lower level CALayer animations and 'UIViewControllerInteractiveTransitioning'
https://github.com/stringcode86/UIPercentDrivenInteractiveTransitionWithCABasicAnimation/blob/master/InteractiveTransition/SCPercentDrivenInteractiveTransition.m
But it was done a long time ago and I haven't had time to verify it. One thing for sure is that it looks pretty ugly.
The UIViewPropertyAnimator class was released in iOS 10 and was meant to help out with this bridging by providing coordination with functions like 'addAnimation' and 'addCompletion'.
These guys have managed to implement a clean solution for bridging animations of 'CALayer' with 'UIViewControllerInteractiveTransitioning' via UIViewPropertyAnimator.
https://github.com/hedjirog/CustomPresentation
In summary
It's too hard. Don't bother.

Related

UIView animation snaps back to original state before transitioning to a new view

I have some UIButtons that I'm animating indefinitely. The buttons all have 3 sublayers that are added, each of which have their own animation. I'm initializing these animations on viewDidAppear which works great - they fade in and start rotating. The problem is that when I transition to a new view, the animations seem to "snap" back to their initial state, then back to some other state right before the transition occurs. I've tried explicitly removing all of the animations on viewWillDisappear, even tried hiding the entire UIButton itself, but nothing seems to prevent this weird snapping behavior from occurring.
Here's a gif of what's happening (this is me transitioning back and forth between two views):
func animateRotation() {
// Gets called on viewDidAppear
let rotationRight: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationRight.toValue = Double.pi * 2
rotationRight.duration = 4
rotationRight.isCumulative = true
rotationRight.repeatCount = Float.greatestFiniteMagnitude
rotationRight.isRemovedOnCompletion = false
rotationRight.fillMode = .forwards
let rotationLeft: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationLeft.toValue = Double.pi * -2
rotationLeft.duration = 3
rotationLeft.isCumulative = true
rotationLeft.repeatCount = Float.greatestFiniteMagnitude
rotationLeft.isRemovedOnCompletion = false
rotationLeft.fillMode = .forwards
let circleImage1 = UIImage(named: "circle_1")?.cgImage
circleLayer1.frame = self.bounds
circleLayer1.contents = circleImage1
circleLayer1.add(rotationRight, forKey: "rotationAnimation")
layer.addSublayer(circleLayer1)
let circleImage2 = UIImage(named: "circle_2")?.cgImage
circleLayer2.frame = self.bounds
circleLayer2.contents = circleImage2
circleLayer2.add(rotationLeft, forKey: "rotationAnimation")
layer.addSublayer(circleLayer2)
let circleImage3 = UIImage(named: "circle_3")?.cgImage
circleLayer3.frame = self.bounds
circleLayer3.contents = circleImage3
circleLayer3.add(rotationRight, forKey: "rotationAnimation")
layer.addSublayer(circleLayer3)
}
I would think something as simple as this would hide the button completely as soon as it knows it's going away:
override func viewWillDisappear(_ animated: Bool) {
animatedButton.isHidden = true
}
What's also interesting is that this seems to stop happening if I let it run for a couple minutes. This tells me that it might be some sort of race condition. Just not sure what that race might be...
When you use the UIView set of animations they set the actual state to the final state and then start the animation, ie:
UIView.animate(withDuration: 1) { view.alpha = 0 }
If you check alpha right after the animation starts, it's already 0.
This is a convenience that UIView does for you but that CALayer does not. When you set a CALayer animation you are only setting the animation value, not the actual value of the variable, so when the animation is done, the layer snaps back to its original value. Check the layer value while you are in the middle of the animation and you will see the real value has not changed; only the animated value in the presentation layer has changed.
If you want to replicate the UIView behavior you need to either set the actual final value not he layer when the animation begins or in the use the delegate to set it when the animation ends.

Preserving stokeEnd change from animation without isRemovedOnCompletion set to false

I have a progress circle that animates. I can hit a button and it animates to 20%, if I hit the button again it animates to 40%. I am setting isRemovedOnCompletion to false. However each time I perform an animation a new animation is added to the CAShapelayer. I imagine this is not good for performance. Is there a better way to do this?
Dummy code:
#IBAction func didTapAnimate(_ sender: Any) {
let animateStroke = CABasicAnimation(keyPath: "strokeEnd")
animateStroke.fromValue = index > 0 ? progressPts[index - 1] : 0
animateStroke.toValue = progressPts[index]
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
index+=1
}
I think no need to remove previous animation as per my knowledge only one animation layer is used for single key so animation automatically replace with previous one.
A string that identifies the animation. Only one animation per unique
key is added to the layer. The special key kCATransition is
automatically used for transition animations. You may specify nil for
this parameter.
So only the last animation occupies the memory previous animation automatically deallocated.
Just remove previous animation from circleLayer by calling removeAnimation(forKey:"yourkey")
#IBAction func didTapAnimate(_ sender: Any) {
circleLayer.removeAnimation(forKey: "MyAnimation")
let animateStroke = CABasicAnimation(keyPath: "strokeEnd")
animateStroke.fromValue = index > 0 ? progressPts[index - 1] : 0
animateStroke.toValue = progressPts[index]
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
index+=1
}

Shrink ViewController to circular shape transition - swift

I'm trying to follow the example in this project to create an expand/shrink view controller transition. In my case, the view controller which is being presented will have the same background color as the button itself, therefore this animation is essentially just the button expanding to cover the whole screen without ever disappearing.
https://github.com/AladinWay/TransitionButton
I've placed my button on the bottom right of the screen and managed to get it to expand using the following function:
private func expand(completion:(()->Void)?, revertDelay: TimeInterval) {
let expandAnim = CABasicAnimation(keyPath: "transform.scale")
let expandScale = (UIScreen.main.bounds.size.height/self.frame.size.height)*2
expandAnim.fromValue = 1.0
expandAnim.toValue = max(expandScale,26.0)
expandAnim.timingFunction = expandCurve
expandAnim.duration = 0.3
expandAnim.fillMode = .forwards
expandAnim.isRemovedOnCompletion = false
CATransaction.setCompletionBlock {
completion?()
// We return to original state after a delay to give opportunity to custom transition
DispatchQueue.main.asyncAfter(deadline: .now() + revertDelay) {
self.setOriginalState(completion: nil)
self.layer.removeAllAnimations() // make sure we remove all animation
}
}
layer.add(expandAnim, forKey: expandAnim.keyPath)
CATransaction.commit()
}
Now I'm trying to create the animation for dismissing the view controller which would essentially reverse the initial animation and shrink the whole screen back to the circular button on the bottom of the screen. I'm not sure how to adapt the above code to reverse this though.
You could try using the exact same block, except switch the fromValue and toValue.
expandAnim.fromValue = max(expandScale,26.0)
expandAnim.toValue = 1.0

Start/stop image view rotation animations

I have a start/stop button and an image view which I want to rotate.
When I press the button I want the to start rotating and when I press the button again the image should stop rotating. I am currently using an UIView animation, but I haven't figured out a way to stop the view animations.
I want the image to rotate, but when the animation stops the image shouldn't go back to the starting position, but instead continue the animation.
var isTapped = true
#IBAction func startStopButtonTapped(_ sender: Any) {
ruotate()
isTapped = !isTapped
}
func ruotate() {
if isTapped {
UIView.animate(withDuration: 5, delay: 0, options: .repeat, animations: { () -> Void in
self.imageWood.transform = self.imageWood.transform.rotated(by: CGFloat(M_PI_2))
}, completion: { finished in
ruotate()
})
} }
It is my code, but it doesn't work like I aspect.
Swift 3.x
Start Animation
let rotationAnimation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.toValue = NSNumber(value: .pi * 2.0)
rotationAnimation.duration = 0.5;
rotationAnimation.isCumulative = true;
rotationAnimation.repeatCount = .infinity;
self.imageWood?.layer.add(rotationAnimation, forKey: "rotationAnimation")
Stop animation
self.imageWood?.layer.removeAnimation(forKey: "rotationAnimation")
Swift 2.x
Start Animation
let rotationAnimation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.toValue = NSNumber(double: M_PI * 2.0)
rotationAnimation.duration = 1;
rotationAnimation.cumulative = true;
rotationAnimation.repeatCount = .infinity;
self.imageWood?.layer.addAnimation(rotationAnimation, forKey: "rotationAnimation")
Stop animation
self.imageWood?.layer.removeAnimation(forKey: "rotationAnimation")
If you use UIView Animation the OS still creates one or more CAAnimation objects. Thus, to stop a UIView animation you can still use:
myView.layer.removeAllAnimations()
Or if you create the animation using a CAAnimation on a layer:
myLayer.removeAllAnimations()
In either case, you can capture the current state of the animation and set that as the final state before removing the animation. If you're doing an animation on a view's transform, like in this question, that code might look like this:
func stopAnimationForView(_ myView: UIView) {
//Get the current transform from the layer's presentation layer
//(The presentation layer has the state of the "in flight" animation)
let transform = myView.layer.presentationLayer.transform
//Set the layer's transform to the current state of the transform
//from the "in-flight" animation
myView.layer.transform = transform
//Now remove the animation
//and the view's layer will keep the current rotation
myView.layer.removeAllAnimations()
}
If you're animating a property other than the transform you'd need to change the code above.
using your existing code you can achieve it the following way
var isTapped = true
#IBAction func startStopButtonTapped(_ sender: Any) {
ruotate()
isTapped = !isTapped
}
func ruotate() {
if isTapped {
let rotationAnimation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.toValue = NSNumber(value: Double.pi * 2.0)
rotationAnimation.duration = 1;
rotationAnimation.isCumulative = true;
rotationAnimation.repeatCount = HUGE;
self.imageWood?.layer.add(rotationAnimation, forKey: "rotationAnimation")
}else{
self.imageWood?.layer.removeAnimation(forKey: "rotationAnimation")
}
}
Apple added a new class, UIViewPropertyAnimator, to iOS 10. A UIViewPropertyAnimator allows you to easily create UIView-based animations that can be paused, reversed, and scrubbed back and forth.
If you refactor your code to use a A UIViewPropertyAnimator you should be able to pause and resume your animation.
I have a sample project on Github that demonstrates using a UIViewPropertyAnimator. I suggest taking a look at that.
Your code make the image rotate for 2PI angle, but if you click on the button while the rotation is not ended, the animation will finish before stop, that's why it comes to the initial position.
You should use CABasicAnimation to use a rotation that you can stop at anytime keeping the last position.

CABasicAnimation Quickly Jumping Before Animation Start

I have a CABasicAnimation animating the strokeEnd property of a CAShapeLayer. Every time I add the animation, it quickly jumps through the animation and then goes and does the real animation (as seen in the image above). If I add the animation in the viewDidLoad, this doesn't happen.
Here's my animation code:
let progressAnim = CABasicAnimation(keyPath: "strokeEnd")
progressAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
progressAnim.duration = 4.5
progressAnim.fromValue = 0.0
progressAnim.toValue = 1.0
progressAnim.removedOnCompletion = false
progressLayer.addAnimation(progressAnim, forKey: "progressAnimation")
progressLayer.strokeEnd = 1.0
I'm not sure what exactly I'm doing wrong, any help would be highly appreciated. Thanks!
My problem was with the line progressLayer.strokeEnd = 1.0. The reason I had that in my code was to stop the animation from going back to it's original values when it finished animating.
The next solution would be to set the fillMode to progressAnim.fillMode = kCAFillModeForwards and removedOnCompletion to progressAnim.removedOnCompletion = false. This sort of fixed my problem. But it created another. In my code, this solution doesn't update the strokeEnd property to the toValue.
My final solution was to set the toValue to the strokeEnd inside override func animationDidStop(anim: CAAnimation, finished flag: Bool).
My animation code:
let progressAnim = CABasicAnimation(keyPath: "strokeEnd")
progressAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
progressAnim.duration = animationDuration
progressAnim.fromValue = 0.0
progressAnim.toValue = 0.5
progressAnim.delegate = self
progressLayer.addAnimation(progressAnim, forKey: "progressAnimation")
Code for when animation is complete:
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
progressLayer.strokeEnd = 0.5
progressLayer.removeAllAnimations()
}

Resources