Synchronize CABasicAnimation and UIView.animate to be the same - ios

I'm trying to create an animation where when the user drags a UIView then release it, the UIView return to it's initial position. Problems is, I have CAShapeLayer connected to that UIView that I want to stay connected while the UIView return to it's place. CAShapeLayer uses CABasicAnimation and the UIView uses UIView.animate.
How can I get the same timing between the two if I want the same EaseOut deceleration? Right now both animation are way off...
let destinations = self.myAspectDestination[self.viewToDrag.accessibilityIdentifier!] ?? []
for destination in destinations {
if let link: CAShapeLayer = self.myAspects["\(self.viewToDrag.accessibilityIdentifier!):\(destination)"] {
let newPath = UIBezierPath()
newPath.move(to: (self.myPlanets[destination]?.center)!)
newPath.addLine(to: self.originalLocation)
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.isRemovedOnCompletion = true
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.fillMode = kCAFillModeBoth // keep to value after finishing
animation.fromValue = link.path
animation.toValue = newPath.cgPath
animation.timingFunction = CAMediaTimingFunction(name: "easeInEaseOut")
link.path = newPath.cgPath
link.add(animation, forKey: animation.keyPath)
}
}
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.viewToDrag.center = self.originalLocation
}) { (result) in
self.viewToDrag = nil
}

Related

Resume CABasicAnimation backwards by setting .speed equal to -1

