Calling Consecutive Animations in Swift with Completion Handler

Calling consecutive animations in Swift with completion handler

I ended up going with

Timer.scheduledTimer(timeInterval: stepDuration, target: self,
selector: #selector(self.animate), userInfo: nil, repeats: true)

similar to James P's comment.

Swift: consecutive CABasicAnimations that repeat

I like @matt's answer and always appreciate their input but since I was trying to use CAAnimation (specifically I wanted to use CAKeyframeAnimation), I ended up nesting two CATransactions using CATransaction.begin() and CATransaction.setCompletionBlock to start one animation exactly at the end of the other and then to recursively call the function repeatedly.

CATransaction documentation

Swift - the completion ends before the animation does

The problem is that UIView.animate() can only be used on animatable properties, and progress is not an animatable property. "Animatable" here means "externally animatable by Core Animation." UIProgressView does its own internal animations, and that conflicts with external animations. This is UIProgressView being a bit over-smart, but we can work around it.

UIProgressView does use Core Animation, and so will fire CATransaction completion blocks. It does not, however, honor the duration of the current CATransaction, which I find confusing since it does honor the duration of the current UIView animation. I'm not actually certain how both of these are true (I would think that the UIView animation duration would be implemented on the transaction), but it seems to be the case.

Given that, the way to do what you're trying looks like this:

func updateProgress() {
if self.index < self.images.count {
progressBar[index].setProgress(0.01, animated: false)

CATransaction.begin()
CATransaction.setCompletionBlock {
self.index += 1
self.updateProgress()
}
UIView.animate(withDuration: 5, delay: 0, options: .curveLinear,
animations: {
self.progressBar[self.index].setProgress(1.0, animated: true)
})
CATransaction.commit()
}
}

I'm creating a nested transaction here (with begin/commit) just in case there is some other completion block created during this transaction. That's pretty unlikely, and the code "works" without calling begin/commit, but this way is a little safer than messing with the default transaction.

Consecutive Animation Calls Not Working

I added some print lines to your animateFinished method, in order to see what's going on:

func animateFinished(textToDisplay: String, footerBtn: UIButton, footerImg: UIImageView) {
//Should cancel any current animation
print("Remove animations")
footerBtn.layer.removeAllAnimations()
print("Animations removed")

footerBtn.alpha = 0
footerBtn.setTitle(textToDisplay, forState: UIControlState.Normal)
footerBtn.titleLabel!.font = UIFont(name: "HelveticaNeue-Regular", size: 18)
footerBtn.setTitleColor(UIColor(red: 255/255.0, green: 255/255.0, blue: 255/255.0, alpha: 1.0), forState: UIControlState.Normal)
//footerBtn.backgroundColor = UIColor(red: 217/255.0, green: 217/255.0, blue: 217/255.0, alpha: 1.0)
print("Initial animation setup completed")

UIView.animateKeyframesWithDuration(3.0 /*Total*/, delay:0.0, options: UIViewKeyframeAnimationOptions.CalculationModeLinear, animations: {

UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration:0.10, animations:{
footerImg.alpha = 0.01 //Img fades out
footerBtn.backgroundColor = UIColor(red: 46/255.0, green: 103/255.0, blue: 00/255.0, alpha: 0.6) //Bg turns to green
})

UIView.addKeyframeWithRelativeStartTime(0.10, relativeDuration:0.30, animations:{
footerBtn.alpha = 1 //Text and green bg fades in
footerBtn.backgroundColor = UIColor(red: 46/255.0, green: 173/255.0, blue: 11/255.0, alpha: 0.6) //BG turns greener
})

UIView.addKeyframeWithRelativeStartTime(0.40, relativeDuration:0.50, animations:{
footerBtn.alpha = 0.01 //Text fades out & bg fade out
})

},
completion: { finished in
print("Completion block started")
footerImg.alpha = 1
footerBtn.alpha = 1
footerBtn.backgroundColor = UIColor.clearColor()
footerBtn.setTitleColor(UIColor(red: 55/255.0, green: 55/255.0, blue: 55/255.0, alpha: 1.0), forState: UIControlState.Normal)
footerBtn.titleLabel!.font = UIFont(name: "HelveticaNeue-Light", size: 18)
footerBtn.setTitle("", forState: UIControlState.Normal)
//Completion blocks sets values back to norm
print("Completion block finished")
}
)
}//End of 'Finished' animation

