Animation not working when use POPDecayAnimation - ios

I make dropping down animation for changing views. When I use POPSpringAnimation all working correctly. But when I change POPSpringAnimation to POPDecayAnimation nothing happens and in debug output I see this message:
ignoring to value on decay animation <POPDecayAnimation:0x7f9f2b5c8700; from = null; to = null; deceleration = 0.998000>
Code:
func switchViewController(from: UIViewController?, toViewController to: UIViewController?, animated: Bool) {
if animated {
to!.view.frame = self.view.frame
// Not working when call trulySwitchViewController
self.addChildViewController(to!)
self.view.insertSubview(to!.view, belowSubview: from!.view)
to!.didMoveToParentViewController(self)
// Calculate new x coord
var frame = from!.view.frame
frame.origin.y += frame.size.height
// POPDecayAnimation not working here
var animation = POPDecayAnimation()
animation.property = POPAnimatableProperty.propertyWithName(kPOPViewFrame) as POPAnimatableProperty
animation.toValue = NSValue(CGRect:frame)
animation.completionBlock = {
(_, _) in
self.trulySwitchViewController(from, toViewController: nil)
}
from!.view.pop_addAnimation(animation, forKey: "dropDown")
} else {
to?.view.frame = self.view.frame
self.trulySwitchViewController(from, toViewController: to)
}
}

For a decay animation you need to set the velocity property, this then decays to zero based on the deceleration property. So, the to property is not needed, the end state is determined based on the start position, velocity and deceleration.
You also need to animate the layer position not the view frame. So something like this should work...
var animation = POPDecayAnimation()
animation.property = POPAnimatableProperty.propertyWithName(kPOPLayerPosition) as POPAnimatableProperty
from!.view.layer.pop_addAnimation(animation, forKey: "dropDown")

Related

CALayer Rotation animation is not working on presented ViewController

I have two view controllers (i.e firstVC and secondVC). I am presenting secondVC over firstVC and trying to achieve rotation animation but it's not working, I have tried running animation on the main thread with some delay but it's giving weird behavior. Following is the code I am using to rotate the view.
extension UIView {
private static let kRotationAnimationKey = "rotationanimationkey"
func rotate(duration: Double = 1, delay: Int = 0) {
if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Float.pi * 2.0
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(delay)) {
self.layer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
}
}
}
func stopRotating() {
if layer.animation(forKey: UIView.kRotationAnimationKey) != nil {
layer.removeAnimation(forKey: UIView.kRotationAnimationKey)
}
}}
Note: By pushing 'secondVC' animation is working properly. But I wanted to present 'secondVC'.
I am not getting what's happening during the presentation of the view controller and rotation animation.
Please let me know if you find any solution.
For using custom presentation transition you have to confirm to UIViewControllerTransitioningDelegate. UIKit always call animationController(forPresented:presenting:source:) to see if a UIViewControllerAnimatedTransitioning is returned. If it is returning nil then default animation is used.

UISlider get Value while Anmation runs