EDIT: i've refactored the question a bit and solved part of the issue, now the question comes down to why does the presentation layer glitches/flashes when the animation is resumed. At this point tho i'm accepting any answer that makes the animation resume both forwards and backwards at will with no issue. I'm not sure the approach i'm using is the right one, i'm still pretty new to Swift.
Note: Sample project at the bottom, for having a better understanding of the issue.
In my project i'm pausing a CABasicAnimation by setting the layer .speed property to 0, then i'm changing the animation value interactively by setting the layer's .timeOffset property equal to a UISlider .value property whenever the user scrolls the slider. By code:
layer.speed = 0
Then when the user slides:
layer.timeOffset = CFTimeInterval(sender.value)
Now i want to resume the animation backwards or forwards at will whenever the user gesture on the slider ends, so from the starting point related to the current animation value. The only viable solution i've found which runs smoothly is the following, but it works only going forwards:
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
Then i can simply pause it again at the completion of the animation:
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 0
layer.speed = 0
}
From my understanding, .speed not only defines the actual speed of the animation combined with the .duration property, but also the direction of the animation: if i set a layer's speed equal to -1 then the animation completes backwards. Referring to this answer in regards to how CAMediaTiming works, i was trying to change the up above snippet's parameters to resume the animation going backwards with no luck. I thought this would work:
let pausedTime = layer.timeOffset
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
layer.timeOffset = pausedTime*2
layer.speed = -1.0
but the layer is never animated like so. The issue seems to be related to the convertTime method.
Then i found this question which is basically the same of mine, and the only answer has a decent solution. Refactoring a bit the code, i can just say:
layer.beginTime = CACurrentMediaTime()
layer.speed = -1
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 0
layer.speed = 0
}
However, when the animation is played backwards is very glitchy, in particular the presentation layer flashes both at the resume and on completion. I've tried various solutions with no luck, some speculations i've made:
it may be an issue related to CAMediaTimingFillMode, as i can set it .back or .forwards but when it resumes the animation is neither in it's final nor in it's initial state and thus the initial frame is not rendered;
it is caused by the fact that i'm not keeping the modal tree and the presentation tree synchronized.
Both of these however doesn't explain while it flickers/flashes both on resume and on completion. Additionally, it seems to me that the animation may have a duration of 1 when resumed forwards, but only of 1-timeOffset when resumed backwards, not sure tho.
Really not sure what's the actual problem and how to fix this mess. All suggestions are more than welcomed.
For anyone interested, here's a sample project similar to mine, inspired by another question (animation is running forward, to run it backwards and catch the glitch just call resumeLayerBackwards()). I know the code should be refactored, but still for the purpose it's fine. Just copy, paste and run:
import UIKit
class ViewController: UIViewController {
var perspectiveLayer: CALayer = {
let perspectiveLayer = CALayer()
perspectiveLayer.speed = 0.0
return perspectiveLayer
}()
var mainView: UIView = {
let view = UIView()
return view
}()
private let slider: UISlider = {
let slider = UISlider()
slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
return slider
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(slider)
animate()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
slider.frame = CGRect(x: view.bounds.size.width/3,
y: view.bounds.size.height/10*8,
width: view.bounds.size.width/3,
height: view.bounds.size.height/10)
}
#objc private func slide(sender: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended:
resumeLayer(layer: perspectiveLayer)
default:
perspectiveLayer.timeOffset = CFTimeInterval(sender.value)
}
}
}
private func resumeLayer(layer: CALayer) {
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 1.0
layer.speed = 0.0
}
}
private func resumeLayerBackwards(layer: CALayer) {
layer.beginTime = CACurrentMediaTime()
layer.speed = -1
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 0
layer.speed = 0
}
}
private func animate() {
var transform:CATransform3D = CATransform3DIdentity
var topSleeve:CALayer
var middleSleeve:CALayer
var bottomSleeve:CALayer
var topShadow:CALayer
var middleShadow:CALayer
let width:CGFloat = 300
let height:CGFloat = 150
var firstJointLayer:CALayer
var secondJointLayer:CALayer
mainView = UIView(frame:CGRect(x: 50, y: 50, width: width, height: height*3))
mainView.backgroundColor = UIColor.yellow
view.addSubview(mainView)
perspectiveLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer = CATransformLayer()
firstJointLayer.frame = mainView.bounds
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve = CALayer()
topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
topSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
topSleeve.backgroundColor = UIColor.red.cgColor
topSleeve.position = CGPoint(x: width/2, y: 0)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer = CATransformLayer()
secondJointLayer.frame = mainView.bounds
secondJointLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
secondJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
secondJointLayer.position = CGPoint(x: width/2, y: height)
firstJointLayer.addSublayer(secondJointLayer)
middleSleeve = CALayer()
middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
middleSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
middleSleeve.backgroundColor = UIColor.blue.cgColor
middleSleeve.position = CGPoint(x: width/2, y: 0)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve = CALayer()
bottomSleeve.frame = CGRect(x: 0, y: height, width: width, height: height)
bottomSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
bottomSleeve.backgroundColor = UIColor.gray.cgColor
bottomSleeve.position = CGPoint(x: width/2, y: height)
secondJointLayer.addSublayer(bottomSleeve)
firstJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
firstJointLayer.position = CGPoint(x: width/2, y: 0)
topShadow = CALayer()
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.black.cgColor
topShadow.opacity = 0
middleShadow = CALayer()
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.black.cgColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform
var animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = -90*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = 180*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = -160*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.height")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = perspectiveLayer.bounds.size.height
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.y")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = perspectiveLayer.position.y
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
}
}
I managed to remove the glitch for resumeLayerBackwards(layer:) in the sample project.
Two problems there in fact:
there is an empty screen after animation has visually finished
the empty screen is visible for 1 - .timeOffset seconds
So, seems like the problem is that animation in fact plays not just for .timeOffset period, but for the whole .duration period. And the empty screen appears because there is no animation defined for 1 - .timeOffset block.
Just to recall: CALayer also adopts CAMediaTiming protocol, as CAAnimation does (with all the properties defined: although some of them seem not be very clear how to be applied to a layer).
With speed = -1 after .timeOffset seconds passed — the property .timeOffset becomes equal to zero. It means that animation has reached its beginning and therefore (with negative speed) it is finished. Though it is not that obvious — seems like it is removed because of the .fillMode property. To fix this I've added perspectiveLayer.fillMode = .forwards to animate() method.
To have animation completed exactly after .timeOffset seconds instead of the whole .duration — use .repeatDuration property. I've added layer.repeatDuration = layer.timeOffset to your resumeLayerBackwards(layer:) method.
The project works only with both lines added.
I can't say that the solution is really logical for me, although it works. Negative speed works a bit unpredictable as for me. In my project I used to reverse animation by swapping begin and end values in cloned animation object.

Multiple Animations For Same UIView in Swift

I am trying to add multiple animations for my image view, but only one of them is animated. Please check the below code. I created scale and rotate animations for my image view but only i see the scale animation when run the below code.
//Rotate animation
let rotation: CABasicAnimation = CABasicAnimation(keyPath:
"transform.rotation.y")
rotation.toValue = 0
rotation.fromValue = 2.61
//Scale animation
let scale: CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
scale.toValue = 1
scale.fromValue = 0
//Adding animations to group
let group = CAAnimationGroup()
group.animations = [rotation,scale]
group.duration = 0.2
myImage.layer.add(group, forKey: nil)
The rotation occurs but the duration is less to notice
group.duration = 0.2
when changed to 5 seconds see
in your completion you can put your animations, when finish one, wait second.. and etc
let myImage = UIImageView()
UIView.animate(withDuration: 1.0, animations: {
let rotation: CABasicAnimation = CABasicAnimation(keyPath:
"transform.rotation.y")
rotation.toValue = 0
rotation.fromValue = 2.61
}, completion: { (value: Bool) in
UIView.animate(withDuration: 1.0, animations: {
//you can here put your other animation
})
})

