UIView keyframe animation timing not as expected - ios

I have some animations (testable in a playground):
import UIKit
import XCPlayground
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution
let view = UIView()
view.frame = .init(x: 0, y: 0, width: 150, height: 150)
view.backgroundColor = .orange
let hand = UIView()
view.addSubview(hand)
hand.frame = .init(x: 0, y: 0, width: 10, height: 10)
hand.center = view.center
hand.backgroundColor = .green
PlaygroundPage.current.liveView = view
let fadeDuration: TimeInterval = 0.4
let translationDuration: TimeInterval = 1
let resetDuration: TimeInterval = 0.25
let duration: TimeInterval =
7 * fadeDuration +
4 * translationDuration +
3 * resetDuration
var currentTime: TimeInterval = 0.0
func addKey(_ animations: #escaping () -> Void, animationTime: TimeInterval) {
UIView.addKeyframe(withRelativeStartTime: currentTime / duration, relativeDuration: animationTime / duration, animations: animations)
currentTime += animationTime
}
func fadeIn() {
addKey({
hand.alpha = 1
}, animationTime: fadeDuration)
}
func fadeOut() {
addKey({
hand.alpha = 0
}, animationTime: fadeDuration)
}
func translate(_ direction: (CGFloat) -> CGFloat) {
let x = direction(50)
addKey({
hand.transform = .init(translationX: x, y: 0)
}, animationTime: translationDuration)
}
func reset() {
addKey({
hand.transform = .identity
}, animationTime: 0)
currentTime += resetDuration
}
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeLinear) {
translate(-)
fadeOut()
reset()
fadeIn()
translate(-)
fadeOut()
reset()
fadeIn()
translate(+)
fadeOut()
reset()
fadeIn()
translate(+)
fadeOut()
} completion: { _ in
PlaygroundPage.current.finishExecution()
}
I expect every single translate() animation to run at the same speed, but for some reason the first and last ones are especially slow, despite using .calculationModeLinear
How to make it so that translationDuration / duration is constant time ?

calculationModeLinear is different from curveLinear. It sounds like you want the latter. You are using curveEaseInOut by default so of course the movement slows near the overall start and finish.
Some hanky panky is needed to overcome Swift’s strict typing. Sample code:
let animationOptions: UIView.AnimationOptions = .curveLinear
let keyframeAnimationOptions = UIView.KeyframeAnimationOptions(rawValue: animationOptions.rawValue)
UIView.animateKeyframes(withDuration: duration, delay: 0.1, options: keyframeAnimationOptions) {

Related

iOS Spin UIImageView with deceleration motion at random position

I trying to create a spin wheel which rotate on tap and it rotates for certain period of time and stops at some random circular angle.
import UIKit
class MasterViewController: UIViewController {
lazy var imageView: UIImageView = {
let bounds = self.view.bounds
let v = UIImageView()
v.backgroundColor = .red
v.frame = CGRect(x: 0, y: 0,
width: bounds.width - 100,
height: bounds.width - 100)
v.center = self.view.center
return v
}()
lazy var subView: UIView = {
let v = UIView()
v.backgroundColor = .black
v.frame = CGRect(x: 0, y: 0,
width: 30,
height: 30)
return v
}()
var dateTouchesEnded: Date?
var dateTouchesStarted: Date?
var deltaAngle = CGFloat(0)
var startTransform: CGAffineTransform?
var touchPointStart: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
self.imageView.addSubview(self.subView)
self.view.addSubview(self.imageView)
self.imageView.isUserInteractionEnabled = true
self.setupGesture()
}
private func setupGesture() {
let gesture = UITapGestureRecognizer(target: self,
action: #selector(handleGesture(_:)))
self.view.addGestureRecognizer(gesture)
}
#objc
func handleGesture(_ sender: UITapGestureRecognizer) {
var timeDelta = 1.0
let _ = Timer.scheduledTimer(withTimeInterval: 0.2,
repeats: true) { (timer) in
if timeDelta < 0 {
timer.invalidate()
} else {
timeDelta -= 0.03
self.spinImage(timeDelta: timeDelta)
}
}
}
func spinImage(timeDelta: Double) {
print("TIME DELTA:", timeDelta)
let direction: Double = 1
let rotation: Double = 1
UIView.animate(withDuration: 5,
delay: 0,
options: .curveEaseOut,
animations: {
let transform = self.imageView.transform.rotated(
by: CGFloat(direction) * CGFloat(rotation) * CGFloat(Double.pi)
)
self.imageView.transform = transform
}, completion: nil)
}
}
I tried via above code, but always stops on initial position
i.e. the initial and final transform is same.
I want it to be random at every time.
One thing you can do is make a counter with a random number within a specified range. When the time fires, call spinImage then decrement the counter. Keep doing that until the counter reaches zero. This random number will give you some variability so that you don't wind up with the same result every time.
#objc func handleGesture(_ sender: UITapGestureRecognizer) {
var counter = Int.random(in: 30...33)
let _ = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { (timer) in
if counter < 0 {
timer.invalidate()
} else {
counter -= 1
self.spinImage()
}
}
}
In spinImage, instead of rotating by CGFloat.pi, rotate by by CGFloat.pi / 2 so that you have four possible outcomes instead of two.
func spinImage() {
UIView.animate(withDuration: 2.5,
delay: 0,
options: .curveEaseOut,
animations: {
let transform = self.imageView.transform.rotated(
by: CGFloat.pi / 2
)
self.imageView.transform = transform
}, completion: nil)
}
You may want to mess around with the counter values, the timer interval, and the animation duration to get the effect that you want. The values I chose here are somewhat arbitrary.

