Best way to run a series of short animations? - ios

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
})
})

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")
}

Trigger fading label delay from view did load instead of at the end of previous fade

Background
Thanks to the excellent answer here, I've managed to make a series of labels fade in sequentially.
However, the delay referred to in the function in the extension only triggers when the previous fade has completed (I think) so there is at least 1 second between them. Ideally I want to edit the model so that the delay refers to from the time the view loads, not the time the previous one has completed.
i.e. If I make the delay quite small, the second one would start fading in before the first one had finished.
This is the extension :
extension UIView {
func fadeIn(duration: TimeInterval = 1.0, delay: TimeInterval = 0.0, completion: ((Bool)->())? = nil) {
self.alpha = 0.0
UIView.animate(withDuration: duration, delay: delay, options: .curveEaseIn, animations: {
self.alpha = 1.0
}, completion: completion)
}
}
and this is my implementation on a VC with 6 labels :
func animateLabels() {
self.titleOutlet.alpha = 0.0
self.line1Outlet.alpha = 0.0
self.line2Outlet.alpha = 0.0
self.line3Outlet.alpha = 0.0
self.line4Outlet.alpha = 0.0
self.line5Outlet.alpha = 0.0
self.line6Outlet.alpha = 0.0
self.titleOutlet.fadeIn(delay: 0.2, completion: { _ in
self.line1Outlet.fadeIn(delay: 0.4, completion: { _ in
self.line2Outlet.fadeIn(delay: 0.6, completion: { _ in
self.line3Outlet.fadeIn(delay: 0.8, completion: { _ in
self.line4Outlet.fadeIn(delay: 1.0, completion: { _ in
self.line5Outlet.fadeIn(delay: 1.2, completion: { _ in
self.line6Outlet.fadeIn(delay: 1.4, completion: { _ in
})})})})})})})
}
What am I trying to get to?
So what I'm trying to get to is having line2outlet start to fade 0.4 seconds after the load of the page but it is starting only when line1 has finished fading in (which takes 1 second and is delayed by 0.2s).
What have I tried?
I have attempted to write my own delay function after reading up. I tried adding this to the extension :
func delay(interval: TimeInterval, closure: #escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
closure()
}
}
and implementing like this :
self.line1Outlet.fadeIn(delay: 0, completion: { _ in
delay(interval: 1, closure: ()->Void)
but that got loads of errors! I also tried setting minus numbers for the delays but that didn't work either.
Any help much appreciated!
One possible solution that I have used before is to use a loop with an incrementing delay variable feeding into the standard UIView.animate() method. This method does not use completion handlers.
Make an array of the labels:
var labels = [titleOutlet, line1Outlet] // etc...
Use this extension:
extension UIView {
func fadeIn(duration: TimeInterval = 1, delay: TimeInterval = 0) {
self.alpha = 0
UIView.animate(withDuration: duration, delay: delay, options: [], animations: { self.alpha = 1 }, completion: nil)
}
}
Then use it like this:
var delay = 0.2
var duration = 0.5
for label in labels {
label.fadeIn(duration: duration, delay: delay)
delay += 0.2 // Increment delay
}

UIView.transition with delay

I have simultaneous animations going on, and I would like to transition the VC in the middle (it will fade so it will see some of the other animations). However, I can't find documentation how to delay a transition similar to UIView.animateWithDuration.
I want to achieve this...
UIView.transition(withDuration: 0.5, delay: 0.1, options: .transitionCrossDissolve, animations: {})
I can manually add a delay like so.. but was wondering if there's a more elegant way.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {}
Unfortunately no. Using a bunch of UIView transitions/animations can get a little hairy.
In the past, I've resorted to setting up an NSTimer that fires 1/30 seconds and I ended up managing all the start times on my own.
What you can do is series out the animations using the closure.
UIView.animate(withDuration: 1, animations: {
}, completion: {Do UIView transition here})
It seems no way except surrounding with Timer:
Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { _ in
UIView.transition(with: self.myImageView, duration: 0.75,
options: [.transitionCrossDissolve],
animations: {
self.myImageView.image = newImage
})
})

Repeating and reversing an animation multiple times using UIViewPropertyAnimator

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()
}

Animation going too fast. Can't seem to slow down

My animations are going too fast. I'm doing a pattern matching game and I have 4 UIViews with different colors. I want one to blink and then about a second later, have another UIView blink. The views are blinking by my tag identifier which I already set differently for each view (1,2,3,4). It seems like they are all going at the same time. I've already tried adjusting the values for the animateWithDuration function and that doesn't seem to help.
Here is my print output so you can see it's executing in the right order...
gary
3
gary
2
gary
2
gary
3
gary
2
func beginGame() {
var level = 5
for _ in 1...level {
self.randomNumber = Int(arc4random_uniform(4)) + 1
let originalColor:UIColor = self.view.viewWithTag(randomNumber)!.backgroundColor!
UIView.animateWithDuration(1, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
self.view.viewWithTag(self.randomNumber)!.backgroundColor = UIColor.whiteColor()
self.view.viewWithTag(self.randomNumber)!.backgroundColor = originalColor
print("gary")
}, completion: nil)
print(randomNumber)
enemyArray.append(randomNumber)
}
}
//New Code
func beginGame(){
var level = 5
for _ in 1...level {
self.randomNumber = Int(arc4random_uniform(4)) + 1
let originalColor:UIColor = self.view.viewWithTag(randomNumber)!.backgroundColor!
UIView.animateWithDuration(1, delay: 4, options: UIViewAnimationOptions.CurveEaseOut, animations: {
self.view.viewWithTag(self.randomNumber)!.backgroundColor = UIColor.whiteColor()
self.view.viewWithTag(self.randomNumber)!.backgroundColor = originalColor
print("gary")
}, completion: nil)
print(randomNumber)
enemyArray.append(randomNumber)
}
}
Listen to what people are telling you. If you use a for loop and create multiple animations that all have a delay value of 0, they will all run at the same time. Don't do that.
Instead, use code more like this:
let pauseBetweenAnimations = 1.0
for step in 1...level {
self.randomNumber = Int(arc4random_uniform(4)) + 1
let originalColor:UIColor =
self.view.viewWithTag(randomNumber)!.backgroundColor!
UIView.animateWithDuration(1, delay: (step - 1) * pauseBetweenAnimations,
options: UIViewAnimationOptions.CurveEaseOut,
animations:
//the rest of your code goes here...
That will make each subsequent animation start 1 second after the previous one. If 1 second is too long, change the value of pauseBetweenAnimations.

Resources