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")
}
Related
I basically have a song that a cartoon needs to dance to.
Is it better to have :
Version A: one full song and dispatch a bunch of queues:
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
//Have the figure dance move 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
//Have the figure dance move 2
}
Or segment the song and actions:
func dancing(){
timeElapsed += 1
if timeElapsed == 1 {
\\figure does move 1
self.playSound()
} else if timeElapsed == 2 {
\\figure does move 2
self.playSound2()
}
As a summary:
Version a: Dispatch multiple queues at the same time
Version b: Segment the queues, but that would mean my project would have 10+ media files
Is there anyway to test this? Or any alternative methods? I've poked around and seen things like concurrent/sync queues, but don't know much on how to use them in practice.
If you don’t want too much nesting, you can use UIViewPropertyAnimator. For Example:
let animation1 = UIViewPropertyAnimator(duration: 0.5, curve: .linear) {
// animation code
}
let animation2 = UIViewPropertyAnimator(duration: 1, curve: .linear) {
// animation code
}
animation1.addCompletion { _ in
animation2.startAnimation()
}
animation1.startAnimation()
Another option you might want to look at is UIView key frame animations.
You need to figure out your relative start times and durations, but it's a bit nicer than using dispatch queues.
More info: link
A simple demonstration:
UIView.animateKeyframes(withDuration: 4.0, delay: 0.0, options: [], animations: {
// Animation 1 that starts immediately and runs for 2 seconds
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.5) {
// Perform animation
}
// Animation 2 that starts after 2 seconds and runs for 2 seconds
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
// Perform animation
}
})
I will go with Version A: one full song and Set the Animation Hierarchy
Better to use UIView Animation with completion block
UIView.animate(withDuration: 0.5, animations: {
//animation 1
}, completion: { (value: Bool) in
UIView.animate(withDuration: 1.0, animations: {
//animation 2
})
})
I am trying to have my animation ease the screen from black to white to black again and repeat that a certain amount of times. Currently with the code I have the animation eases from black to white then jumps back to black. Is there anyway to run an animation in reverse or add an animation that runs after the first animation is completed?
override func viewDidAppear(_ animated: Bool) {
let viewColorAnimator: UIViewPropertyAnimator = UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 4.0,
delay: 0.0,
options: [.curveEaseInOut],
animations: {
UIView.setAnimationRepeatCount(3);
self.lightView.backgroundColor = .white
})
viewColorAnimator.startAnimation()
}
I tried adding this block of code to the project but the outcome was the same:
viewColorAnimator.addCompletion {_ in
let secondAnimator = UIViewPropertyAnimator(duration: 4.0, curve: .linear) {
self.lightView.backgroundColor = .black
}
secondAnimator.startAnimation()
}
EDIT: I've found out it is because of the setAnimationRepeatCount because the last of the iterations works properly. How do I run the animation multiple times without the repeat count?
Documentation says that the options related to direction are ignored. you can check the image attached here.
To achieve reverse animation:
Create an animator property.
private var animator: UIViewPropertyAnimator = {
let propertyAnimator = UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 4.0,
delay: 0.0,
options: [.curveEaseInOut, .autoreverse],
animations: {
UIView.setAnimationRepeatCount(3)
UIView.setAnimationRepeatAutoreverses(true)
self.lightView.backgroundColor = .white
})
return propertyAnimator
}()
P.S. we need to mention the .autoreverse in the options. Otherwise UIView.setAnimationRepeatAutoreverses(true) won't work.
There's an easy way to run animations. And for this method: if you want the animation to repeat after completing, you can add the .autoreverse, and the .repeat option. Like this:
UIView.animate(withDuration: TimeInterval(randomTime), delay: 0, options: [.repeat,.autoreverse], animations: {
//Animation code here
}, completion: {(bool)
//Do something after completion
})
You can put the codes you want to execute after the animation in the completion block.
And, you can use a Timer as a way to run the animation for certain times.
Xcode 9.4.1 (Swift 4.1.2) and Xcode 10 beta 3 (Swift 4.2):
Here's the way to do it using UIViewPropertyAnimator -- basically, we just add .repeat and .autoreverse to the options. You were 99% of the way there:
override func viewDidAppear(_ animated: Bool) {
let viewColorAnimator: UIViewPropertyAnimator = UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 4.0,
delay: 0.0,
options: [.curveEaseInOut, .repeat, .autoreverse],
animations: {
UIView.setAnimationRepeatCount(3)
self.lightView.backgroundColor = .white
})
viewColorAnimator.startAnimation()
}
I would like to change the color of a UIView in a while loop. I am calling start() from the main thread.
Example :
func start() {
let item = Percolation(n: self.tileSideCount)
while (!item.percolates()) {
let v = Int(arc4random_uniform(UInt32(self.tileSideCount))) + 1
let j = Int(arc4random_uniform(UInt32(self.tileSideCount))) + 1
UIView.animate(withDuration: 0.3, delay: 0.0, options: [], animations: {
self.container?.viewWithTag(v)?.backgroundColor = UIColor.white
self.container?.viewWithTag(j)?.backgroundColor = UIColor.white
}, completion: { (finished: Bool) in
})
if (!item.isOpen(i: v, j: j)) {
item.open(i: v, j: j);
}
}
}
If you want the colors to change from one to the next in sequence, you need to submit your animation calls with delay and duration values so the previous animation is finished as the next one starts.
Say the duration of each animation is 1 second. Make the first one start with a delay of 0, the second animation start with a delay of 1 second, etc.
(Same applies for other durations. if your duration is .3 second, have each subsequent animation start an additional 0.3 seconds later than the previous one.)
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.
I have an Array words containing ["alice", "bob", "charles"] and a UILabel label. I want label to repeatedly fade in and out, with a different word from words each time. If I put the text-changing code inside the animations block, it doesn’t execute, even though the fading works as expected (the completion block is run only when something stops the repetition):
label.alpha = 0
UIView.animateWithDuration(4, delay: 0, options: .Autoreverse | .Repeat, animations: {
self.label.text = nextWord()
self.label.alpha = 1
}, completion: {_ in
NSLog("Completed animation")
})
What’s a good way to fix this? Thanks.
surely not the most elegant but working solution:
#IBOutlet weak var label: UILabel!
let words = ["Is", "this", "what", "you", "want?"]
var currentIndex = -1
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
showNextWord()
}
func showNextWord() {
if currentIndex == words.count - 1 {
currentIndex = -1
}
UIView.animateWithDuration(1, delay: 1, options: UIViewAnimationOptions(0), animations: { () -> Void in
self.label.alpha = 0.0
}) { (_) -> Void in
self.label.text = self.words[++self.currentIndex]
UIView.animateWithDuration(1, animations: { () -> Void in
self.label.alpha = 1.0
}, completion: { (_) -> Void in
self.showNextWord()
})
}
}
You could construct this as a keyframe animation. That way, you can chain three animations together (one for each word) and repeat that entire chain.
Alternatively (this is what I would probably do), put one animation into a method of its own, and in the completion block, add a short delay and call the method - thus creating a perpetual loop. The loop creates the repetition, but each animation is just one animation, so now you can progress through the array on successive calls. So the structure (pseudo-code) would look like this:
func animate() {
let opts = UIViewAnimationOptions.Autoreverse
UIView.animateWithDuration(1, delay: 0, options: opts, animations: {
// animate one fade in and out
}, completion: {
_ in
delay(0.1) {
// set the next text
self.animate() // recurse after delay
}
})
}