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)
}
Example of .repeat animation and .autoreverse
The .animate method give you the ability to set some animation options. In particular the structure UIViewAnimationOptions contains:
- .repeat, which repeat indefinitely your animation block
- .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
Getting Reference to a Dictionary Value
How to Create Generic Closures in Swift
Problems with Unified Logging, Staticstring, Customstringconvertible and Description
Swiftui - How to Change The Button's Image on Click
Uitableviewcell Subclass Wrong Image in Cell or Old Image Bug
Cannot Use Mutating Member ... Because Append
Swift Casting Generic to Optional with a Nil Value Causes Fatalerror
Uibutton Borders Function Only Gives Back White Borders
Aws Cognito Credentialsprovider.Login Always Shows Nil (Swift)
Problem with Frameworks in Command Line Tool
Proper Way of Editing a Cocoapod Library
How to Increment a Swift Int Enumeration
Decoding Different Type with and Without Array
Rlmexception', Reason: 'Attempting to Modify Object Outside of a Write Transaction
[Core Data]: Threw While Encoding a Value. with Userinfo of (Null)
User Already Authenticated in App and Firebase Realtime Database Returns Failed: Permission_Denied