Swift UITabBarController hide with animation

I'm trying to add animation to my tabBarController when hidden. Im able to accomplish this effect with the navigationBarController by using self.navigationController?.isNavigationBarHidden = true. I'm able to hide the tabBar by using self.tabBarController?.tabBar.isHidden = true but i do not get the animation how can I do this thank you in advance.
You could change the tab bar's frame inside an animation, so something like:
func hideTabBar() {
var frame = self.tabBarController?.tabBar.frame
frame?.origin.y = self.view.frame.size.height + (frame?.size.height)!
UIView.animate(withDuration: 0.5, animations: {
self.tabBarController?.tabBar.frame = frame!
})
}
func showTabBar() {
var frame = self.tabBarController?.tabBar.frame
frame?.origin.y = self.view.frame.size.height - (frame?.size.height)!
UIView.animate(withDuration: 0.5, animations: {
self.tabBarController?.tabBar.frame = frame!
})
}
Which sets the tab bar just below the visible screen, so that it slides up/down from the bottom.
I've developed a util extension for UIViewController
Swift 4 compatible:
extension UIViewController {
func setTabBarHidden(_ hidden: Bool, animated: Bool = true, duration: TimeInterval = 0.3) {
if animated {
if let frame = self.tabBarController?.tabBar.frame {
let factor: CGFloat = hidden ? 1 : -1
let y = frame.origin.y + (frame.size.height * factor)
UIView.animate(withDuration: duration, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: y, width: frame.width, height: frame.height)
})
return
}
}
self.tabBarController?.tabBar.isHidden = hidden
}
}
Improvement of the response of #Luca Davanzo. If the bar is already hidden, it will continue hiding it and moving it lower. Also get rid of the return, so the state of the tabbar.hidden changes when the animation happens.
So I added a check:
extension UIViewController {
func setTabBarHidden(_ hidden: Bool, animated: Bool = true, duration: TimeInterval = 0.5) {
if self.tabBarController?.tabBar.isHidden != hidden{
if animated {
//Show the tabbar before the animation in case it has to appear
if (self.tabBarController?.tabBar.isHidden)!{
self.tabBarController?.tabBar.isHidden = hidden
}
if let frame = self.tabBarController?.tabBar.frame {
let factor: CGFloat = hidden ? 1 : -1
let y = frame.origin.y + (frame.size.height * factor)
UIView.animate(withDuration: duration, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: y, width: frame.width, height: frame.height)
}) { (bool) in
//hide the tabbar after the animation in case ti has to be hidden
if (!(self.tabBarController?.tabBar.isHidden)!){
self.tabBarController?.tabBar.isHidden = hidden
}
}
}
}
}
}
}
In case if you need to toggle it from hide to visible and vice versa:
func toggleTabbar() {
guard var frame = tabBarController?.tabBar.frame else { return }
let hidden = frame.origin.y == view.frame.size.height
frame.origin.y = hidden ? view.frame.size.height - frame.size.height : view.frame.size.height
UIView.animate(withDuration: 0.3) {
self.tabBarController?.tabBar.frame = frame
}
}
Swift 4 solution:
tabBarController?.tabBar.isHidden = true
UIView.transition(with: tabBarController!.view, duration: 0.35, options: .transitionCrossDissolve, animations: nil)
Here is a simple extension :
func setTabBar(hidden:Bool) {
guard let frame = self.tabBarController?.tabBar.frame else {return }
if hidden {
UIView.animate(withDuration: 0.3, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: frame.origin.y + frame.height, width: frame.width, height: frame.height)
})
}else {
UIView.animate(withDuration: 0.3, animations: {
self.tabBarController?.tabBar.frame = UITabBarController().tabBar.frame
})
}
}
So I've been playing around for 3 days with this now, finding out that the one that worked for me in my code was Adriana's post from 14th Sept 2018. But I was not sure how to use the coding once copied into my Project. So, after much experimenting I found that the way I could use this func was to put the following into into the respective swipe actions.
setTabBarHidden(false)
setTabBarHidden(true)
My next step is to try to get the swipe actions working while using UIScrollView in the same UIView at the same time.
You have to add UIView transitionWithView class func
Swift 2
func hideTabBarWithAnimation() -> () {
UIView.transitionWithView(tableView, duration: 1.0, options: .TransitionCrossDissolve, animations: { () -> Void in
self.tabBarController?.tabBar.hidden = true
}, completion: nil)
}
Swift 3, 4, 5
func hideTabBarWithAnimation() -> () {
UIView.transition(with: tableView, duration: 1.0, options: .transitionCrossDissolve, animations: { () -> Void in
self.tabBarController?.tabBar.isHidden = true
}, completion: nil)
}

