iOS stop animateWithDuration before completion - ios

I have a CollectionView and I want to create an animation inside the CollectionViewCell selected by the user. I chose to use animateKeyframesWithDuration because I want to create a custom animation step by step. My code looks like this:
func animate() {
UIView.animateKeyframesWithDuration(1.0, delay: 0.0, options: .AllowUserInteraction, animations: { () -> Void in
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.5, animations: { () -> Void in
// First step
})
UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5, animations: { () -> Void in
// Second step
})
}) { (finished: Bool) -> Void in
if self.shouldStopAnimating {
self.loadingView.layer.removeAllAnimations()
} else {
self.animate()
}
}
}
This is executed inside the custom CollectionViewCell when it is selected.
The problem is that I want to force stop the animation immediately at some certain point. But when I do that, the animation doesn't fully stop, it just moves the remaining animation on a different cell (probably the last reused cell?)
I can't understand why this is happening. I have tried different approaches but none of them successfully stop the animation before normally entering the completion block
Does anyone have any idea about this?

Instead of removing the animations from the layer you could try adding another animation with a very short duration that sets the view properties that you want to stop animating.
Something like this:
if self.shouldStopAnimating {
UIView.animate(withDuration: 0.01, delay: 0.0, options: UIView.AnimationOptions.beginFromCurrentState, animations: { () -> Void in
//set any relevant properties on self.loadingView or anything else you're animating
//you can either set them to the final animation values
//or set them as they currently are to cancel the animation
}) { (completed) -> Void in
}
}
This answer may also be helpful.

Related

For `UIView.animate`, is it possible to *chain* Swift Trailing Closures?

In Swift, I would like to have several calls to UIView.animate run in series. That is, when one animation finishes, then I would like another animation to continue after, and so on.
The call to UIView.animate has a Trailing Closure which I am currently using to make a second call to UIView.animate to occur.
The Problem is: I want to do N separate animations
From the Apple Documentation for UIView.animate
completion
A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.
Ideally, I would like to iterate over an array of animation durations and use in the calls to animate()
For example,
Goal
Iterate over an array and apply those parameters for each animation
let duration = [3.0, 5.0, 10.0]
let alpha = [0.1, 0.5, 0.66]
Xcode Version 11.4.1 (11E503a)
What I've tried
Use a map to iterate, and 🤞 hope that it works
Issue is that that only final animation occurs. So there is nothing in series
Research Q: Is it possible that I need to set a boolean in UIView that says animation occur in series?
let redBox = UIView()
redBox.backgroundColor = UIColor.red
self.view.addSubview(redBox)
let iterateToAnimate = duration.enumerated().map { (index, element) -> Double in
print(index, element, duration[index])
UIView.animate(withDuration: duration[index], // set duration from the array
animations: { () in
redBox.alpha = alpha[index]
}, completion:{(Bool) in
print("red box has faded out")
})
}
How to do one animation
let redBox = UIView()
redBox.backgroundColor = UIColor.red
self.view.addSubview(redBox)
// One iteration of Animation
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0
}, completion:{(Bool) in
print("red box has faded out")
})
Ugly way to do chain two animate iterations (wish to avoid)
// two iterations of Animation, using a trailing closure
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0
}, completion:{(Bool) in
print("red box has faded out")
}) { _ in // after first animation finishes, call another in a trailing closure
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0.75
}, completion:{(Bool) in
print("red box has faded out")
}) // 2nd animation
}
If the parameters are few and are known at compile time, the simplest way to construct chained animations is as the frames of a keyframe animation.
If the parameters are not known at compile time (e.g. your durations are going to arrive at runtime) or there are many of them and writing out the keyframe animation is too much trouble, then just put a single animation and its completion handler into a function and recurse. Simple example (adapt to your own purposes):
var duration = [3.0, 5.0, 10.0]
var alpha = [0.1, 0.5, 0.66] as [CGFloat]
func doTheAnimation() {
if duration.count > 0 {
let dur = duration.removeFirst()
let alp = alpha.removeFirst()
UIView.animate(withDuration: dur, animations: {
self.yellowView.alpha = alp
}, completion: {_ in self.doTheAnimation()})
}
}
you can use UIView.animateKeyframes
UIView.animateKeyframes(withDuration: 18.0, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 3.0/15.0, relativeDuration: 3.0/15.0, animations: {
self.redBox.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 8.0/15.00, relativeDuration: 5.0/15.0, animations: {
self.redBox.alpha = 0.75
})
}) { (completed) in
print("completed")
}

old swift code is not working (animating a UIView)

