How to calculate relativeDuration in animateKeyframes - swift

I'm trying to understand how to use UIView.animateKeyframes however I cannot for the life of me understand how to calculate the timings / durations.
I am trying to achieve the following:
I expect this entire animation to last 10 seconds, made up of...
first addKeyframe runs at 01 seconds and takes 1 second
second addKeyframe runs at 02 seconds and takes 1 second
third addKeyframe runs at 05 seconds and takes 2 seconds
fourth addKeyframe runs at 07 seconds and takes 1 second
fourth addKeyframe runs at 09 seconds and takes 1 second
fileprivate func animateWelcomeText() -> Void {
let duration: TimeInterval = 10
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 1.0, relativeDuration: 1.0, animations: {
self.introTextLabel.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 2.0, relativeDuration: 1.0, animations: {
self.introTextLabelTwo.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 5.0, relativeDuration: 2.0, animations: {
self.introTextLabel.alpha = 0
self.introTextLabelTwo.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 7.0, relativeDuration: 1.0, animations: {
self.introTextLabelThree.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 9.0, relativeDuration: 1.0, animations: {
self.introTextLabelFour.alpha = 1
})
}) { (_) in
print("Complete")
}
}
Nothing happens and then the animation completes and the last 2 items suddenly appear.

It's just math. Just divide all the values by your duration to get the relative values.
fileprivate func animateWelcomeText() -> Void {
let duration: TimeInterval = 10
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 1/duration, relativeDuration: 1/duration, animations: {
self.introTextLabel.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 2/duration, relativeDuration: 1/duration, animations: {
self.introTextLabelTwo.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 5/duration, relativeDuration: 2/duration, animations: {
self.introTextLabel.alpha = 0
self.introTextLabelTwo.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 7/duration, relativeDuration: 1/duration, animations: {
self.introTextLabelThree.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 9/duration, relativeDuration: 1/duration, animations: {
self.introTextLabelFour.alpha = 1
})
}) { (_) in
print("Complete")
}
}
You have your values out of x (10). The method needs it out of 1.

Related

AnimateKeyframes animate for the UILabel's value

I ran the code below and the label was moved perfectly. But I want to increase the value of the label at the same time with each move. I have tried many ways but it doesn't work.
UIView.animateKeyframes(withDuration: 6, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: (0 / 6), relativeDuration: (2 / 6), animations: {
self.label.transform = label.transform.translatedBy(x: 0, y: 50) //OK
//I want the value of the label to be increased from 0 to 5 (exactly equal to the duration of this key frame)
})
UIView.addKeyframe(withRelativeStartTime: (2 / 6), relativeDuration: (2 / 6), animations: {
self.label.transform = label.transform.translatedBy(x: 50, y: 0) //OK
//Then I want the value of the label to decrease from 5 to 0
})
}, completion: nil)
Can you help me? Thanks in advance!
You will not be able to do it within the animation block, you can use Timer and change label's text property with it.
Example, in your case I would do it like this:
var tick = 0
let duration: TimeInterval = 6
let countTo = 5
Timer.scheduledTimer(withTimeInterval: duration / (2 * Double(countTo)), repeats: true) { timer in
tick += 1
let mod = tick % countTo
if tick >= countTo, tick < countTo * 2 {
label.text = String(countTo - mod)
} else {
label.text = String(mod)
}
if tick >= countTo * 2 {
timer.invalidate()
}
}
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
label.transform = label.transform.translatedBy(x: 0, y: 50)
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
label.transform = label.transform.translatedBy(x: 50, y: 0)
})
}, completion: nil)
This way you can set duration you need and to what number you want to count to and back to 0 within the duration.

How can I repeat chained animation in Swift?

I would like to animate the whole chain for forever.
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseOut, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 9, y: 9)
}) { (_) in
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseIn, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
}, completion: nil)
}
You can wrap the two animations within a keyframe animation. Then you can .repeat that:
UIView.animateKeyframes(withDuration: 4.4, delay: 0, options: .repeat, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 2.0/4.4) {
self.circleImageView.transform = .init(scaleX: 9, y: 9)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 2.0/4.4) {
self.circleImageView.transform = .init(scaleX: 0.01, y: 0.01)
}
})
Wrap it up inside a method:
func repeatingAnimation() {
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseOut, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 9, y: 9)
}) { (_) in
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseIn, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
}, completion: { [weak self] _ in
self?.repeatingAnimation()
})
}
}

Swift only call recursive function X times

I have a function I am using to animate 4 UILabels. The animation is chained and I am trying to reduce repeated code by creating a recursive function that accepts the label as an argument, on completion calls itself with the next label.
fileprivate func handleAnimations(firstLabel: UILabel, secondLabel: UILabel) -> Void {
UIView.animate(withDuration: 1, delay: 2, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
firstLabel.alpha = 1
}) { (_) in
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
secondLabel.alpha = 1
}) { (_) in
UIView.animate(withDuration: 0.5, delay: 3, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
firstLabel.alpha = 0
secondLabel.alpha = 0
}, completion: { (_) in
self.handleAnimations(firstLabel: self.introTextLabelThree, secondLabel: self.introTextLabelFour)
})
}
}
}
Is this the best way to achieve this? If so, how I prevent the final complete block calling itself over and over with the final 2 labels?
The effect I am trying to achieve is:
labelOne - fades in
labelTwo - fades in
labelOne and labelTwo - fade out
labelThree - fades in
labelFour - fades in
labelThree and labelFour - fade out
After labelThree and labelFour I would be looking to implement another function call.
Do not do this. Please use animateKeyframes. This will allow you to properly chain animations without the dreaded pyramid of doom.
UIView.animateKeyframes(withDuration: 5.0, delay: 0, options: [.calculationModeCubic], animations: {
// Add animations
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0/6.0, animations: {
self.introTextLabel.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 1.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelTwo.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 2.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabel.alpha = 0
self.introTextLabelTwo.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 3.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelThree.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 4.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelFour.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 5.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelThree.alpha = 0
self.introTextLabelFour.alpha = 0
})
}, completion:{ _ in
// fire off whatever other method you want here
})