Want to make rotating button which I can press while animating

I've made a rotating button. But while button rotates I can't press it. So The question is how I can make it "pressable"?
My animation code is:
var max: Bool = true
func startAnimation() {
max = !max
let duration: Double = 1
let fullCircle = 2 * M_PI
let upOrDown = (max ? CGFloat(-1 / 16 * fullCircle) : CGFloat(1 / 16 * fullCircle))
let scale: (CGFloat, CGFloat) = (max ? (1.0, 1.0) : (1.3, 1.3))
UIView.animateWithDuration(duration, animations: { () -> Void in
let rotationAnimation = CGAffineTransformMakeRotation(upOrDown)
let scaleAnimation = CGAffineTransformMakeScale(scale)
self.startButton.transform = CGAffineTransformConcat(rotationAnimation, scaleAnimation)
}) { (finished) -> Void in
self.startAnimation()
}
So if I press button there is no any effect that it has happened. No text inside button text animation - nothing! But button keep rotating and don't perform segue to other scene. But if it is no animation I can do segue.
Use the animation option UIViewAnimationOptionAllowUserInteraction.
UIView.animateWithDuration(duration, delay:, options: UIViewAnimationOptions.AllowUserInteraction, animations: , completion: )
The final code works fine and looks like:
var max: Bool = true
func startAnimation() {
max = !max
let duration: Double = 1
let fullCircle = 2 * M_PI
let upOrDown = (max ? CGFloat(-1 / 16 * fullCircle) : CGFloat(1 / 16 * fullCircle))
let scale: (CGFloat, CGFloat) = (max ? (1.0, 1.0) : (1.3, 1.3))
UIView.animateWithDuration(duration, delay: 0, options: UIViewAnimationOptions.AllowUserInteraction, animations: { () -> Void in
let rotationAnimation = CGAffineTransformMakeRotation(upOrDown)
let scaleAnimation = CGAffineTransformMakeScale(scale)
self.startButton.transform = CGAffineTransformConcat(rotationAnimation, scaleAnimation)
}) { (finished) -> Void in
self.startAnimation()
}
If you are using UIViewPropertyAnimator the following might also help you
let animator = UIViewPropertyAnimator(duration: 2, curve: .easeInOut) {
// animation code
}
animator.isUserInteractionEnabled = true
animator.startAnimation()

Rotate a view for 360 degrees indefinitely in Swift?

I want to rotate an image view for 360 degrees indefinitely.
UIView.animate(withDuration: 2, delay: 0, options: [.repeat], animations: {
self.view.transform = CGAffineTransform(rotationAngle: 6.28318530717959)
}, completion: nil)
How can I do it?
UPDATE Swift 5.x
// duration will helps to control rotation speed
private func rotateView(targetView: UIView, duration: Double = 5) {
UIView.animate(withDuration: duration, delay: 0.0, options: .curveLinear, animations: {
targetView.transform = targetView.transform.rotated(by: .pi)
}) { finished in
self.rotateView(targetView: targetView, duration: duration)
}
}
Swift 2.x way to rotate UIView indefinitely, compiled from earlier answers:
// Rotate <targetView> indefinitely
private func rotateView(targetView: UIView, duration: Double = 1.0) {
UIView.animateWithDuration(duration, delay: 0.0, options: .CurveLinear, animations: {
targetView.transform = CGAffineTransformRotate(targetView.transform, CGFloat(M_PI))
}) { finished in
self.rotateView(targetView, duration: duration)
}
}
UPDATE Swift 3.x
// Rotate <targetView> indefinitely
private func rotateView(targetView: UIView, duration: Double = 1.0) {
UIView.animate(withDuration: duration, delay: 0.0, options: .curveLinear, animations: {
targetView.transform = targetView.transform.rotated(by: CGFloat(M_PI))
}) { finished in
self.rotateView(targetView: targetView, duration: duration)
}
}
Swift 3.0
let imgViewRing = UIImageView(image: UIImage(named: "apple"))
imgViewRing.frame = CGRect(x: 0, y: 0, width: UIImage(named: "apple")!.size.width, height: UIImage(named: "apple")!.size.height)
imgViewRing.center = CGPoint(x: self.view.frame.size.width/2.0, y: self.view.frame.size.height/2.0)
rotateAnimation(imageView: imgViewRing)
self.view.addSubview(imgViewRing)
This is the animation logic
func rotateAnimation(imageView:UIImageView,duration: CFTimeInterval = 2.0) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(.pi * 2.0)
rotateAnimation.duration = duration
rotateAnimation.repeatCount = .greatestFiniteMagnitude
imageView.layer.add(rotateAnimation, forKey: nil)
}
You can check output in this link
Use this extension to rotate UIImageView 360 degrees.
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 1.0, completionDelegate: AnyObject? = nil) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI)
rotateAnimation.duration = duration
if let delegate: CAAnimationDelegate = completionDelegate as! CAAnimationDelegate? {
rotateAnimation.delegate = delegate
}
self.layer.addAnimation(rotateAnimation, forKey: nil)
}
}
Than to rotate UIImageView simply use this method
self.YOUR_SUBVIEW.rotate360Degrees()
This one work for me in Swift 2.2:
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = 360 * CGFloat(M_PI/180)
let innerAnimationDuration : CGFloat = 1.0
rotationAnimation.duration = Double(innerAnimationDuration)
rotationAnimation.repeatCount = HUGE
self.imageView.addAnimation(rotationAnimation, forKey: "rotateInner")
I would stick it in a function like rotateImage() and in the completion code just call rotateImage() again. I think you should use M_PI (or the swift equivalent) for the rotation amount, though.
Updated for Swift 3:
I made an extension to UIView and included the following function within:
func rotate(fromValue: CGFloat, toValue: CGFloat, duration: CFTimeInterval = 1.0, completionDelegate: Any? = nil) {
let rotateAnimation = CABasicAnimation(keyPath: >"transform.rotation")
rotateAnimation.fromValue = fromValue
rotateAnimation.toValue = toValue
rotateAnimation.duration = duration
if let delegate: Any = completionDelegate {
rotateAnimation.delegate = delegate as? CAAnimationDelegate
}
self.layer.add(rotateAnimation, forKey: nil)
}
You can then call the function via (on a UIView I made into a button for example) :
monitorButton.rotate(fromValue: 0.0, toValue: CGFloat(M_PI * 2), completionDelegate: self)
Hope this helps!
I think what you really want here is to use a CADisplayLink. Reason being that this would be indefinitely smooth versus using completion blocks which may cause slight hiccups and are not as easily cancelable. See the following solution:
var displayLink : CADisplayLink?
var targetView = UIView()
func beginRotation () {
// Setup display link
self.displayLink = CADisplayLink(target: self, selector: #selector(onFrameInterval(displayLink:)))
self.displayLink?.preferredFramesPerSecond = 60
self.displayLink?.add(to: .current, forMode: RunLoop.Mode.default)
}
func stopRotation () {
// Invalidate display link
self.displayLink?.invalidate()
self.displayLink = nil
}
// Called everytime the display is refreshed
#objc func onFrameInterval (displayLink: CADisplayLink) {
// Get frames per second
let framesPerSecond = Double(displayLink.preferredFramesPerSecond)
// Based on fps, calculate how much target view should spin each interval
let rotationsPerSecond = Double(3)
let anglePerSecond = rotationsPerSecond * (2 * Double.pi)
let anglePerInterval = CGFloat(anglePerSecond / framesPerSecond)
// Rotate target view to match the current angle of the interval
self.targetView.layer.transform = CATransform3DRotate(self.targetView.layer.transform, anglePerInterval, 0, 0, 1)
}
Avoiding the completion closure with recursive calls!
Bit late to this party, but using UIView keyFrame animation & varying stages of rotation for each keyFrame, plus setting the animation curve works nicely. Here's an UIView class function -
class func rotate360(_ view: UIView, duration: TimeInterval, repeating: Bool = true) {
let transform1 = CGAffineTransform(rotationAngle: .pi * 0.75)
let transform2 = CGAffineTransform(rotationAngle: .pi * 1.5)
let animationOptions: UInt
if repeating {
animationOptions = UIView.AnimationOptions.curveLinear.rawValue | UIView.AnimationOptions.repeat.rawValue
} else {
animationOptions = UIView.AnimationOptions.curveLinear.rawValue
}
let keyFrameAnimationOptions = UIView.KeyframeAnimationOptions(rawValue: animationOptions)
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [keyFrameAnimationOptions, .calculationModeLinear], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.375) {
view.transform = transform1
}
UIView.addKeyframe(withRelativeStartTime: 0.375, relativeDuration: 0.375) {
view.transform = transform2
}
UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25) {
view.transform = .identity
}
}, completion: nil)
}
Looks pretty gnarly, with the weird rotation angles, but as the op & others have found, you can't just tell it to rotate 360
Try this one it works for me, i am rotating image for once
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveLinear, animations: {
self.imgViewReload.transform = CGAffineTransformRotate(self.imgViewReload.transform, CGFloat(M_PI))
}, completion: {
(value: Bool) in
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveLinear, animations: {
self.imgViewReload.transform = CGAffineTransformIdentity
}, completion: nil)
})
you can use this function ... just give it the view and the duration
it has two animations the the first rotates the view 180° and the other one rotates it to 360° then the function calls itself which allows it to continue the rotation animation infinitely
func infinite360Animation(targetView: UIView, duration: Double) {
UIView.animate(withDuration: duration/2, delay: 0, options: .curveLinear) {
targetView.transform = CGAffineTransform.identity.rotated(by: .pi )
} completion: { (_) in
UIView.animate(withDuration: duration/2, delay: 0, options: .curveLinear) {
targetView.transform = CGAffineTransform.identity.rotated(by: .pi * 2)
} completion: { (_) in
self.infinite360Animation(targetView: targetView, duration: duration)
}
}
}
You can then call the function like this
infinite360Animatio(targetView: yourView, duration: 3)
On your tabBarController, make sure to set your delegate and do the following didSelect method below:
func tabBarController(_ tabBarController: UITabBarController,
didSelect viewController: UIViewController) {
if let selectedItem:UITabBarItem = tabBarController.tabBar.selectedItem {
animated(tabBar: tabBarController.tabBar, selectedItem: selectedItem)
}
}
fileprivate func animated(tabBar: UITabBar, selectedItem:UITabBarItem){
if let view:UIView = selectedItem.value(forKey: "view") as? UIView {
if let currentImageView = view.subviews.first as? UIImageView {
rotate(imageView: currentImageView, completion: { (completed) in
self.restore(imageView: currentImageView, completion: nil)
})
}
}
}
fileprivate func rotate(imageView:UIImageView, completion:((Bool) ->Void)?){
UIView.animate(withDuration: animationSpeed, delay: 0.0, options: .curveLinear, animations: {
imageView.transform = CGAffineTransform.init(rotationAngle: CGFloat.pi)
}, completion: {
(value: Bool) in
completion?(value)
})
}
fileprivate func restore(imageView:UIImageView, completion:((Bool) ->Void)?){
UIView.animate(withDuration: animationSpeed, delay: 0.0, options: .curveLinear, animations: {
imageView.transform = CGAffineTransform.identity
}, completion: {
(value: Bool) in
completion?(value)
})
}

