I use this code to create rotation animation for my imageView:
func rotate(imageView: UIImageView, aCircleTime: Double) {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = -Double.pi * 2
rotationAnimation.duration = aCircleTime
rotationAnimation.repeatCount = .infinity
imageView.layer.add(rotationAnimation, forKey: nil)
}
But how to pause this animation?
It's possible to pause and resume CAAnimations, but it's fussy and a little confusing. Take a look at this project on Github:
https://github.com/DuncanMC/ClockWipeSwift.git
It uses an extension on CALayer:
// Credit to Rand, from Stack Overflow, for the basis of this extension
// see https://stackoverflow.com/a/59079995/205185
import UIKit
import CoreGraphics
import Foundation
extension CALayer
{
func isPaused() -> Bool {
return speed == 0
}
private func internalPause(_ pause: Bool) {
if pause {
let pausedTime = convertTime(CACurrentMediaTime(), from: nil)
speed = 0.0
timeOffset = pausedTime
} else {
let pausedTime = timeOffset
speed = 1.0
timeOffset = 0.0
beginTime = 0.0
let timeSincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime
beginTime = timeSincePause
}
}
func pauseAnimation(_ pause: Bool) {
if pause != isPaused() {
internalPause(pause)
}
}
func pauseOrResumeAnimation() {
internalPause(isPaused())
}
}
Pausing and resuming is much easier if you use UIView animation and UIViewPropertyAnimator. There are lots of tutorials that explain how to do it.
You can look at this project on Github that shows how to use UIViewPropertyAnimator.
Related
I have a button, when it's tapped, it should rotate itself, here's my code:
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI)
rotateAnimation.speed = 3.0
rotateAnimation.repeatCount = 6000
calculateButton.layer.add(rotateAnimation, forKey: nil)
DispatchQueue.main.async {
self.openCircle(withCenter: sender.center, dataSource: self.calculator!.iterateWPItems())
self.calculateButton.layer.removeAllAnimations()
}
}
However, sometimes when I tap the button, it immediately goes back to normal state then rotates, sometimes the button changes to dark selected state, and doesn't animate at all, tasks after the animates will get finished. If I don't stop the animation, it starts after openCircle is finished.
What could be the cause?
You're not setting duration of your animation.
Replace this
rotateAnimation.speed = 3.0
with this
rotateAnimation.duration = 3.0
#alexburtnik and it's ok to block the main thread
No, it's not ok. You should add a completion parameter in openCircle method and call it whenever it's animation (or whatever) is finished. If you block main thread, you will have a frozen UI, which is strongly discouraged.
If you're unsure that calculateButtonTapped is called on main thread, you should dispatch first part of your method as well. Everything related to UI must be done on the main thread.
It should look similar to this:
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI)
rotateAnimation.duration = 3.0
rotateAnimation.repeatCount = .infinity //endless animation
calculateButton.layer.add(rotateAnimation, forKey: nil)
self.openCircle(
withCenter: sender.center,
dataSource: self.calculator!.iterateWPItems(),
completion: {
self.calculateButton.layer.removeAllAnimations()
})
}
func openCircle(withCenter: CGPoint, dataSource: DataSourceProtocol, completion: (()->Void)?) {
//do your staff and call completion when you're finished
//don't block main thread!
}
Try this out in order to rotate a button that is clicked by connecting the button to the action on a storyboard. You can of course call this function by passing any UIButton as the sender!
#IBAction func calculateButtonTapped(_ sender: UIButton) {
guard (sender.layer.animation(forKey: "rotate") == nil) else { return }
let rotationDuration: Float = 3.0
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.toValue = Float.pi * rotationDuration
animation.duration = CFTimeInterval(rotationDuration)
animation.repeatCount = .infinity
sender.layer.add(animation, forKey: "rotate")
}
Change the rotationDuration to whatever time length you want for a full rotation. You could also adjust the function further to take that as an argument.
Edit: Added a guard statement so that the rotations don't keep adding up every time that the button is tapped.
Thanks to everybody for answering, I found the solution myself after a crash course on multithreading, the problem is I blocked the main thread with openCircle method.
Here's the updated code:
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI)
rotateAnimation.speed = 3.0
rotateAnimation.repeatCount = .infinity
DispatchQueue.global(qos: .userInitiated).async {
self.openCircle(withCenter: sender.center, dataSource: self.calculator!.iterateWPItems()){}
DispatchQueue.main.sync {
self.calculateButton.layer.removeAllAnimations()
}
}
self.calculateButton.layer.add(rotateAnimation, forKey: nil)
}
I have amended some code I pulled from https://github.com/kaandedeoglu/KDCircularProgress
I have a small UIView which represents the circle progress timer. I have referenced it and also instantiated it to KDCircularProgress class.
I have managed to implement methods to start and reset the timer progress circular bar.
But I am having trouble restarting the circular progress bar when I pause the animation.
My code preamble:
import UIKit
class SomeTableViewController: UITableViewController {
//Circular progress vars
var currentCount = 1.0
let maxCount = 60.0
//Reference selected UIView & instantiate
#IBOutlet weak var circularProgressView: KDCircularProgress!
Start animation - 60 second animation:
if currentCount != maxCount {
currentCount += 1
circularProgressView.animateToAngle(360, duration: 60, completion: nil)
}
To stop and reset the animation:
currentCount = 0
circularProgressView.animateFromAngle(circularProgressView.angle, toAngle: 0, duration: 0.5, completion: nil)
To pause the animation:
circularProgressView.pauseAnimation()
How would I set up a method to restart the animation after the paused state?
Many thanks in advance for any clarification. It's my first animation, I have tried to resolve the matter myself, but cannot seem to find any syntax applicable to my particular case.
UPDATED SOLUTION:
Thanks to #Duncan C for putting me on the right path.
I solved my problem as follows ...
Since I initiated the counter's progress using currentCount += 1 I thought I would try to pause the counter with:
if currentCount != maxCount {
currentCount -= 1
circularProgressView.animateToAngle(360, duration: 60, completion: nil)
}
which I thought would have a 'net' effect on the counter (netting off counter +=1 and counter -=1) to effectively stop the counter. In theory this should being the counter's progress to zero, but it continued to count down.
So I reverted back to circularProgressView.pauseAnimation() to pause the circular counter animation.
To restart the animation after being paused, I had to amend the duration to represent the updated duration - i.e. the time at which the animation was paused.
I used a bit of a trick here and included a NSTimer - which I happened to have in my code anyway.
To restart the animation at the time of pause:
if currentCount != maxCount {
currentCount += 1
circularProgressView.animateToAngle(360, duration: NSTimeInterval(swiftCounter), completion: nil)
}
I couldn't figure out how to update my duration for the animated circular bar, but did know how to update time passed using an NSTimer - timer with the same duration and countdown speed. So I tagged the reference to the updated timer's value. Issue resolved ;)
My Code:
import UIKit
class SomeFunkyTableViewController: UITableViewController {
//Circular progress variables
var currentCount = 0.0
let maxCount = 60.0
#IBOutlet weak var circularProgressView: KDCircularProgress!
//Timer countdown vars
var swiftTimer = NSTimer()
var swiftCounter = 60
#IBOutlet weak var startButton: UIButton!
#IBOutlet weak var timerView: UIView!
#IBOutlet weak var timerLabel: UILabel!
#IBOutlet weak var startView: UIView!
override func viewDidLoad() {
circularProgressView.angle = 0
timerLabel.text = String(swiftCounter)
super.viewDidLoad()
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func startButton(sender: AnyObject) {
pauseBtn.alpha = 1.0
playBtn.alpha = 1.0
stopBtn.alpha = 1.0
circularProgressView.hidden = false
if currentCount != maxCount {
currentCount += 1
circularProgressView.animateToAngle(360, duration: 60, completion: nil)
}
startView.hidden = true
timerView.hidden = false
swiftTimer = NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: #selector(SomeFunkyTableViewController.updateCounter), userInfo: nil, repeats: true)
}
#IBOutlet weak var pauseBtn: UIButton!
#IBAction func pauseButton(sender: AnyObject) {
circularProgressView.pauseAnimation()
swiftTimer.invalidate()
pauseBtn.alpha = 0.5
playBtn.alpha = 1.0
stopBtn.alpha = 1.0
}
#IBOutlet weak var playBtn: UIButton!
#IBAction func playButton(sender: AnyObject) {
if currentCount != maxCount {
currentCount += 1
circularProgressView.animateToAngle(360, duration: NSTimeInterval(swiftCounter), completion: nil)
}
if !swiftTimer.valid {
swiftTimer = NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: #selector(SomeFunkyTableViewController.updateCounter), userInfo: nil, repeats: true)
}
if swiftCounter == 0 {
swiftTimer.invalidate()
}
pauseBtn.alpha = 1.0
playBtn.alpha = 0.5
stopBtn.alpha = 1.0
}
#IBOutlet weak var stopBtn: UIButton!
#IBAction func stopButton(sender: AnyObject) {
currentCount = 0
circularProgressView.animateFromAngle(circularProgressView.angle, toAngle: 0, duration: 0.5, completion: nil)
circularProgressView.hidden = true
timerView.hidden = true
startView.hidden = false
swiftTimer.invalidate()
swiftCounter = 60
timerLabel.text = String(swiftCounter)
pauseBtn.alpha = 1.0
playBtn.alpha = 1.0
stopBtn.alpha = 0.5
}
func updateCounter() {
swiftCounter -= 1
timerLabel.text = String(swiftCounter)
if swiftCounter == 0 {
swiftTimer.invalidate()
}
}
}
Side Note : I have two overlapping views - StartView and TimerView. One is hidden on view load, hence the hide/unhide references. And I dim buttons on press - play/pause/stop.
Pausing and resuming an animation takes special code. You might not be able to do it without modifying the library you are using.
The trick is to set the speed on the animation on the parent layer that's hosting the animation to 0 and record the time offset of the animation.
Here are a couple of methods (written in Objective-C) from one of my projects that pause and resume an animation:
- (void) pauseLayer: (CALayer *) theLayer
{
CFTimeInterval mediaTime = CACurrentMediaTime();
CFTimeInterval pausedTime = [theLayer convertTime: mediaTime fromLayer: nil];
theLayer.speed = 0.0;
theLayer.timeOffset = pausedTime;
}
//-----------------------------------------------------------------------------
- (void) removePauseForLayer: (CALayer *) theLayer;
{
theLayer.speed = 1.0;
theLayer.timeOffset = 0.0;
theLayer.beginTime = 0.0;
}
//-----------------------------------------------------------------------------
- (void) resumeLayer: (CALayer *) theLayer;
{
CFTimeInterval pausedTime = [theLayer timeOffset];
[self removePauseForLayer: theLayer];
CFTimeInterval mediaTime = CACurrentMediaTime();
CFTimeInterval timeSincePause =
[theLayer convertTime: mediaTime fromLayer: nil] - pausedTime;
theLayer.beginTime = timeSincePause;
}
Note that starting in iOS 10, there is a newer, better way to pause and resume UIView animations: UIViewPropertyAnimator.
I have a sample project (written in Swift) on Github that demonstrates this new UIViewPropertyAnimator class. Here's the link: UIViewPropertyAnimator-test
Below is an extract from the README from that project:
A UIViewPropertyAnimator allows you to easily create UIView-based animations that can be paused, reversed, and scrubbed back and forth.
A UIViewPropertyAnimator object takes a block of animations, very much like the older animate(withDuration:animations:) family of UIView class methods. However, a UIViewPropertyAnimator can be used to run mulitiple animation blocks.
There is built-in support for scrubbing an animation by setting the fractionComplete property on the animator. There is NOT an automatic mechanism to observe the animation progress, however.
You can reverse a UIViewPropertyAnimator animation by setting its isReversed property, but there are some quirks. If you change the isReversed property of a running animator from false to true, the animate reverses, but you can't set the isReversed property from true to false while the animation is running and have it switch direction from reverse to forward "live". You have to first pause the animation, switch the isReversed flag, and then restart the animation. (To use an automotive analogy, you can switch from forward to reverse while moving, but you have to come to a comlete stop before you can switch from reverse back into drive.)
in my swift 2 app, i have this extension:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 2.5, completionDelegate: AnyObject? = nil) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2.0)
rotateAnimation.duration = duration
if let delegate: AnyObject = completionDelegate {
rotateAnimation.delegate = delegate
}
self.layer.addAnimation(rotateAnimation, forKey: nil)
}
}
with this code, i can rotate an image 360 degree.
now i would like to stop this animation directly after i pressed on a button.
in my view controller is an action for my button. if i press this button, the following value will set:
self.shouldStopRotating = true
and i have this code part in the same vc, too:
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if self.shouldStopRotating == false {
self.LoadingCircle.rotate360Degrees(completionDelegate: self)
}
}
the image will stop after i pressed the button, but it will stop after the animation will be finished (after 360 degrees) - but this is to late.
the image have to stop rotating directly on the actual position after i press the button
Try to add this when the button that stops the animation is presse:
self.LoadingCircle.layer.removeAllAnimations()
let currentLayer = self.LoadingCircle.layer.presentationLayer();
let currentRotation = currentLayer?.valueForKeyPath("transform.rotation.z")?.floatValue;
let rotation = CGAffineTransformMakeRotation(CGFloat(currentRotation!));
self.LoadingCircle.transform = rotation;
I'm trying to make four UIButtons rotate in Swift. I got this:
import UIKit
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 * 2.0)
rotateAnimation.duration = duration
if let delegate: AnyObject = completionDelegate {
rotateAnimation.delegate = delegate
}
self.layer.addAnimation(rotateAnimation, forKey: nil)
}
}
But I need it to repeat. In my UIViewController I used the animationDidStop function but how can I know which of the four animation triggers it? It has a parameter called CAAnimation but I cannot compare it to anything. Any suggestion?
Re-write your method using CATransition and use the solution from this question:
How to identify CAAnimation within the animationDidStop delegate?
Is there an easy way to be called back when a Core Animation reaches certain points as it's running (for example, at 50% and 66% of completion ?
I'm currently thinking about setting up an NSTimer, but that's not really as accurate as I'd like.
I've finally developed a solution for this problem.
Essentially I wish to be called back for every frame and do what I need to do.
There's no obvious way to observe the progress of an animation, however it is actually possible:
Firstly we need to create a new subclass of CALayer that has an animatable property called 'progress'.
We add the layer into our tree, and then create an animation that will drive the progress value from 0 to 1 over the duration of the animation.
Since our progress property can be animated, drawInContext is called on our sublass for every frame of an animation. This function doesn't need to redraw anything, however it can be used to call a delegate function :)
Here's the class interface:
#protocol TAProgressLayerProtocol <NSObject>
- (void)progressUpdatedTo:(CGFloat)progress;
#end
#interface TAProgressLayer : CALayer
#property CGFloat progress;
#property (weak) id<TAProgressLayerProtocol> delegate;
#end
And the implementation:
#implementation TAProgressLayer
// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it's animating.
- (id)initWithLayer:(id)layer
{
self = [super initWithLayer:layer];
if (self) {
TAProgressLayer *otherLayer = (TAProgressLayer *)layer;
self.progress = otherLayer.progress;
self.delegate = otherLayer.delegate;
}
return self;
}
// Override needsDisplayForKey so that we can define progress as being animatable.
+ (BOOL)needsDisplayForKey:(NSString*)key {
if ([key isEqualToString:#"progress"]) {
return YES;
} else {
return [super needsDisplayForKey:key];
}
}
// Call our callback
- (void)drawInContext:(CGContextRef)ctx
{
if (self.delegate)
{
[self.delegate progressUpdatedTo:self.progress];
}
}
#end
We can then add the layer to our main layer:
TAProgressLayer *progressLayer = [TAProgressLayer layer];
progressLayer.frame = CGRectMake(0, -1, 1, 1);
progressLayer.delegate = self;
[_sceneView.layer addSublayer:progressLayer];
And animate it along with the other animations:
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"progress"];
anim.duration = 4.0;
anim.beginTime = 0;
anim.fromValue = #0;
anim.toValue = #1;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[progressLayer addAnimation:anim forKey:#"progress"];
Finally, the delegate will be called back as the animation progresses:
- (void)progressUpdatedTo:(CGFloat)progress
{
// Do whatever you need to do...
}
If you don't want to hack a CALayer to report progress to you, there's another approach. Conceptually, you can use a CADisplayLink to guarantee a callback on each frame, and then simply measure the time that has passed since the start of the animation divided by the duration to figure out the percent complete.
The open source library INTUAnimationEngine packages this functionality up very cleanly into an API that looks almost exactly like the UIView block-based animation one:
// INTUAnimationEngine.h
// ...
+ (NSInteger)animateWithDuration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
animations:(void (^)(CGFloat percentage))animations
completion:(void (^)(BOOL finished))completion;
// ...
All you need to do is call this method at the same time you start other animation(s), passing the same values for duration and delay, and then for each frame of the animation the animations block will be executed with the current percent complete. And if you want peace of mind that your timings are perfectly synchronized, you can drive your animations exclusively from INTUAnimationEngine.
I made a Swift (2.0) implementation of the CALayer subclass suggested by tarmes in the accepted answer:
protocol TAProgressLayerProtocol {
func progressUpdated(progress: CGFloat)
}
class TAProgressLayer : CALayer {
// MARK: - Progress-related properties
var progress: CGFloat = 0.0
var progressDelegate: TAProgressLayerProtocol? = nil
// MARK: - Initialization & Encoding
// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it's animating.
override init(layer: AnyObject) {
super.init(layer: layer)
if let other = layer as? TAProgressLayerProtocol {
self.progress = other.progress
self.progressDelegate = other.progressDelegate
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
progressDelegate = aDecoder.decodeObjectForKey("progressDelegate") as? CALayerProgressProtocol
progress = CGFloat(aDecoder.decodeFloatForKey("progress"))
}
override func encodeWithCoder(aCoder: NSCoder) {
super.encodeWithCoder(aCoder)
aCoder.encodeFloat(Float(progress), forKey: "progress")
aCoder.encodeObject(progressDelegate as! AnyObject?, forKey: "progressDelegate")
}
init(progressDelegate: TAProgressLayerProtocol?) {
super.init()
self.progressDelegate = progressDelegate
}
// MARK: - Progress Reporting
// Override needsDisplayForKey so that we can define progress as being animatable.
class override func needsDisplayForKey(key: String) -> Bool {
if (key == "progress") {
return true
} else {
return super.needsDisplayForKey(key)
}
}
// Call our callback
override func drawInContext(ctx: CGContext) {
if let del = self.progressDelegate {
del.progressUpdated(progress)
}
}
}
Ported to Swift 4.2:
protocol CAProgressLayerDelegate: CALayerDelegate {
func progressDidChange(to progress: CGFloat)
}
extension CAProgressLayerDelegate {
func progressDidChange(to progress: CGFloat) {}
}
class CAProgressLayer: CALayer {
private struct Const {
static let animationKey: String = "progress"
}
#NSManaged private(set) var progress: CGFloat
private var previousProgress: CGFloat?
private var progressDelegate: CAProgressLayerDelegate? { return self.delegate as? CAProgressLayerDelegate }
override init() {
super.init()
}
init(frame: CGRect) {
super.init()
self.frame = frame
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.progress = CGFloat(aDecoder.decodeFloat(forKey: Const.animationKey))
}
override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(Float(self.progress), forKey: Const.animationKey)
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == Const.animationKey { return true }
return super.needsDisplay(forKey: key)
}
override func display() {
super.display()
guard let layer: CAProgressLayer = self.presentation() else { return }
self.progress = layer.progress
if self.progress != self.previousProgress {
self.progressDelegate?.progressDidChange(to: self.progress)
}
self.previousProgress = self.progress
}
}
Usage:
class ProgressView: UIView {
override class var layerClass: AnyClass {
return CAProgressLayer.self
}
}
class ExampleViewController: UIViewController, CAProgressLayerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let progressView = ProgressView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
progressView.layer.delegate = self
view.addSubview(progressView)
var animations = [CAAnimation]()
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0
opacityAnimation.toValue = 1
opacityAnimation.duration = 1
animations.append(opacityAnimation)
let progressAnimation = CABasicAnimation(keyPath: "progress")
progressAnimation.fromValue = 0
progressAnimation.toValue = 1
progressAnimation.duration = 1
animations.append(progressAnimation)
let group = CAAnimationGroup()
group.duration = 1
group.beginTime = CACurrentMediaTime()
group.animations = animations
progressView.layer.add(group, forKey: nil)
}
func progressDidChange(to progress: CGFloat) {
print(progress)
}
}