I am trying to make a simple animation in Swift. It is a fade in.
I attempted:
self.myFirstLabel.alpha = 0
self.myFirstButton.alpha = 0
self.mySecondButton.alpha = 0
Then, I have:
self.view.addSubview(myFirstLabel)
self.view.addSubview(myFirstButton)
self.view.addSubview(mySecondButton)
And then:
UIView.animateWithDuration(1.5, animations: {
self.myFirstLabel.alpha = 1.0
self.myFirstButton.alpha = 1.0
self.mySecondButton.alpha = 1.0
})
I have all of this in my viewDidLoad function.
How do I make this work?
The problem is that you're trying start the animation too early in the view controller's lifecycle. In viewDidLoad, the view has just been created, and hasn't yet been added to the view hierarchy, so attempting to animate one of its subviews at this point produces bad results.
What you really should be doing is continuing to set the alpha of the view in viewDidLoad (or where you create your views), and then waiting for the viewDidAppear: method to be called. At this point, you can start your animations without any issue.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 1.5) {
self.myFirstLabel.alpha = 1.0
self.myFirstButton.alpha = 1.0
self.mySecondButton.alpha = 1.0
}
}
0x7ffffff's answer is ok and definitely exhaustive.
As a plus, I suggest you to make an UIView extension, in this way:
public extension UIView {
/**
Fade in a view with a duration
- parameter duration: custom animation duration
*/
func fadeIn(duration duration: NSTimeInterval = 1.0) {
UIView.animateWithDuration(duration, animations: {
self.alpha = 1.0
})
}
/**
Fade out a view with a duration
- parameter duration: custom animation duration
*/
func fadeOut(duration duration: NSTimeInterval = 1.0) {
UIView.animateWithDuration(duration, animations: {
self.alpha = 0.0
})
}
}
Swift-3
/// Fade in a view with a duration
///
/// Parameter duration: custom animation duration
func fadeIn(withDuration duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 1.0
})
}
/// Fade out a view with a duration
///
/// - Parameter duration: custom animation duration
func fadeOut(withDuration duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 0.0
})
}
Swift-5
public extension UIView {
/**
Fade in a view with a duration
- parameter duration: custom animation duration
*/
func fadeIn(duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 1.0
})
}
/**
Fade out a view with a duration
- parameter duration: custom animation duration
*/
func fadeOut(duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 0.0
})
}
}
In this way you can do this wherever in your code:
let newImage = UIImage(named: "")
newImage.alpha = 0 // or newImage.fadeOut(duration: 0.0)
self.view.addSubview(newImage)
...
newImage.fadeIn()
Code reuse is important!
Swift only solution
Similar to Luca's anwer, I use a UIView extension. Compared to his solution I use DispatchQueue.main.async to make sure animations are done on the main thread, alpha parameter for fading to a specific value and optional duration parameters for cleaner code.
extension UIView {
func fadeTo(_ alpha: CGFloat, duration: TimeInterval = 0.3) {
DispatchQueue.main.async {
UIView.animate(withDuration: duration) {
self.alpha = alpha
}
}
}
func fadeIn(_ duration: TimeInterval = 0.3) {
fadeTo(1.0, duration: duration)
}
func fadeOut(_ duration: TimeInterval = 0.3) {
fadeTo(0.0, duration: duration)
}
}
How to use it:
// fadeIn() - always animates to alpha = 1.0
yourView.fadeIn() // uses default duration of 0.3
yourView.fadeIn(1.0) // uses custom duration (1.0 in this example)
// fadeOut() - always animates to alpha = 0.0
yourView.fadeOut() // uses default duration of 0.3
yourView.fadeOut(1.0) // uses custom duration (1.0 in this example)
// fadeTo() - used if you want a custom alpha value
yourView.fadeTo(0.5) // uses default duration of 0.3
yourView.fadeTo(0.5, duration: 1.0)
If you want repeatable fade animation you can do that by using CABasicAnimation like below :
First create handy UIView extension :
extension UIView {
enum AnimationKeyPath: String {
case opacity = "opacity"
}
func flash(animation: AnimationKeyPath ,withDuration duration: TimeInterval = 0.5, repeatCount: Float = 5){
let flash = CABasicAnimation(keyPath: animation.rawValue)
flash.duration = duration
flash.fromValue = 1 // alpha
flash.toValue = 0 // alpha
flash.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
flash.autoreverses = true
flash.repeatCount = repeatCount
layer.add(flash, forKey: nil)
}
}
How to use it:
// You can use it with all kind of UIViews e.g. UIButton, UILabel, UIImage, UIImageView, ...
imageView.flash(animation: .opacity, withDuration: 1, repeatCount: 5)
titleLabel.flash(animation: .opacity, withDuration: 1, repeatCount: 5)
Swift 5
Other answers are correct, but in my case I need to handle other properties also (alpha, animate, completion). Because of this, I modified a bit to expose these parameters as below:
extension UIView {
/// Helper function to update view's alpha with animation
/// - Parameter alpha: View's alpha
/// - Parameter animate: Indicate alpha changing with animation or not
/// - Parameter duration: Indicate time for animation
/// - Parameter completion: Completion block after alpha changing is finished
func set(alpha: CGFloat, animate: Bool, duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) {
let animation = { (view: UIView) in
view.alpha = alpha
}
if animate {
UIView.animate(withDuration: duration, animations: {
animation(self)
}, completion: { finished in
completion?(finished)
})
} else {
layer.removeAllAnimations()
animation(self)
completion?(true)
}
}
}
import UIKit
/*
Here is simple subclass for CAAnimation which create a fadeIn animation
*/
class FadeInAdnimation: CABasicAnimation {
override init() {
super.init()
keyPath = "opacity"
duration = 2.0
fromValue = 0
toValue = 1
fillMode = CAMediaTimingFillMode.forwards
isRemovedOnCompletion = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
/*
Example of usage
*/
class ViewController: UIViewController {
weak var label: UILabel!
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.alpha = 0
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.label = label
let button = UIButton(type: .custom)
button.frame = CGRect(x: 0, y: 250, width: 300, height: 100)
button.setTitle("Press to Start FadeIn", for: UIControl.State())
button.backgroundColor = .red
button.addTarget(self, action: #selector(startFadeIn), for: .touchUpInside)
view.addSubview(button)
self.view = view
}
/*
Animation in action
*/
#objc private func startFadeIn() {
label.layer.add(FadeInAdnimation(), forKey: "fadeIn")
}
}
Related
Hi I want to make animation with 3 UIView. The main problem is I can't start animation once it's stopped by removing animation from layer.
Here is the code:
class HTAnimatedTypingView: UIView {
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
#IBOutlet weak var view3: UIView!
func startAnimation(){
UIView.animate(withDuration: 0.5, delay: 0, options: .repeat, animations: {
self.view1.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.3, delay: 0.5, options: .repeat, animations: {
self.view2.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.2, delay: 1.0, options: .repeat, animations: {
self.view3.frame.origin.y = 0
}, completion: nil)
}
func stopAnimations(){
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
self.view3.layer.removeAllAnimations()
}
}
Output of Above Code:
Expected Animation:
How can make it work with start animation & stop animation functionality? Thanks in advance...
Since you need to add some pause in between each sequence of animations, I would personally do it using key frames as it gives you some flexibility:
class AnimationViewController: UIViewController {
private let stackView: UIStackView = {
$0.distribution = .fill
$0.axis = .horizontal
$0.alignment = .center
$0.spacing = 10
return $0
}(UIStackView())
private let circleA = UIView()
private let circleB = UIView()
private let circleC = UIView()
private lazy var circles = [circleA, circleB, circleC]
func animate() {
let jumpDuration: Double = 0.30
let delayDuration: Double = 1.25
let totalDuration: Double = delayDuration + jumpDuration*2
let jumpRelativeDuration: Double = jumpDuration / totalDuration
let jumpRelativeTime: Double = delayDuration / totalDuration
let fallRelativeTime: Double = (delayDuration + jumpDuration) / totalDuration
for (index, circle) in circles.enumerated() {
let delay = jumpDuration*2 * TimeInterval(index) / TimeInterval(circles.count)
UIView.animateKeyframes(withDuration: totalDuration, delay: delay, options: [.repeat], animations: {
UIView.addKeyframe(withRelativeStartTime: jumpRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y -= 30
}
UIView.addKeyframe(withRelativeStartTime: fallRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y += 30
}
})
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circles.forEach {
$0.layer.cornerRadius = 20/2
$0.layer.masksToBounds = true
$0.backgroundColor = .systemBlue
stackView.addArrangedSubview($0)
$0.widthAnchor.constraint(equalToConstant: 20).isActive = true
$0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animate()
}
}
It should be pretty straightforward, but feel free to let me know if you have any questions!
And this is how the result looks like:
One way could be to use a Timer. Keep an instance of Timer in your class. When startAnimation is called, schedule it. When stopAnimation is called, invalidate it. (This means that the currently ongoing animation will be completed before the animation actually stops, which IMO makes it a nice non-abrupt stop).
On each tick of the timer, animate the dots once. Note that the animation you apply on each dot should have the same duration, as in the expected output, they all bounce at the same rate, just at different instants in time.
Some illustrative code:
// startAnimation
timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { _ in
self.animateDotsOnce()
}
// stopAnimation
timer.invalidate()
// animateDotsOnce
UIView.animate(withDuration: animationDuration, delay: 0, animations: {
self.view1.frame.origin.y = animateHeight
}, completion: {
_ in
UIView.animate(withDuration: animationDuration) {
self.view1.frame.origin.y = 0
}
})
// plus the other two views, with different delays...
I'll leave it to you to find a suitable animateHeight, timerInterval, animationDuration and delays for each view.
I'd recommend using a CAKeyframeAnimation instead of handling completion blocks and that sorcery. Here's a quick example:
for i in 0 ..< 3 {
let bubble = UIView(frame: CGRect(x: 20 + i * 20, y: 200, width: 10, height: 10))
bubble.backgroundColor = .red
bubble.layer.cornerRadius = 5
self.view.addSubview(bubble)
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [0, 10, 0]
animation.keyTimes = [0, 0.5, 1]
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 1
animation.isAdditive = true
animation.repeatCount = HUGE
animation.timeOffset = CACurrentMediaTime() + 0.2 * Double(i)
bubble.layer.add(animation, forKey: "anim")
}
When you wanna remove the animation you just use bubble.layer.removeAnimation(forKey: "anim"). You might have to play around with the timing function or values and keyTimes to get the exact movement you want. But keyframes is the way to go to make a specific animation.
Side note: this example won't work in viewDidLoad cause the view doesn't have a superview yet so the animation won't work. If you test it in viewDidAppear it will work.
UIView animation is different to CALayer animation,best not to mix them.
Write locally and tested.
import UIKit
import SnapKit
class HTAnimatedTypingView: UIView {
private let view0 = UIView()
private let view1 = UIView()
private let view2 = UIView()
init() {
super.init(frame: CGRect.zero)
makeUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
makeUI()
}
private func makeUI() {
backgroundColor = UIColor.white
view0.backgroundColor = UIColor.red
view1.backgroundColor = UIColor.blue
view2.backgroundColor = UIColor.yellow
addSubview(view0)
addSubview(view1)
addSubview(view2)
view0.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.left.equalTo(self.snp.left)
}
view1.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.centerX.equalTo(self.snp.centerX)
}
view2.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.right.equalTo(self.snp.right)
}
}
public func startAnimation() {
let duration:CFTimeInterval = 0.5
let animation_delay:CFTimeInterval = 0.1
assert(duration >= animation_delay * 5, "animation_delay should be way smaller than duration in order to make animation natural")
let translateAnimation = CABasicAnimation(keyPath: "position.y")
translateAnimation.duration = duration
translateAnimation.repeatCount = Float.infinity
translateAnimation.toValue = 0
translateAnimation.fillMode = CAMediaTimingFillMode.both
translateAnimation.isRemovedOnCompletion = false
translateAnimation.autoreverses = true
view0.layer.add(translateAnimation, forKey: "translation")
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay) { [unowned self ] in
self.view1.layer.add(translateAnimation, forKey: "translation")
}
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay * 2) { [unowned self ] in
self.view2.layer.add(translateAnimation, forKey: "translation")
}
}
public func stopAnimation() {
self.view0.layer.removeAllAnimations()
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
}
}
I have an imageView and want it to rotate 360° all the time, but I found an issue which is that when the App enters background and then back to the foreground, the rotate animation will be stopped.
And I don't want to re-call the function rotate360Degree() when the app back to the foreground, the reason is that I want the rotate-animation will start at the position where it left when entering background, instead of rotating from 0 again.
But when I call the function resumeRotate(), it doesn't work.
The extension as follow:
extension UIImageView {
// 360度旋转图片
func rotate360Degree() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") // 让其在z轴旋转
rotationAnimation.toValue = NSNumber(value: .pi * 2.0) // 旋转角度
rotationAnimation.duration = 20 // 旋转周期
rotationAnimation.isCumulative = true // 旋转累加角度
rotationAnimation.repeatCount = MAXFLOAT // 旋转次数
rotationAnimation.autoreverses = false
layer.add(rotationAnimation, forKey: "rotationAnimation")
}
// 暂停旋转
func pauseRotate() {
layer.pauseAnimation()
}
// 恢复旋转
func resumeRotate() {
layer.resumeAnimation()
}
}
Here is the layer Extension :
var pauseTime:CFTimeInterval!
extension CALayer {
//暂停动画
func pauseAnimation() {
pauseTime = convertTime(CACurrentMediaTime(), from: nil)
speed = 0.0
timeOffset = pauseTime
}
//恢复动画
func resumeAnimation() {
// 1.取出时间
pauseTime = timeOffset
// 2.设置动画的属性
speed = 1.0
timeOffset = 0.0
beginTime = 0.0
// 3.设置开始动画
let startTime = convertTime(CACurrentMediaTime(), from: nil) - pauseTime
beginTime = startTime
}
}
I can solve the above 'stopped' issue with CADisplayLink, but the animation will not rotate from the position where it left(rotate all the time).
I wonder how to solve it with CADisplayLink?
And how with the above core animation?
displayLink = CADisplayLink(target: self, selector: #selector(rotateImage))
displayLink.add(to: .current, forMode: .commonModes)
func rotateImage(){
let angle = CGFloat(displayLink.duration * Double.pi / 18)
artworkImageView.transform = artworkImageView.transform.rotated(by: angle)
}
You can do this with UIKit's higher-level block based animation api. If you want a continuously rotating view with 20.0 second duration. You can with a function like:
func animateImageView()
{
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .repeat, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi))
self.imageView.transform = transform
},
completion:
{ _ in
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi / 2.0))
self.imageView.transform = transform
})
})
}
This will just rotate the view by 180 degrees then once that is complete rotate it another 180 for 360 degree rotation. The .repeat option will cause it to repeat indefinitely. However, the animation will stop when the app is backgrounded. For that, we need to save the state of the presentation layer of the view being rotated. We can do that by storing the CGAffineTransform of the presentation layer and then setting that transform to the view being animated when the app comes back into the foreground. Here's an example with a UIImageView
class ViewController: UIViewController
{
#IBOutlet weak var imageView: UIImageView!
var imageViewTransform = CGAffineTransform.identity
override func viewDidLoad()
{
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.didEnterBackground), name: .UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.willEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
animateImageView()
}
deinit
{
NotificationCenter.default.removeObserver(self)
}
func didEnterBackground()
{
imageViewTransform = imageView.layer.presentation()?.affineTransform() ?? .identity
}
func willEnterForeground()
{
imageView.transform = imageViewTransform
animateImageView()
}
func animateImageView()
{
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .repeat, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi))
self.imageView.transform = transform
},
completion:
{ _ in
UIView.animate(withDuration: 10.0, delay: 0.0, options: [.beginFromCurrentState, .curveLinear], animations:
{ [unowned self] in
var transform = self.imageView.transform
transform = transform.concatenating(CGAffineTransform(rotationAngle: CGFloat.pi / 2.0))
self.imageView.transform = transform
})
})
}
}
Which results in this:
I have a UIView subclass called View, in which I want a "ripple" to move outwards from wherever the user double clicks (as well as preforming other drawing functions). Here is the relevant part of my draw method, at present:
override func draw(_ rect: CGRect) {
for r in ripples {
r.paint()
}
}
This is how r.paint() is implemented, in class Ripple:
func paint() {
let c = UIColor(white: CGFloat(1.0 - (Float(iter))/100), alpha: CGFloat(1))
print(iter, " - ",Float(iter)/100)
c.setFill()
view.fillCircle(center: CGPoint(x:x,y:y), radius: CGFloat(iter))
}
iter is supposed to be incremented every 0.1 seconds by a Timer which is started in the constructor of Ripple:
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: move)
move is implemented as follows:
func move(_ timer: Timer) {
while (tier<100) {
iter += 1
DispatchQueue.main.async {
self.view.setNeedsDisplay()
}
}
timer.invalidate()
}
What is supposed to happen is that every 0.1 seconds when the timer fires, it will increment iter and tell the View to repaint, which it will then do using the Ripple.paint() method, which uses iter to determine the color and radius of the circle.
However, as revealed using print statements, what happens instead is that the Timer fires all 100 times before the redraw actually takes place. I have tried dealing with this by replacing DispatchQueue.main.async with DispatchQueue.main.sync, but this just made the app hang. What am I doing wrong? If this is just not the right way to approach the problem, how can I get a smooth ripple animation (which involves a circle which grows while changing colors) to take place while the app can still preform other functions such as spawning more ripples? (This means that multiple ripples need to be able to work at once.)
You can achieve what you want with CABasicAnimation and custom CALayer, like that:
class MyLayer : CALayer
{
var iter = 0
override class func needsDisplay(forKey key: String) -> Bool
{
let result = super.needsDisplay(forKey: key)
return result || key == "iter"
}
override func draw(in ctx: CGContext) {
UIGraphicsPushContext(ctx)
UIColor.red.setFill()
ctx.fill(self.bounds)
UIGraphicsPopContext()
NSLog("Drawing, \(iter)")
}
}
class MyView : UIView
{
override class var layerClass: Swift.AnyClass {
get {
return MyLayer.self
}
}
}
class ViewController: UIViewController {
let myview = MyView()
override func viewDidLoad() {
super.viewDidLoad()
myview.frame = self.view.bounds
self.view.addSubview(myview)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animation = CABasicAnimation(keyPath: "iter")
animation.fromValue = 0
animation.toValue = 100
animation.duration = 10.0
myview.layer.add(animation, forKey: "MyIterAnimation")
(myview.layer as! MyLayer).iter = 100
}
}
Custom UIView like AlertView with Animation for Swift 3 and Swift 4
You can use a extension:
extension UIVIew{
func customAlertView(frame: CGRect, message: String, color: UIColor, startY: CGFloat, endY: CGFloat) -> UIVIew{
//Adding label to view
let label = UILabel()
label.frame = CGRect(x: 0, y: 0, width: frame.width, height:70)
label.textAlignment = .center
label.textColor = .white
label.numberOfLines = 0
label.text = message
self.addSubview(label)
self.backgroundColor = color
//Adding Animation to view
UIView.animate(withDuration:0.5, delay: 0, options:
[.curveEaseOut], animations:{
self.frame = CGRect(x: 0, y: startY, width: frame.width, height: 64)
}) { _ in
UIView.animate(withDuration: 0.5, delay: 4, options: [.curveEaseOut], animations: {
self.frame = CGRect(x: 0, y: endY, width: frame.width, height:64)
}, completion: {_ in
self.removeFromSuperview()
})
}
return self
}
And you can use it un your class. This example startY is when you have a navigationController:
class MyClassViewController: UIViewController{
override func viewDidLoad(){
super.viewDidLoad()
self.view.addSubView(UIView().addCustomAlert(frame: self.view.frame, message: "No internet connection", color: .red, startY:64, endY:-64))
}
}
I am using facebook Pop framework for iOS.
How do i make a view flip horizontal (on its Y axis) ?
Here is my code:
func buttonClicked(sender: UIButton) {
aBoolValue = !aBoolValue
sender.userInteractionEnabled = false
let layer = sender.layer
// First let's remove any existing animations
layer.pop_removeAllAnimations()
layer.removeAllAnimations()
let rotation: POPSpringAnimation = POPSpringAnimation(propertyNamed:kPOPLayerRotationY)
if aBoolValue == true {
rotation.toValue = M_PI
sender.setTitle("G", forState: UIControlState.Normal)
} else {
rotation.toValue = 0
sender.setTitle("P", forState: UIControlState.Normal)
}
rotation.completionBlock = {(anim: POPAnimation! , finished: Bool) -> Void in
layer.pop_removeAllAnimations()
sender.userInteractionEnabled = true
}
layer.pop_addAnimation(rotation, forKey: "rotation")
The results of this is that it flips as expected, but after about 5-6 flips, the animation begins to flip randomly and not normally. How do i flip a view horizontally the right way?
It is a bug in pop library. see https://github.com/facebook/pop/issues/71
People can create somewhat similar animation with UIView.animateWithDuration: method.
for example
private func get3DTransformation(angle: Double) -> CATransform3D {
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 500.0
transform = CATransform3DRotate(transform, CGFloat(angle * M_PI / 180.0), 0, 1, 0.0)
return transform
}
private func flipAnimation(view: UIView, completion: (() -> Void) = {}) {
let angle = 180.0
view.layer.transform = get3DTransformation(angle)
UIView.animateWithDuration(1, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: .TransitionNone, animations: { () -> Void in
view.layer.transform = CATransform3DIdentity
}) { (finished) -> Void in
completion()
}
}
func buttonClicked(sender: UIButton) {
aBoolValue = !aBoolValue
if aBoolValue == true {
sender.setTitle("G", forState: UIControlState.Normal)
} else {
sender.setTitle("P", forState: UIControlState.Normal)
}
flipAnimation(sender)
}
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)
})
}