How to pause and resume UIView.animateWithDuration - ios

I have an image, I animate it with this code, in viewDidAppear:
UIView.animateWithDuration(10.5, delay:0.0, options: [], animations:{
self.myImage.transform = CGAffineTransformMakeTranslation(0.0, 200)
}, completion: nil)
I want to pause the animation when I tap myPauseButton, and resume the animation if I tap it again.

2 functions to pause and resume animation, I take from here and convert to Swift.
func pauseLayer(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
func resumeLayer(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
I have a button to pause or resume the animation which is initiated in viewDidLoad:
var pause = false
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 10.5) {
self.image.transform = CGAffineTransformMakeTranslation(0.0, 200)
}
}
#IBAction func changeState() {
let layer = image.layer
pause = !pause
if pause {
pauseLayer(layer)
} else {
resumeLayer(layer)
}
}

Here is Swift 3 version of that answer + I moved those function to an extension
extension CALayer {
func pause() {
let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
self.speed = 0.0
self.timeOffset = pausedTime
}
func resume() {
let pausedTime: CFTimeInterval = self.timeOffset
self.speed = 1.0
self.timeOffset = 0.0
self.beginTime = 0.0
let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
self.beginTime = timeSincePause
}
}

Since iOS 10 provides UIViewPropertyAnimator you can solve your problem easier.
Declare these properties in your controller:
var animationPaused = false
lazy var animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: 10.5, curve: .easeInOut, animations: {
self.myImage.transform = CGAffineTransform(translationX: 0.0, y: 200)
})
Add the following code to the tap handler of myPauseButton:
if self.animator.state == .active { // Don't start or pause the animation when it's finished
self.animationPaused = !self.animationPaused
self.animationPaused ? self.animator.pauseAnimation() : self.animator.startAnimation()
}
Start the animation in viewDidAppear(_ animated: Bool) with these lines of code:
self.animationPaused = false
self.animator.startAnimation()

Related

How to reset a CABasicAnimation before it has ended?

I have a CAShapelayer circle that I animate as a progress circle with CABasicAnimation from 0 back to 360 degrees alongside a countdown timer in statemachine with Reset, Play, Pause, and Complete states.
Right now, I can play or pause and resume until the timer finishes and the animation completes and model goes back to its original values, then pick another countdown timer value to start with animation.
However, if I play and then reset at anytime before animation is complete, it cancels but when I go to play again, the animation no longer works. I am noticing that for some reason after I reset before animation is complete, my strokeEnd doesn't start at 0.0 again and instead gets a seemingly arbitrary decimal point value. I think this is the root cause of my issue, but I don't know why the strokeEnd value are these random numbers. Here's a screenshot of the strokeEnd values - https://imgur.com/a/YXmNkaK
Here's what I have so far:
//draws CAShapelayer
func drawCircle() {}
//CAShapeLayer animation
func progressCircleAnimation(transitionDuration: TimeInterval, speed: Double, strokeEnd: Double) {
let fillLineAnimation = CABasicAnimation(keyPath: "strokeEnd")
fillLineAnimation.duration = transitionDuration
fillLineAnimation.fromValue = 0
fillLineAnimation.toValue = 1.0
fillLineAnimation.speed = Float(speed)
circleWithProgressBorderLayer.strokeEnd = CGFloat(strokeEnd)
circleWithProgressBorderLayer.add(fillLineAnimation, forKey: "lineFill")
}
//Mark: Pause animation
func pauseLayer(layer : CALayer) {
let pausedTime : CFTimeInterval = circleWithProgressBorderLayer.convertTime(CACurrentMediaTime(), from: nil)
circleWithProgressBorderLayer.speed = 0.0
circleWithProgressBorderLayer.timeOffset = pausedTime
}
//Mark: Resume CABasic Animation on CAShaperLayer at pause offset
func resumeLayer(layer : CALayer) {
let pausedTime = circleWithProgressBorderLayer.timeOffset
circleWithProgressBorderLayer.speed = 1.0;
circleWithProgressBorderLayer.timeOffset = 0.0;
circleWithProgressBorderLayer.beginTime = 0.0;
let timeSincePause = circleWithProgressBorderLayer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
circleWithProgressBorderLayer.beginTime = timeSincePause;
}
//Mark: Tried to removeAnimation
func resetLayer(layer : CALayer) {
layer.removeAnimation(forKey: "lineFill")
circleWithProgressBorderLayer.strokeEnd = 0.0
}
Here's how I have it set up in my statemachine, in case the error has to do with this:
#objc func timerIsReset() {
resetTimer()
let currentRow = timePickerView.selectedRow(inComponent: 0)
self.counter = self.Times[currentRow].amount
}
//Mark: action for RunningTimerState
#objc func timerIsStarted() {
runTimer()
}
//Mark: action for PausedTimerState
#objc func timerIsPaused() {
pauseTimer()
}
//Mark: action for TimerTimeRunOutState
func timerTimeRunOut() {
resetTimer()
}
func runTimer() {
if isPaused == true {
finishTime = Date().addingTimeInterval(-remainingTime)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
resumeLayer(layer: circleWithProgressBorderLayer)
} else if isPaused == false {
finishTime = Date().addingTimeInterval(counter)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
progressCircleAnimation(transitionDuration: counter, speed: 1.0, strokeEnd: self.completionPercentage)
}
}
//Mark: Pause Timer and pause CABasic Animation
func pauseTimer() {
timer.invalidate()
isPaused = true
remainingTime = -finishTime.timeIntervalSinceNow
completionPercentage = (counter + remainingTime) / counter
pauseLayer(layer: circleWithProgressBorderLayer)
}
func resetTimer() {
timer.invalidate()
isPaused = false
resetLayer(layer: circleWithProgressBorderLayer)
}
Lets say your animation looks something like this...
func runTimerMaskAnimation(duration: CFTimeInterval, fromValue : Double){
...
let path = UIBezierPath(roundedRect: circleBounds, cornerRadius:
circleBounds.size.width * 0.5)
maskLayer?.path = path.reversing().cgPath
maskLayer?.strokeEnd = 0
parentCALayer.mask = maskLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = fromValue
animation.toValue = 0.0
maskLayer?.add(animation!, forKey: "strokeEnd")
}
If I wanted to restart my timer to the original position before the animation is complete I would remove the animation, remove the maskLayer and then run the animation again.
maskLayer?.removeAnimation(forKey: "strokeEnd")
maskLayer?.removeFromSuperlayer()
runTimerMaskAnimation(duration: 15, fromValue : 1)