If you allow the animations to run to completion, the log shows, as you would expect:

Remove animations
Animations removed
Initial animation setup completed
Completion block started
Completion block finished

But if you tap the button during the animation, you see this:

Remove animations
Animations removed
Initial animation setup completed
Remove animations
Animations removed
Initial animation setup completed
Completion block started
Completion block finished
Completion block started
Completion block finished

What's happening it that the removeAllAnimations causes the completion block (for the first call) to be executed, after the initial setup for the second call is completed, but before the second animations are undertaken. So, for example, the button title is "" during the second animation.

The fix is relatively straight forward: don't execute the completion block if the animations have not finished:

        completion: {  finished in
if (!finished) {
return
}
print("Completion block started")
footerImg.alpha = 1
footerBtn.alpha = 1
footerBtn.backgroundColor = UIColor.clearColor()
footerBtn.setTitleColor(UIColor(red: 55/255.0, green: 55/255.0, blue: 55/255.0, alpha: 1.0), forState: UIControlState.Normal)
footerBtn.titleLabel!.font = UIFont(name: "HelveticaNeue-Light", size: 18)
footerBtn.setTitle("", forState: UIControlState.Normal)
print("Completion block finished")
//Completion blocks sets values back to norm
}

Also, as per Shripada, you will need to remove animations from footerImg as well as footerBtn, with:

footerImg.layer.removeAllAnimations()

at the start of the method.

How to sequence two animations with delay in between

You can perform the second animation in the completionHandler presented on UIView.animate

let duration = self.transitionDuration(using: transitionContext)

let firstAnimDuration = 0.5
UIView.animate(withDuration: firstAnimDuration, animations: {
/* Do here the first animation */
}) { (completed) in

let secondAnimDuration = 0.5
UIView.animate(withDuration: secondAnimDuration, animations: {
/* Do here the second animation */
})
}

Now you could have another problem.

If you rotate your view with the CGAffineTransform and for every animation you assign a new object of this type to your view.transform, you will lose the previous transform operation

So, according to this post: How to apply multiple transforms in Swift, you need to concat the transform operation

Example with 2 animation block

This is an example to made a rotation of 180 and returning back to origin after 1 sec:

let view = UIView.init(frame: CGRect.init(origin: self.view.center, size: CGSize.init(width: 100, height: 100)))
view.backgroundColor = UIColor.red
self.view.addSubview(view)

var transform = view.transform
transform = transform.rotated(by: 180)

UIView.animate(withDuration: 2, animations: {
view.transform = transform
}) { (completed) in

transform = CGAffineTransform.identity
UIView.animate(withDuration: 2, delay: 1, options: [], animations: {
view.transform = transform
}, completion: nil)
}

Sample Image

Example of .repeat animation and .autoreverse

The .animate method give you the ability to set some animation options. In particular the structure UIViewAnimationOptions contains:

  1. .repeat, which repeat indefinitely your animation block
  2. .autoreverse, which restore your view to the original status

With this in mind you could do this:

var transform = view.transform.rotated(by: 180)
UIView.animate(withDuration: 2, delay: 0, options: [.repeat, .autoreverse], animations: {
self.myView.transform = transform
})

But you need a delay between the two animations, so you need to do this trick:

Example of recursive animation and a delay of 1 sec

Just create a method inside your ViewController which animate your view. In the last completionHandler, just call the method to create a infinite loop.

Last you need to call the method on viewDidAppear to start the animation.

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

self.animation()
}

func animation() {
var transform = view.transform
transform = transform.rotated(by: 180)

UIView.animate(withDuration: 2, delay: 0, options: [], animations: {

self.myView.transform = transform

}) { bool in
transform = CGAffineTransform.identity

UIView.animate(withDuration: 2, delay: 1, options: [], animations: {

self.myView.transform = transform

}, completion: { bool in
self.animation()
})
}
}


Related Topics



Leave a reply



Submit