Create a continuously rotating square on the screen using animateKeyframesWithDuration

I tried to use the code below to create a continuously rotating square on the screen. But I don't know why the rotational speed is changing. How could I change the code to make the rotational speed invariable? I tried different UIViewKeyframeAnimationOptions, but seems none of them work.
override func viewDidLoad() {
super.viewDidLoad()
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let duration = 1.0
let delay = 0.0
let options = UIViewKeyframeAnimationOptions.Repeat
UIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {
let fullRotation = CGFloat(M_PI * 2)
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1/3, animations: {
square.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)
})
UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: {
square.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)
})
UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: {
square.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)
})
}, completion: {finished in
})
}
I faced same problem before, this is how I make it work:
Swift 2
let raw = UIViewKeyframeAnimationOptions.Repeat.rawValue | UIViewAnimationOptions.CurveLinear.rawValue
let options = UIViewKeyframeAnimationOptions(rawValue: raw)
Swift 3,4,5
let raw = UIView.KeyframeAnimationOptions.repeat.rawValue | UIView.AnimationOptions.curveLinear.rawValue
let options = UIView.KeyframeAnimationOptions(rawValue: raw)
I figured this problem out just before gave it up. I don't think there is doc about it, but it just work.
That's really odd... UIView.animateKeyframesWithDuration isn't working as I would expect it to with UIViewKeyframeAnimationOptions.CalculationModeLinear|UIViewKeyframeAnimationOpti‌​ons.Repeat passed in with options.
If you use the non-block method of creating a keyframe animation (see below) the rotation repeats as expected.
If I find out why the block-based option isn't working I'll try and remember to update answer here too!
override func viewDidLoad() {
super.viewDidLoad()
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let fullRotation = CGFloat(M_PI * 2)
let animation = CAKeyframeAnimation()
animation.keyPath = "transform.rotation.z"
animation.duration = 2
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
animation.repeatCount = Float.infinity
animation.values = [fullRotation/4, fullRotation/2, fullRotation*3/4, fullRotation]
square.layer.addAnimation(animation, forKey: "rotate")
}
Add UIViewKeyframeAnimationOptionCalculationModeLinear do your keyframe options. The default behavior for UIView animations is to "ease in/out" of the animation. i.e., start slow, go up to speed, then slow down again just near the end.
AFAIU, this is happening because the default animation curve is UIViewAnimationOption.CurveEaseInOut.
Unfortunately, UIViewKeyframeAnimationOptions doesn't have options for changing the curve, but you can add them manually!
Use this extension:
Swift 2
extension UIViewKeyframeAnimationOptions {
static var CurveEaseInOut: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveEaseInOut.rawValue) }
static var CurveEaseIn: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveEaseIn.rawValue) }
static var CurveEaseOut: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveEaseOut.rawValue) }
static var CurveLinear: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveLinear.rawValue) }
}
Swift 3, 4, 5
extension UIView.KeyframeAnimationOptions {
static var curveEaseInOut: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveEaseInOut.rawValue) }
static var curveEaseIn: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveEaseIn.rawValue) }
static var curveEaseOut: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveEaseOut.rawValue) }
static var curveLinear: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveLinear.rawValue) }
}
Now you can use curve options in UIView.animateKeyframesWithDuration method
Swift 2
let keyframeOptions: UIViewKeyframeAnimationOptions = [.Repeat, .CurveLinear]
UIView.animateKeyframesWithDuration(duration, delay: delay, options: keyframeOptions, animations: {
// add key frames here
}, completion: nil)
Swift 3, 4, 5
let keyframeOptions: UIView.KeyframeAnimationOptions = [.repeat, .curveLinear]
UIView.animateKeyframes(withDuration: duration, delay: delay, options: keyframeOptions, animations: {
// add key frames here
}, completion: nil)

Resources