I'm having an animation that it supposed to rotate an image constantly. But there are couple issues with it. The velocity is quite odd and despite I've set it to repeat constantly, you can see how it starts, stops and then repeats. Which should not happen. It should be uninterrupted rotating.
Also, the other problem is when the animation stops, the image moves left for some reason.
Here's my code:
func animateLogo()
{
UIView.animate(withDuration: 6.0, delay: 0.0, options: .repeat, animations: {
self.logo.transform = CGAffineTransform(rotationAngle: ((180.0 * CGFloat(Double.pi)) / 180.0))
}, completion: nil)
}
Try this
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: YOUR_LOGO, duration: duration)
}
}
How to use
self.rotateView(targetView: YOUR_LOGO, duration: duration)
In iOS, the coordinate system is flipped. So you go clockwise as your degree gains. It means that passing 270° will give you an angle, equivalent to 90° in the standard coordinate system. Keep that in mind and provide needed angle accordingly.
Consider the following approach.
1) Handy extension for angle
postfix operator °
protocol IntegerInitializable: ExpressibleByIntegerLiteral {
init (_: Int)
}
extension Int: IntegerInitializable {
postfix public static func °(lhs: Int) -> CGFloat {
return CGFloat(lhs) * .pi / 180
}
}
extension CGFloat: IntegerInitializable {
postfix public static func °(lhs: CGFloat) -> CGFloat {
return lhs * .pi / 180
}
}
2) Rotate to any angle with CABasicAnimation:
extension UIView {
func rotateWithAnimation(angle: CGFloat, duration: CGFloat? = nil) {
let pathAnimation = CABasicAnimation(keyPath: "transform.rotation")
pathAnimation.duration = CFTimeInterval(duration ?? 2.0)
pathAnimation.fromValue = 0
pathAnimation.toValue = angle
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
self.transform = transform.rotated(by: angle)
self.layer.add(pathAnimation, forKey: "transform.rotation")
}
}
Usage:
override func viewDidAppear(_ animated: Bool) {
// clockwise
myView.rotateWithAnimation(angle: 90°)
// counter-clockwise
myView.rotateWithAnimation(angle: -270°)
}
Passing negative value will rotate counter-clockwise.
Angles should be in radians and not degrees. Angle in radians = degrees * pi / 180. So if you want to rotate by 360 degrees you should enter radians = 360 * pi / 180 = 2 * pi = 2 * 3.1415 = 6.283.
Related
I am using this code to rotate a view 360 degrees infinitely. But my view is not rotating:
func rotateImageView() {
UIView.animate(withDuration: 3.0, delay: 0, options: [.repeat, .curveLinear], animations: {
self.vinylView.transform = CGAffineTransform(rotationAngle: .pi * 2)
})
}
How to fix it?
Substitute pi * with /, delete repeat, add completion and recall the function like this:
private func rotateImageView() {
UIView.animate(withDuration: 3, delay: 0, options: .curveLinear, animations: {
self.vinylView.transform = self.vinylView.transform.rotated(by: .pi / 2)
}) { (finished) in
if finished {
self.rotateImageView()
}
}
}
Solution using CABasicAnimation
// Rotate vinvlView
vinylView.layer.add(CABasicAnimation.rotation, forKey: nil)
extension CABasicAnimation {
static let rotation : CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.repeatCount = .infinity // Rotate a view 360 degrees infinitely
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 3.0
return animation
}()
}
I have an animation of a bubble, like so:
func bubblePoint(_ value: CGFloat, midX: CGFloat) -> CGPoint {
let startY: CGFloat = UIScreen.main.bounds.height
let endY: CGFloat = -100
let rangeX: CGFloat = UIScreen.main.bounds.width * 0.1
let y = startY + (endY - startY) * value
let x = sin(value * 4 * .pi) * rangeX * (0.1 + value * 0.9) + midX * UIScreen.main.bounds.width
let point = CGPoint(x: x, y: y)
return point
}
func bubblePath(midX: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.move(to: bubblePoint(0, midX: midX))
for value in stride(from: CGFloat(0.01), through: 1, by: 0.01) {
path.addLine(to: bubblePoint(value, midX: midX))
}
return path
}
func createAnimation(midX: CGFloat, duration: CFTimeInterval) -> CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = bubblePath(midX: midX).cgPath
animation.duration = duration
animation.repeatCount = Float.infinity
return animation
}
func createBubble(midX: CGFloat, duration: CFTimeInterval) -> (UIImageView, CAKeyframeAnimation) {
return (
view: UIImageView().then {
$0.image = image
},
animation: createAnimation(midX: midX, duration: duration)
)
}
let bubbles = createBubble(midX: 100, duration: 11, )
bubble.layer.add(animation, forKey: nil)
I want to pause the bubble when the user goes to another screen (and then resume the animation when the user comes back). I have looked into a solution like this, but I would have no idea how to do this with an animation that uses a path like mine. Is it practical to do this?
There are two main ways to pause (freeze) an animation. One is to set the layer speed to zero. The other is to wrap the animation in a UIViewPropertyAnimator and pause the animator (you can do this even with a keyframe animation).
Notice, however, that when "user goes to another screen" the animation may be removed entirely. You may thus need to store info about where in the animation we were and start from there when your view controller comes back on screen.
I want to animate a UIImageView. The image should only spin clockwise. When the image is spinning, I want to speed it up. When I do speed it up, it goes counterclockwise and the start position is not correct as well. This is my code:
func rotateAnimation(theView: UIView, duration: CFTimeInterval = 2.0, repeatCount: Float = 1, toValue: CGFloat = CGFloat(.pi * 2.0), linear: Bool = false, animation: String = "transform.rotation") {
let rotateAnimation = CABasicAnimation(keyPath: animation)
let currentLayer = (theView.layer.presentation())
var currentAngle = CFloat((currentLayer?.value(forKeyPath: "transform.rotation") as? NSNumber)!)
currentAngle = roundf(currentAngle * 180 / Float(Double.pi))
print(currentAngle)
rotateAnimation.fromValue = currentAngle
if !linear{
rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
}
rotateAnimation.toValue = toValue
rotateAnimation.duration = duration
rotateAnimation.repeatCount = repeatCount
theView.layer.add(rotateAnimation, forKey: nil)
}
The print shows me to correct angle.
I am calling this method below twice. The second time I am calling it, the image is spinning counterclock:
rotateAnimation(theView: star, duration: 15.0, repeatCount: Float.infinity, toValue: CGFloat(Double.pi * 2), linear: true)
delay(delay: 3.5, closure: {
self.rotateAnimation(theView: self.star, duration: 7, repeatCount: Float.infinity, toValue: CGFloat(Double.pi * 2), linear: true)
})
I have a subclassed imageview that I'd like to fade in, scale up, and rotate, then continue rotating while scaling back down and fading out.
I am using a UIView animate block with a completion handler to handle the shrinking back down.
The problem is it's not a fluid animation. Before the completion handler runs, the animation stops before running again. I need it to be one nice "swoop" of an animation.
Code below:
let duration: TimeInterval = 3.0
let rotate = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
UIView.animate(withDuration: duration * 3, delay: 0, options: [.curveLinear], animations: {
// initial transform
self.alpha = 0
self.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
// initial spin for duration of animaton
UIView.animate(withDuration: duration * 3, delay: 0.0, options: [.curveLinear], animations: {
self.transform = rotate
}, completion: nil)
// scaling and fading
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear], animations: {
UIView.setAnimationRepeatCount(3)
self.transform = self.transform.scaledBy(x: 0.8, y: 0.8)
self.alpha = 1
}) { (true) in
UIView.animate(withDuration: duration, animations: {
UIView.setAnimationRepeatCount(3)
self.transform = self.transform.scaledBy(x: 0.1, y: 0.1)
self.alpha = 0
})
}
}, completion: nil)
How can I get the animation to rotate the entire time while fading in and scaling up before scaling back down and fading out? The entire animation should last 3 seconds, and repeat 3 times. Thanks.
For your modified request I am expanding what you already saw using the block animations. As some have said, key frame animations may be better, regardless, here is the thought.
Create an animation that rotates the entire time by transforming the view.
Create another animation that does the scaling and fading based off the current transform (which is rotating). In this pass, I just created some variable to allow you to customize (and repeat) portions of the animation. I broke some things out to be clear and know I could refactor to write thing even more concise.
Here is the code
import UIKit
class OrangeView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let duration: TimeInterval = 9.0
self.transform = CGAffineTransform.identity
// initial transform
self.alpha = 1
self.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
// start rotation
rotate(duration: duration)
// scaling and fading
scaleUpAndDown(desiredRepetitions: 3, initalDuration: duration)
}
func rotate(duration: TimeInterval) {
UIView.animate(withDuration: duration/2.0,
delay: 0.0,
options: [.curveLinear], animations: {
let angle = Double.pi
self.transform = self.transform.rotated(by: CGFloat(angle))
}, completion: {[weak self] finished in
guard let strongSelf = self else {
return
}
if finished &&
strongSelf.transform != CGAffineTransform.identity {
strongSelf.rotate(duration: duration)
} else {
// rotation ending
}
})
}
func scaleUpAndDown(timesRepeated: Int = 0, desiredRepetitions: Int, initalDuration: TimeInterval) {
guard timesRepeated < desiredRepetitions,
desiredRepetitions > 0, initalDuration > 0 else {
self.transform = CGAffineTransform.identity
return
}
let repeatedCount = timesRepeated + 1
let scalingDuration = initalDuration/2.0/Double(desiredRepetitions)
UIView.animate(withDuration: scalingDuration,
delay: 0.0,
animations: {
let desiredOriginalScale: CGFloat = 0.8
let scaleX = abs(CGAffineTransform.identity.a / self.transform.a) * desiredOriginalScale
let scaleY = abs(CGAffineTransform.identity.d / self.transform.d) * desiredOriginalScale
self.transform = self.transform.scaledBy(x: scaleX, y: scaleY)
self.alpha = 1
}) { (true) in
UIView.animate(withDuration:scalingDuration,
delay: 0.0,
animations: {
let desiredOriginalScale: CGFloat = 0.1
let scaleX = abs(CGAffineTransform.identity.a / self.transform.a) * desiredOriginalScale
let scaleY = abs(CGAffineTransform.identity.d / self.transform.d) * desiredOriginalScale
self.transform = self.transform.scaledBy(x: scaleX, y: scaleY)
self.alpha = 0
}) { finshed in
self.scaleUpAndDown(timesRepeated: repeatedCount, desiredRepetitions: desiredRepetitions, initalDuration: initalDuration);
}
}
}
}
Finally here is another animated gif
I see the slight stutter in rotation at the start of the onCompletion.
I created a reduction with your code (shown below in the Blue View) and a variation in the Orange View. This was taken from the simulator and turned into an animated GIF, so speed is slowed down. The Orange View continues to spin as the complete transform just scales down.
This is the code for the layoutSubviews() for the Orange View
override func layoutSubviews() {
super.layoutSubviews()
let duration: TimeInterval = 3.0
let rotate = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
// initial transform
self.alpha = 0
self.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
// initial spin for duration of animaton
UIView.animate(withDuration: duration,
delay: 0.0,
options: [.curveLinear],
animations: {
self.transform = rotate;
},
completion: nil)
// scaling and fading
UIView.animate(withDuration: duration/2.0, animations: {
self.transform = self.transform.scaledBy(x: 0.8, y: 0.8)
self.alpha = 1
}) { (true) in
UIView.animate(withDuration: duration/2.0, animations: {
self.transform = self.transform.scaledBy(x: 0.1, y: 0.1)
self.alpha = 0
})
}
}
Try using CGAffineTransformConcat()
CGAffineTransform scale = CGAffineTransformMakeScale(0.8, 0.8);
self.transform = CGAffineTransformConcat(CGAffineTransformRotate(self.transform, M_PI / 2), scale);
According to the documentation for rotated(by:):
angle
The angle, in radians, by which to rotate the affine transform. In iOS, a positive value specifies counterclockwise rotation and a negative value specifies clockwise rotation.
This has caused much confusion and has been partly answered, but even if we accept that the coordinate system is flipped (and just do the opposite of what the documentation says), it still provides inconsistent results when animating.
The direction of rotation -- clockwise or counterclockwise -- depends on the proximity of the target rotation to the current rotation:
<= 180º animates clockwise
> 180º animates counter-clockwise
Using UIView.animate or UIViewPropertyAnimator shows the inconsistency:
// Animates CLOCKWISE
UIView.animate(withDuration: 2.0) {
let radians = Angle(179).radians // 3.12413936106985
view.transform = view.transform.rotated(by: CGFloat(radians))
}
// Animates COUNTER-CLOCKWISE
UIViewPropertyAnimator(duration: 2, curve: .linear) {
let radians = Angle(181).radians // 3.15904594610974
view.transform = view.transform.rotated(by: CGFloat(radians))
}
// Does not animate
UIViewPropertyAnimator(duration: 2, curve: .linear) {
let radians = Angle(360).radians // 6.28318530717959
view.transform = view.transform.rotated(by: CGFloat(radians))
}
Try this:
let multiplier: Double = isSelected ? 1 : -2
let angle = CGFloat(multiplier * Double.pi)
UIView.animate(withDuration: 0.4) {
self.indicatorImageView.transform = CGAffineTransform(rotationAngle: angle)
}
The answer is to use CABasicAnimation for rotations past 180º, keeping in mind that positive values are clockwise and negative values are counterclockwise.
// Rotate 360º clockwise.
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = Angle(0).radians
rotate.toValue = Angle(360).radians
rotate.duration = 2.0
self.layer.add(rotate, forKey: "transform.rotation")
if sender.isSelected {
/// clockwise
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = 0
rotate.toValue = CGFloat.pi
rotate.duration = 0.25
rotate.fillMode = CAMediaTimingFillMode.forwards
rotate.isRemovedOnCompletion = false
self.arrowButton.layer.add(rotate, forKey: "transform.rotation")
} else {
/// anticlockwise
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = CGFloat.pi
rotate.toValue = 0
rotate.duration = 0.25
rotate.fillMode = CAMediaTimingFillMode.forwards
rotate.isRemovedOnCompletion = false
self.arrowButton.layer.add(rotate, forKey: "transform.rotation")
}
The third option allows you to rotate over 180º in the desired direction:
// #1 This rotates 90º clockwise
UIView.animate(withDuration: 0.25, animations:{
view.transform = CGAffineTransform(rotationAngle: -CGFloat.pi/2)
})
// #2 This rotates 90º counter clockwise back to #1
UIView.animate(withDuration: 0.25, animations:{
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
})
// #3 This rotates 270º clockwise back to #1
UIView.animate(withDuration: 0.1875, animations:{
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi/2)
}, completion: { _ in
UIView.animate(withDuration: 0.0625, animations:{
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
})
})