How to animate the view with pre-existing animation?

I want to animate a view with the pre-existing animation of UIView.
I am using this code for animating the UIView -
CATransition *transition = [[CATransition alloc] init];
transition.duration = 0.1;
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
[viewToAnimate.layer addAnimation:transition forKey:kCATransition];
I want to animate some other view, along with this animation.
What you need to do is to chain your animations. Here is example from Apple.
let fadeOut = CABasicAnimation(keyPath: "opacity")
fadeOut.fromValue = 1
fadeOut.toValue = 0
fadeOut.duration = 1
let expandScale = CABasicAnimation()
expandScale.keyPath = "transform"
expandScale.valueFunction = CAValueFunction(name: kCAValueFunctionScale)
expandScale.fromValue = [1, 1, 1]
expandScale.toValue = [3, 3, 3]
let fadeAndScale = CAAnimationGroup()
fadeAndScale.animations = [fadeOut, expandScale]
fadeAndScale.duration = 1
You can use basic iOS animation that is:
UIView .animate(withDuration: TimeInterval) {
code
}
In the code segment you can write to change the x positions of different views. whenever you will click on button that respective view's x corrdinate will animate to 0.
Try this
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
let containerView = transitionContext.containerView()
let bounds = UIScreen.mainScreen().bounds
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
containerView.addSubview(toViewController.view)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: {
fromViewController.view.alpha = 0.5
toViewController.view.frame = finalFrameForVC
}, completion: {
finished in
transitionContext.completeTransition(true)
fromViewController.view.alpha = 1.0
})
}

iOS animating object goes back to original position

I am trying to animate the position of a UIView object with CABasicAnimation on a button Tap. The object animated and moves to the 'to' position, but returns back to the original position after the animation ends. I want to retain the position of the view object even after the animation ends. This the code snippet that performs the animation. viewObject is the object which I'm trying to animate.
let animation = CABasicAnimation(keyPath: "position")
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1.0)
animation.duration = 0.5
animation.fromValue = NSValue(cgPoint: CGPoint(x: viewObject.center.x, y: viewObject.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: viewObject.center.x + 64, y: viewObject.center.y))
viewObject.layer.add(animation, forKey: "position")
add following lines before adding animation
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
Swift
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
Please add the following code:
Objective-C:
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
Swift 4:
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
I think you need to give frame again on completion. So, this can be a nice approach
UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveLinear, animations: {
let animation = CABasicAnimation(keyPath: "position")
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1.0)
animation.duration = 0.5
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.viewObject.center.x, y: self.viewObject.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.viewObject.center.x + 64, y: self.viewObject.center.y))
self.viewObject.layer.add(animation, forKey: "position")
}, completion: { finished in
self.viewObject.frame.origin.x = self.viewObject.frame.origin.x + 64
})
Try this. It will work perfectly
UIView.animateWithDuration(0.7, delay: 1.0, options: .CurveEaseOut, animations:
{
Code
}, completion:
{
finished in
println("Nothing to do!")
})
Just do animation inside a block and when it finishes. hold that position and do not revert back. It should stay at same position
and add this line inside code
cabasicanimation.removedOnCompletion = false;
This line will make it do not go back in the same state
If you want keep your position, you should put your code same my structure
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock {
// Input code set new position at here
}
// Intput code animation
CATransaction.commit()

How to animate view layer shadow with UIViewPropertyAnimator

I know we need to use CABasicAnimation to animate shadow, but I don't know how to integrate UIViewPropertyAnimator with CABasicAnimation.
let shadowAnimation = CABasicAnimation(keyPath: "shadowOpacity")
shadowAnimation.fillMode = kCAFillModeForwards
shadowAnimation.isRemovedOnCompletion = false
shadowAnimation.fromValue = 0.3
shadowAnimation.toValue = 0
shadowAnimation.duration = transitionDuration
animator = UIViewPropertyAnimator(duration: transitionDuration, dampingRatio: 95, animations: {
topShadowContainer.layer.add(shadowAnimation, forKey: "shadowOpacity")
}
I found the solution.

Resources