How to resume core animation when the app back to foreground

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:

Animation not working when home button is pressed and the app is relaunch again

My animation stopped running when I press the home button and then relaunch the app. The settings button just stop spinning and the blink label just faded away. Here is my code for both animation:
Blink animation:
extension UILabel {
func startBlink() {
UIView.animate(withDuration: 0.8,
delay:0.0,
options:[.autoreverse, .repeat],
animations: {
self.alpha = 0
}, completion: nil)
}
}
Rotating animation:
extension UIButton {
func startRotating() {
UIView.animate(withDuration: 4.0, delay: 0.0, options:[.autoreverse, .repeat,UIViewAnimationOptions.allowUserInteraction], animations: {
self.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}, completion: nil)
}
}
Where I run it:
override func viewDidLoad() {
super.viewDidLoad()
settingsButton.layer.cornerRadius = 0.5 * settingsButton.bounds.size.width
settingsButton.clipsToBounds = true
settingsButton.imageView?.contentMode = .scaleAspectFit
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToForeground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
}
func appMovedToForeground() {
tapToPlayLabel.startBlink()
settingsButton.startRotating()
print("DID")
}
To restart your animation you have to do below thing, please check below code.
Check extension
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tapToPlayLabel: UILabel!
#IBOutlet weak var settingsButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
settingsButton.layer.cornerRadius = settingsButton.frame.size.width/2
settingsButton.clipsToBounds = true
//settingsButton.imageView?.contentMode = .scaleAspectFit
settingsButton.startRotating()
tapToPlayLabel.startBlink()
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToForeground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
}
func appMovedToForeground() {
self.tapToPlayLabel.startBlink()
self.settingsButton.startRotating()
}
}
extension UILabel {
func startBlink() {
self.alpha = 1
UIView.animate(withDuration: 0.8,
delay:0.0,
options:[.autoreverse, .repeat],
animations: {
self.alpha = 0
}, completion: nil)
}
}
extension UIButton {
func startRotating() {
self.transform = CGAffineTransform(rotationAngle: CGFloat.pi/2)
UIView.animate(withDuration: 4.0, delay: 0.0, options:[.autoreverse, .repeat,UIViewAnimationOptions.allowUserInteraction], animations: {
self.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}, completion: nil)
}
}
Output
I think you need to run your animations with a little delay as there is already the delegate of app in execution while app is moving to foreground.
Or you can add CALayerAnimation on UILabel
extension UILabel {
func startBlink() {
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform")
scaleAnimation.delegate = self as? CAAnimationDelegate
let transform: CATransform3D = CATransform3DMakeScale(1.5, 1.5, 1)
scaleAnimation.values = [NSValue(caTransform3D: CATransform3DIdentity), NSValue(caTransform3D: transform), NSValue(caTransform3D: CATransform3DIdentity)]
scaleAnimation.duration = 0.5
scaleAnimation.repeatCount = 100000000
self.layer.add(scaleAnimation as? CAAnimation ?? CAAnimation(), forKey: "scaleText")
}
func startRotating() {
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = NSNumber(value: Double.pi * 2)
rotation.duration = 2
rotation.isCumulative = true
rotation.repeatCount = .greatestFiniteMagnitude
self.layer.add(rotation, forKey: "rotationAnimation")
}
}