Swift, playing multiple animations consecutively from an array?

Sorry, I am new to swift. I can't get each animation to play consecutively and not all at once. I have tried using sleep(), but that then doesn't seem to allow the animation to play. This is how I find which animation to play.
for number in sequence {
switch number {
case 1:
print("blue")
animateB()
case 2:
print("green")
animateG()
case 3:
print("magenta")
animateM()
case 4:
print("orange")
animateO()
case 5:
print("yellow")
animateY()
case 6:
print("red")
animateR()
case 7:
print("purple")
animateP()
case 8:
print("cyan")
animateC()
default:
print("error")
}
}
And this is one of the functions i am using to animate. I realize this is probably very inefficient too, but wasn't sure how to make the function better.
private func animateB(){
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.toValue = 1.3
animation.duration = 0.5
animation.autoreverses = true
self.pulsatingB.add(animation, forKey: "pulsing")
}
Any help would be great thanks. :)
You can use CATransaction to chain the CAAnimations:
class ViewController: UIViewController {
// The animations, to be applied in order
var animationQueue = [() -> Void]()
#IBAction func animate(_ sender: Any) {
animationQueue.removeAll()
// Build the animation queue
for number in sequence {
switch number {
case 1:
print("blue")
animationQueue.append(animateB)
case 2:
print("green")
animationQueue.append(animateG)
// ....
default:
break
}
}
// Start the animation
applyNextAnimation()
}
func applyNextAnimation() {
guard !animationQueue.isEmpty else { return }
let animation = animationQueue.removeFirst()
// When an animation completes, call this function again to apply the next animation
CATransaction.begin()
CATransaction.setCompletionBlock({ self.applyNextAnimation() })
animation()
CATransaction.commit()
}
}
For a sequence of animations, a block based keyframe animation can often do the job, too, e.g.:
UIView.animateKeyframes(withDuration: 4.0, delay: 0, options: .repeat, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25, animations: {
self.subview.transform = .init(scaleX: 0.5, y: 0.5)
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25, animations: {
self.subview.transform = .init(scaleX: 1.3, y: 1.3)
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.25, animations: {
self.subview.transform = .init(scaleX: 0.75, y: 0.75)
})
UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25, animations: {
self.subview.transform = .identity
})
}, completion: nil)
Or, if you have an array of functions:
let animations = [animateA, animateB, animateC, animateD]
UIView.animateKeyframes(withDuration: 4.0, delay: 0, options: .repeat, animations: {
for (index, animation) in animations.enumerated() {
UIView.addKeyframe(withRelativeStartTime: Double(index) / Double(animations.count), relativeDuration: 1 / Double(animations.count), animations: {
animation()
})
}
}, completion: nil)
Where,
func animateA() {
subview.transform = .init(scaleX: 0.5, y: 0.5)
}
func animateB() {
subview.transform = .init(scaleX: 1.3, y: 1.3)
}
...

Whats the Swift animate WithDuration syntax?

I'm porting an older app over to Xcode 7 beta and I'm getting an error on my animations:
Cannot invoke 'animateWithDuration' with an argument list of type
'(Double, delay: Double, options: nil, animations: () -> _,
completion: nil)'
Here's the code:
UIView.animateWithDuration(0.5, delay: 0.3, options: nil, animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
This works in Xcode 6 so I'm assuming this is an update in Swift. So my question is:
What's the Swift 3 syntax for animateWithDuration?
Swift 3/4 Syntax
Here's an update with the Swift 3 Syntax:
UIView.animate(withDuration: 0.5, delay: 0.3, options: [.repeat, .curveEaseOut, .autoreverse], animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
If you need to add a completion handler just add a closure like so:
UIView.animate(withDuration: 0.5, delay: 0.3, options: [.repeat, .curveEaseOut, .autoreverse], animations: {
// animation stuff
}, completion: { _ in
// do stuff once animation is complete
})
Old Answer:
It turns out it's a very simple fix, just change options: nil to options: [].
Swift 2.2 Syntax:
UIView.animateWithDuration(0.5, delay: 0.3, options: [], animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
What changed?
Swift 2 got rid of the C-Style comma-delimited list of options in favor of option sets (see: OptionSetType). In my original question, I passed in nil for my options, which was valid prior to Swift 2. With the updated syntax, we now see an empty option list as an empty set: [].
An example of animateWithDuration with some options would be this:
UIView.animateWithDuration(0.5, delay: 0.3, options: [.Repeat, .CurveEaseOut, .Autoreverse], animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
Swift 3, 4, 5
UIView.animate(withDuration: 1.5, delay: 0.05 * Double(index), usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: [], animations: {
cell.transform = CGAffineTransform(translationX: 0, y: 0)
}, completion: nil)
Swift 3 Syntax with completion block
UIView.animate(withDuration: 3.0 , delay: 0.25, options: .curveEaseOut, animations: {
// animation
}, completion: { _ in
// completion
})
Swift 2
UIView.animateWithDuration(1.0, delay: 0.1, options: [.Repeat, .CurveEaseOut, .Autoreverse], animations: {
// animation
}, completion: { finished in
// completion
})
Swift 3, 4, 5
UIView.animate(withDuration: 1.0, delay: 0.1, options: [.repeat, .curveEaseOut, .autoreverse], animations: {
// animation
}, completion: { finished in
// completion
})