How to animate view layer shadow with UIViewPropertyAnimator - ios

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.

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
})
})

Synchronize CABasicAnimation and UIView.animate to be the same

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
}

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()

CALayer resizing back to original size after transform animation

I'm trying to implement two consecutive transformation animations. When the first animation ends, the second animation is called through the completion handler. Because this is a transformation animation, my issue is that when the first animation finishes, the layer resizes back to the original size, and then the second animation begins. I'd like for the second animation to begin with the new layer size after the first transformation animation. This post Objective-C - CABasicAnimation applying changes after animation? says I have to resize/transform the layer before beginning the first animation, so that when the first animation ends, the layer is actually the new size. I've tried to do that by changing the bounds or actually applying the transform to the layer but its still not working.
override func viewDidAppear(_ animated: Bool) {
buildBar()
}
func buildBar(){
progressBar1.bounds = CGRect(x: 0, y: 0, width: 20, height: 5)
progressBar1.position = CGPoint(x: 0, y: 600)
progressBar1.backgroundColor = UIColor.white.cgColor
view.layer.addSublayer(progressBar1)
extendBar1()
}
func extendBar1(){
CATransaction.begin()
let transform1 = CATransform3DMakeScale(10, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
// self.progressBar1.bounds = CGRect(x: 0, y: 0, width: 200, height: 5)
// self.progressBar1.transform = transform1
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.toValue = NSValue(caTransform3D:transform1)
anim.duration = 5.00
CATransaction.setCompletionBlock {
self.extendBar2()
}
progressBar1.add(anim, forKey: "transform")
CATransaction.commit()
}
func extendBar2(){
let transform1 = CATransform3DMakeScale(2, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.toValue = NSValue(caTransform3D:transform1)
anim.duration = 5.00
progressBar1.add(anim, forKey: "transform")
}
Because you are modifying the transform property of the layer in both animation, it will be easier here to use CAKeyframeAnimation, which will handle the chaining of the animations for you.
func extendBar(){
let transform1 = CATransform3DMakeScale(10, 1, 1)
let transform2 = CATransform3DMakeScale(2, 1, 1)
let anim = CAKeyframeAnimation()
anim.keyPath = "transform"
anim.values = [progressBar1.transform, transform1, transform2] // the stages of the animation
anim.keyTimes = [0, 0.5, 1] // when they occurs, 0 being the very begining, 1 the end
anim.duration = 10.00
progressBar1.add(anim, forKey: "transform")
progressBar1.transform = transform2 // we set the transform property to the final animation's value
}
A word about the content of values and keyTimes:
We set the first value to be the current transform of progressBar1. This will make sure we start in the current layer's state.
In keyTimes, we say that at the begining, the first value in the values array should be used. We then say at animation's half time, the layer should be transformed in values second value. Hence, the animation between the initial state and the second one will occurs during that time. Then, between 0.5 to 1, we'll go from tranform1 to transform2.
You can read more about animations in this very nice article from objc.io.
If you really need two distinct animations (because maybe you want to add subviews to progressBar1 in between, here's the code that will do it
func extendBar1() {
CATransaction.begin()
CATransaction.setCompletionBlock {
print("side effects")
extendBar2()
}
let transform1 = CATransform3DMakeScale(5, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.fromValue = progressBar1.transform
anim.toValue = transform1
anim.duration = 2.00
progressBar1.add(anim, forKey: "transform")
CATransaction.setDisableActions(true)
progressBar1.transform = transform1
CATransaction.commit()
}
func extendBar2() {
CATransaction.begin()
let transform2 = CATransform3DMakeScale(2, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.fromValue = progressBar1.transform
anim.toValue = transform2
anim.duration = 2.00
progressBar1.add(anim, forKey: "transform")
CATransaction.setDisableActions(true)
progressBar1.transform = transform2
CATransaction.commit()
}
What happens here? Basically, we setup a first "normal animation". So we create the animation, that will modify the presentation layer and set the actual layer to the final first animation's transform.
Then, when the first animation completes, we call extendBar2 which will in turn queue a normal animation.
You also want to call CATransaction.setDisableActions(true) before explicitly updating the transform, otherwise, core animation will create an implicit one, which will override the one created just before.

Resources