i have found this code which is responsible for animating a UIView but unfortunately the code does not work and i can not figure the reason (maybe an older version of swift)
this is the code :
(this is helper function according to the creator)
func moveView(#view:UIView, toPoint destination:CGPoint, completion☹()->())?) {
//Always animate on main thread
dispatch_async(dispatch_get_main_queue(), { () Void in
//Use UIView animation API
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping:
0.6, initialSpringVelocity: 0.3, options:
UIViewAnimationOptions.AllowAnimatedContent, animations: { () ->
Void in
//do actual move
view.center = destination
}, completion: { (complete) -> Void in
//when animation completes, activate block if not nil
if complete {
if let c = completion {
c()
}
}
})
})
}
and this is the animation
//Create your face object (Just a UIImageView with a face as the image
var face = Face();
face.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
//find our trajectory points
var center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2);
var left = CGPointMake(center.x *-0.3, center.y)
var right = CGPointMake(center.x *2.2, center.y)
//place our view off screen
face.center = right
self.view.addSubview(face)
//move to center
moveView(view: face, toPoint: center) { () -> () in
//Do your Pop
face.pop()
// Move to left
moveView(view: face, toPoint: left, completion: { () -> () in
}
}
and i quote from the creator of the code
General Steps: Create a new face on the right edge of the screen. Make
the face visible. Move the face to the middle of the screen. Pop the
face Start the process with the next face. Move the first face to the
left as soon as the new face gets to the middle.
Actual slide animation Once again, we will do the following here: Move
view off screen on the right Move to center Pop Move to left
To get the repeating effect, just call this method on a timer
and a summary :
UIView’s animation API is very powerful. Both the pop and movement
animations use depend on this API. If you’re stuck with trying to
create an animation, UIView animation block is usually a good place to
start.
NOTE : im a beginner in IOS development if anyone can please explain the code for me
Indeed this moveView method had a few issues, one being it was written for Swift 1 (but there were also some typos, faulty characters and useless operations).
Here's the fixed version:
func moveView(view view:UIView, toPoint destination: CGPoint, afterAnim: ()->()) {
//Always animate on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Use UIView animation API
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping:
0.6, initialSpringVelocity: 0.3, options:
UIViewAnimationOptions.AllowAnimatedContent, animations: { () -> Void in
//do actual move
view.center = destination
}, completion: { (complete) -> Void in
//if and when animation completes, callback
if complete {
afterAnim()
}
})
})
}
You can use it like this now:
moveView(view: face, toPoint: center) {
//Do your Pop
face.pop()
// Move to left
moveView(view: face, toPoint: left) {
// Do stuff when the move is finished
}
}
Observe the differences between your version and mine to understand what was obsolete/wrong and how I fixed it. I'll help if you're stuck.

using animatewithduration to move image view

I am trying to move an image multiple times. This is how i tried implementing it.
override func viewDidAppear(animated: Bool) {
UIView.animateWithDuration(1, animations: { () -> Void in
self.sattelite.center.x = 50
self.sattelite.center.y = 100
self.sattelite.center.x = 50
self.sattelite.center.y = 50
self.sattelite.center.x = 50
self.sattelite.center.y = 300
})
}
The animatewithduration method however only executes one of the movements. Is there any way to animate all three movements of the image view? Thanks.
you can easily chain animations ; start another at completion :
Here an illustration :
//1st animation
UIView.animateWithDuration(1.0,
delay: 0.0,
options: .CurveEaseInOut | .AllowUserInteraction,
animations: {
//some code ex : view.layer.alpha = 0.0
},
completion: { finished in
//second animation at completion
UIView.animateWithDuration(1.0
, delay: 0.0,
, options: .CurveEaseInOut | .AllowUserInteraction,
, animations: { () -> Void in
//some code ex : view.layer.alpha = 1.0
}
, completion: { finished in
//third animation at completion
UIView.animateWithDuration(1.0,
delay: 0.0,
options: .CurveEaseInOut | .AllowUserInteraction,
animations: {
//some code ex : view.layer.alpha = 0.0
},
completion: { finished in
//FINISH : 3 animations!!!
})
})
})
You need to either chain the animations with completion or use a Key Frame animation
Use the animation version which has completion:
class func animateWithDuration(_ duration: NSTimeInterval,
animations animations: () -> Void,
completion completion: ((Bool) -> Void)?)
Do the first animation only on animations and start the second in the completion. The third goes in the completion of the second.
So basically a chain of animations, where each starts when the previous finishes.

How to cancel previous animation when a new one is triggered?