I animate a UISlider with this function:
func animateSlider(){
slider.value = Float(min)
UIView.animate(withDuration: 2.0, animations: {
self.slider.setValue(Float(self.max), animated:true)
})
When I tap the screen I want to get the current value but it only returns the maximum value. What can I do to solve that problem?
When you schedule an animation Core Animation sets the model layer's properties immediately, and the creates a presentation layer which you may inspect. This presentation layer is what you actually see on screen, and is valid to query its properties such as its frame, bounds, position, etc.
You were asking for the model layer's value which is set immediately in your animation block, when instead what you want is what is displayed through the presentation layer.
You can get the currently displayed presentation value from the label when there is an in-progress animation by using this extension on UISlider:
extension UISlider {
var currentPresentationValue: Float {
guard let presentation = layer.presentation(),
let thumbSublayer = presentation.sublayers?.max(by: {
$0.frame.height < $1.frame.height
})
else { return self.value }
let bounds = self.bounds
let trackRect = self.trackRect(forBounds: bounds)
let minRect = self.thumbRect(forBounds: bounds, trackRect: trackRect, value: 0)
let maxRect = self.thumbRect(forBounds: bounds, trackRect: trackRect, value: 1)
let value = (thumbSublayer.frame.minX - minRect.minX) / (maxRect.minX - minRect.minX)
return Float(value)
}
}
And its usage:
func animateSlider() {
slider.value = 0
DispatchQueue.main.async {
UIView.animate(withDuration: 2, delay: 0, options: .curveLinear, animations: {
self.slider.setValue(1, animated: true)
}, completion: nil)
}
// quarter done:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
print(self.slider.currentPresentationValue)
// prints 0.272757, expected 0.25 with some tolerance
}
// half done:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print(self.slider.currentPresentationValue)
// prints 0.547876, expected 0.5 with some tolerance
}
// three quarters done:
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
print(self.slider.currentPresentationValue)
// prints 0.769981, expected 0.75 with some tolerance
}
}
I don't think that a slider's value property can be animated using UIView.animate(). However, if you read the documentation on UISlider, you'll find the function
func setValue(Float, animated: Bool)
Use that to animate your slider to a new position.
If you want to animate a change over a longer time period like 2 seconds, though, you'll probably have to set up a repeating timer or a CADisplayLink timer that changes the slider value by small increments each time it fires.
As for your statement "When I tap the screen I want to get the current value but it only returns the maximum" That is the way Cocoa animation works. You can't interrogate an in-flight animation and get a value that's the value of the property you are animating at that instant. (You could do that if you used a repeating timer to animate the slider though.)

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.

'Press and hold' animation without NSTimer

Update: The NSTimer approach works now, but comes with a huge performance hit. The question is now narrowed down to an approach without NSTimers.
I'm trying to animate a 'Press and hold' interactive animation. After following a load of SO answers, I've mostly followed the approach in Controlling Animation Timing by #david-rönnqvist. And it works, if I use a Slider to pass the layer.timeOffset.
However, I can't seem to find a good way to continuously update the same animation on a press and hold gesture. The animation either doesn't start, only shows the beginning frame or at some points finishes and refuses to start again.
Can anyone help with achieving the following effect, without the horrible NSTimer approach I'm currently experimenting with?
On user press, animation starts, circle fills up.
While user holds (not necessarily moving the finger), the animation should continue until the end and stay on that frame.
When user lifts finger, the animation should reverse, so the circle is empties again.
If the user lifts his finger during the animation or presses down again during the reverse, the animation should respond accordingly and either fill or empty from the current frame.
Here's a Github repo with my current efforts.
As mentioned, the following code works well. It's triggered by a slider and does its job great.
func animationTimeOffsetToPercentage(percentage: Double) {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
let timeOffset = maximumDuration * percentage
print("Set animation to percentage \(percentage) with timeOffset: \(timeOffset)")
fillAnimationLayer.timeOffset = timeOffset
}
However, the following approach with NSTimers works, but has an incredible performance hit. I'm looking for an approach which doesn't use the NSTimer.
func beginAnimation() {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset >= 1.0 {
self.layer.timeOffset = self.maximumDuration
}
else {
self.layer.timeOffset += 0.1
}
})
}
func reverseAnimation() {
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset <= 0.0 {
self.layer.timeOffset = 0.0
}
else {
self.layer.timeOffset -= 0.1
}
})
}
When you use slider you use fillAnimationLayer layer for animation
fillAnimationLayer.timeOffset = timeOffset
However, in beginAnimation and reverseAnimation functions you are using self.layer.
Try to replace self.layer.timeOffset with self.fillShapeLayer!.timeOffset in your timer blocks.
The solution is two-fold;
Make sure the animation doesn't remove itself on completion and keeps its final frame. Easily accomplished with the following lines of code;
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
The hard part; you have to remove the original animation and start a new, fresh reverse animation that begins at the correct point. Doing this, gives me the following code;
func setAnimation(layer: CAShapeLayer, startPath: AnyObject, endPath: AnyObject, duration: Double)
{
// Always create a new animation.
let animation: CABasicAnimation = CABasicAnimation(keyPath: "path")
if let currentAnimation = layer.animationForKey("animation") as? CABasicAnimation {
// If an animation exists, reverse it.
animation.fromValue = currentAnimation.toValue
animation.toValue = currentAnimation.fromValue
let pauseTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
// For the timeSinceStart, we take the minimum from the duration or the time passed.
// If not, holding the animation longer than its duration would cause a delay in the reverse animation.
let timeSinceStart = min(pauseTime - startTime, currentAnimation.duration)
// Now convert for the reverse animation.
let reversePauseTime = currentAnimation.duration - timeSinceStart
animation.beginTime = pauseTime - reversePauseTime
// Remove the old animation
layer.removeAnimationForKey("animation")
// Reset startTime, to be when the reverse WOULD HAVE started.
startTime = animation.beginTime
}
else {
// This happens when there is no current animation happening.
startTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
animation.fromValue = startPath
animation.toValue = endPath
}
animation.duration = duration
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
layer.addAnimation(animation, forKey: "animation")
}
This Apple article explains how to do a proper pause and resume animation, which is converted to use with the reverse animation.