Progress bar pause/resume

I have table view and in every reusable cell there is a progress bar, now the question is how can i pause and play the progress and its animation if the the button is touched.
var arr = [10 , 20 , 30 , 40]
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let tablecell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! TableViewCell
tablecell.tableImage.image = images[indexPath.row]
tablecell.tabelLabel.text = nameUrl[indexPath.row]
let count: Float = Float(arr[indexPath.row])
if flag == false{
tablecell.prog_bar.setProgress(count, animated: true)
} else {
tablecell.prog_bar.setProgress(0, animated: false)
tableView.reloadData()
}
return tablecell
}
Try these functions. These functions work perfectly.
func start() {
self.progressview.progress = 1
UIView.animate(withDuration: 5.0, delay: 0.0, options: .curveLinear, animations: {
self.progressview.layoutIfNeeded()
}, completion: nil)
}
func resume() {
let layer = progressview.layer
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
func pause() {
let layer = progressview.layer
let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}

CABasicAnimation responsiveness

I have a custom UIActivityIndicatorView. It is a view, with a CAAnimation on its layer. The problem is the following:
I do some heavy work and create a lot of views. It takes approximately 0.5 seconds. In order for it to be smooth I decided to use activity indicator, while it "happens". It was all fine with default activity indicator, however with the one that I wrote I get unexpected results.
So, when the view loads I launch my activity indicator, which starts animating. When heavy duty work starts my view freezes for 0.5 seconds and when it's done I stop animating it and it disappears. This "freeze" looks very unpleasant to an eye. Because the idea was to keep animating while other views get initialized and added as subviews(although hidden).
I suspect that the problem is that my "activity indicator" is not asynchronous or simply was not coded right.
Here is the code for it:
class CustomActivityIndicatorView: UIView {
// MARK - Variables
var colors = [UIColor.greenColor(),UIColor.grayColor(),UIColor.blueColor(),UIColor.redColor()]
var colorIndex = 0
var animation: CABasicAnimation!
lazy var customView : UIView! = {
let frame : CGRect = CGRectMake(0.0, 0.0, 100, 100)
let view = UIView(frame: frame)
image.frame = frame
image.center = view.center
view.backgroundColor = UIColor.greenColor()
view.clipsToBounds = true
view.layer.cornerRadius = frame.width/2
return view
}()
var isAnimating : Bool = false
var hidesWhenStopped : Bool = true
var from: NSNumber = 1.0
var to: NSNumber = 0.0
var growing = false
override func animationDidStart(anim: CAAnimation!) {
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
growing = !growing
if growing {
colorIndex++
if colorIndex == colors.count {
colorIndex = 0
}
println(colorIndex)
customView.backgroundColor = colors[colorIndex]
from = 0.0
to = 1.0
} else {
from = 1.0
to = 0.0
}
if isAnimating {
addPulsing()
resume()
}
}
// MARK - Init
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(customView)
addPulsing()
pause()
self.hidden = true
}
// MARK - Func
func addPulsing() {
let pulsing : CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
pulsing.duration = 0.4
pulsing.removedOnCompletion = false
pulsing.fillMode = kCAFillModeForwards
pulsing.fromValue = from
pulsing.toValue = to
pulsing.delegate = self
let layer = customView.layer
layer.addAnimation(pulsing, forKey: "pulsing")
}
func pause() {
let layer = customView.layer
let pausedTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
isAnimating = false
}
func resume() {
let layer = customView.layer
let pausedTime : CFTimeInterval = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
layer.beginTime = timeSincePause
isAnimating = true
}
func startAnimating () {
if isAnimating {
return
}
if hidesWhenStopped {
self.hidden = false
}
resume()
}
func stopAnimating () {
let layer = customView.layer
if hidesWhenStopped {
self.hidden = true
}
pause()
layer.removeAllAnimations()
}
deinit {
println("Spinner Deinitied")
}
}
Regarding animationDidStop method:
The idea is the following. The view pulsates, and after it has shrunk, it starts growing again and the background color is changed.
Any idea on what I'm doing wrong?
Solved it using CAKeyFrameAnimation to achieve the same effect. For everybody with the same problem, remember that animationDidStart and animationDidStop start running on the main thread, so that whatever you do with your animation there will be halted if the main thread is busy.

Resources