I am writing a camera app, and have trouble with showing the focus square when user tap on the screen.
My code is (in swift):
self.focusView.center = sender.locationInView(self.cameraWrapper)
self.focusView.transform = CGAffineTransformMakeScale(2, 2)
self.focusView.hidden = false
UIView.animateWithDuration(0.5, animations: { [unowned self] () -> Void in
self.focusView.transform = CGAffineTransformIdentity
}, completion: { (finished) -> Void in
UIView.animateWithDuration(0.5, delay: 1.0, options: nil, animations: { () -> Void in
self.focusView.alpha = 0.0
}, completion: { (finished) -> Void in
self.focusView.hidden = true
self.focusView.alpha = 1.0
})
})
However, if use tap the screen consecutively when the previous animation does not finish, the old and new animation will mix up and the focus view will behave strangely, for example it will disappear very quick.
Could anyone tell me how to cancel previous animation, especially the previous completion block?
You can user method removeAllAnimations to stop animation
Replace your code with below
self.focusView.center = sender.locationInView(self.cameraWrapper)
self.focusView.transform = CGAffineTransformMakeScale(2, 2)
self.focusView.hidden = false
self.focusView.layer.removeAllAnimations() // <<==== Solution
UIView.animateWithDuration(0.5, animations: { [unowned self] () -> Void in
self.focusView.transform = CGAffineTransformIdentity
}, completion: { (finished) -> Void in
UIView.animateWithDuration(0.5, delay: 1.0, options: nil, animations: { () -> Void in
self.focusView.alpha = 0.0
}, completion: { (finished) -> Void in
self.focusView.hidden = true
self.focusView.alpha = 1.0
})
})
Reference : link
#Jageen solution is great, but I worked with UIStackView animation and there I needed additional steps. I have stackView with view1 and view2 inside, and one view should be visible and one hidden:
public func configureStackView(hideView1: Bool, hideView2: Bool) {
let oldHideView1 = view1.isHidden
let oldHideView2 = view2.isHidden
view1.layer.removeAllAnimations()
view2.layer.removeAllAnimations()
view.layer.removeAllAnimations()
stackView.layer.removeAllAnimations()
// after stopping animation the values are unpredictable, so set values to old
view1.isHidden = oldHideView1 // <- Solution is here
view2.isHidden = oldHideView2 // <- Solution is here
UIView.animate(withDuration: 0.3,
delay: 0.0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: 1,
options: [],
animations: {
view1.isHidden = hideView1
view2.isHidden = hideView2
stackView.layoutIfNeeded()
},
completion: nil)
}
In my case, I add the focusing indicator by using addSubview().
self.view.addSubview(square)
square is the UIView I defined in the function. So when the user tap the screen to focus, this function will add a square on subview.
And to cancel this animation when the next tap happen, I just simply use the removeFromSuperview() function, when this function called it removes the view from its superview which is the focusing square here.
filterView.subviews.forEach({ $0.removeFromSuperview() })
It's different from the method above to remove the animation, but remove the subview directly.
In my case, i just needed to set the value, i animated to concrete value.
For e.g.
if ([indexPath isEqual:self.currentPlayerOrderIndexPath])
{
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveEaseInOut
animations:^{
cell.avatarImageV.transform = CGAffineTransformMakeScale(1.15,1.15);
}
completion:NULL];
}
else
{
cell.avatarImageV.transform = CGAffineTransformMakeScale(1.0, 1.0);

Swift UIView animateWithDuration completion closure called immediately

I'm expecting the completion closure on this UIView animation to be called after the specified duration, however it appears to be firing immediately...
UIView.animateWithDuration(
Double(0.2),
animations: {
self.frame = CGRectMake(0, -self.bounds.height, self.bounds.width, self.bounds.height)
},
completion: { finished in
if(finished) {
self.removeFromSuperview()
}
}
)
Has anyone else experienced this? I've read that others had more success using the center rather than the frame to move the view, however I had the same problems with this method too.
For anyone else that is having a problem with this, if anything is interrupting the animation, the completion closure is immediately called. In my case, this was due to a slight overlap with a modal transition of the view controller that the custom segue was unwinding from. Using the delay portion of UIView.animateWithDuration(0.3, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations:{} had no effect for me. I ended up using GCD to delay animation a fraction of a second.
// To avoid overlapping with the modal transiton
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.2 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
// Animate the transition
UIView.animateWithDuration(0.3, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
// Animations
}, completion: { finished in
// remove the views
if finished {
blurView.removeFromSuperview()
snapshot.removeFromSuperview()
}
})
})
I resolved this in the end by moving the animation from hitTest() and into touchesBegan() in the UIView

Resources