Playing a "snap in/snap out" animation during zoom in, zoom out

So i got a set of rings one inside the other. I want to be able to zoom then in and, when I zoom close enough the zooming should animate the snap in and then I should keep zooming again.
I've done the zooming part pretty easily, using CGAffineTransform and UIPinchGesture recognizer, here is how the scale method looks like:
func scale(gesture: UIPinchGestureRecognizer) {
if !animationInProgress {
for var i = 0; i < scrollViews.count; i++ {
var view = scrollViews[i]
var j = 0
for ; j < scrollViews.count; j++ {
if view == scrollViews[j] {
break
}
}
println("Animation in progress: \(animationInProgress)")
if view.frame.origin.x < -80 || view.frame.origin.y < -140 {
if !view.hidden {
currentViewIndex = j + 1
view.hidden = true
view.visible = false
println("new view got out of the screen")
snapIn = true
animateSnap(snapIn)
}
} else {
if view.hidden == true {
currentViewIndex = j
view.hidden = false
view.visible = true
println("new view got into the screen")
snapIn = false
animateSnap(snapIn)
}
}
view.transform = CGAffineTransformScale(view.transform, gesture.scale, gesture.scale)
println("transformed")
}
gesture.scale = 1
}
}
so we iterate through our views, the frame.origin x and y checks, whether the views is out of the screen and I set some flags, which turn of some calculations.
The idea is the following, when one circle gets out of the screen, the rest animate zoom in and zoom out, when the new view gets on the screen.
Here is a animateSnap function:
private func animateSnap(snapIn: Bool) {
let factor: CGFloat = snapIn ? 1.5 : 0.5
for var a = currentViewIndex; a < scrollViews.count; a++ {
let next = scrollViews[a]
var transformAnimation = CABasicAnimation(keyPath: "transform")
transformAnimation.duration = 1
transformAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
transformAnimation.removedOnCompletion = false
transformAnimation.fillMode = kCAFillModeForwards
transformAnimation.delegate = self
let transform = CATransform3DScale(next.layer.transform, factor, factor, 1)
transformAnimation.toValue = NSValue(CATransform3D:transform)
next.layer.addAnimation(transformAnimation, forKey: "transform")
}
}
The question is: does somebody know an elegant solution to this or see any flaw in my approach ? The problem happens when the animation ends the views kinda go back to where they were at the start. I don't know why because remove on completion is set to false. In addition, can the fact that I use CGAffineTransform for zooming and CATransform3D somehow affect the result ?
Thank you very much !
For everybody who might encounter the same problem, when you work with 2D use CGAffineTransform instead of CATransform3D. That solved the problem, in addition I suspect that they also change different things(for example, CATransform3D changes layer) that's why when animation finished the result did not look as expected.

Resources