CABasicAnimation responsiveness - ios

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.

Related

Incorrect UIViewPropertyAnimator behaviour when called recursively and app is in background

I have created a very simple class (SpinningCircleView) of type UIView that performs a spinning circle animation forever. I want to call this class from my ViewController to display the spinning circle animation on the screen. While the animation works great, I am observing incorrect behavior when the app is put in the background. Here is what the spinning circle animation looks like:
To create the spinning circle, I am using two separate UIViewPropertyAnimator, one to rotate the circle by 180 degrees (i.e. Pi) and the other to complete to 360 (i.e. 0 degrees). In the completion block of the second animator, I am then recursively calling the startSpinningCircleAnimation() function. I created a counter to track the number of times the startSpinningCircleAnimation() is called (i.e. the number of times the circle has rotated). While the app is active (in the foreground), the counter increments as expected and I can see the output in my Xcode terminal window:
Starting animation
1: + START Spinning Circle Animation
2: + START Spinning Circle Animation -> recursive call
3: + START Spinning Circle Animation -> recursive call
4: + START Spinning Circle Animation -> recursive call
The problem or incorrect behavior happens is when I put the app into the background...all of a sudden I am seeing several hundred "+ START Spinning Circle Animation -> recursive call" in my terminal. When the app comes back to the foreground, the counter and terminal output resume normal increments of the counter.
Why are several hundred calls being made to the startSpinningCircleAnimation() function when the app is put in the background? How can I correctly pause the animation and resume the animation as the app is moved between background and foreground? I have scoured through various posts but I can't figure out the solution.
Please help!!
Here is my SpinningCircleView class:
import UIKit
class SpinningCircleView: UIView
{
private lazy var spinningCircle = CAShapeLayer()
private lazy var animator1 = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: nil)
private lazy var animator2 = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: nil)
public var counter = 0
override init (frame: CGRect)
{
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
private func configure()
{
frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let rect = self.bounds
let circularPath = UIBezierPath(ovalIn: rect)
spinningCircle.path = circularPath.cgPath
spinningCircle.fillColor = UIColor.clear.cgColor
spinningCircle.strokeColor = UIColor.systemRed.cgColor
spinningCircle.lineWidth = 10
spinningCircle.strokeEnd = 0.25
spinningCircle.lineCap = .round
self.layer.addSublayer(spinningCircle)
}
func startSpinningCircleAnimation()
{
counter += 1
let criteria1 = animator1.state == .active && !animator1.isRunning
let criteria2 = animator2.state == .active && !animator2.isRunning
let criteria3 = animator1.state == .inactive && animator2.state == .inactive
let criteria4 = (animator1.state == .inactive && animator2.state.rawValue == 5) || (animator2.state == .inactive && animator1.state.rawValue == 5)
if (criteria1)
{
// Since animator1 is Paused, we will resume the animation
print("\(self.counter): ~ RESUME Spinning Circle Animation")
animator1.startAnimation()
} else if (criteria2)
{
// Since animator2 is Paused, we will resume the animation
print("\(self.counter): ~ RESUME Spinning Circle Animation")
animator2.startAnimation()
} else if (criteria3 || criteria4)
{
if (criteria3)
{
print("\(self.counter): + START Spinning Circle Animation")
} else if (criteria4)
{
print("\(self.counter): + START Spinning Circle Animation -> recursive call")
}
animator1.addAnimations
{
self.transform = CGAffineTransform(rotationAngle: .pi)
}
animator1.addCompletion
{ _ in
self.animator2.addAnimations
{
self.transform = CGAffineTransform(rotationAngle: 0)
}
self.animator2.addCompletion
{ _ in
// Recursively call this start spinning
self.startSpinningCircleAnimation()
}
self.animator2.startAnimation()
}
animator1.startAnimation()
} else
{
print("\(self.counter): >>>>>>>>> HERE <<<<<<<<<<< \(self.animator1.state) \(self.animator1.isRunning) \(self.animator2.state) \(self.animator2.isRunning)")
}
}
func stopSpinningCircleAnimation()
{
print("\(self.counter): - STOP Spinning Circle Animation Begin: \(self.animator1.state) \(self.animator1.isRunning) \(self.animator2.state) \(self.animator2.isRunning)")
if (self.animator1.isRunning)
{
self.animator1.pauseAnimation()
} else if (self.animator2.isRunning)
{
self.animator2.pauseAnimation()
}
print("\(self.counter): - STOP Spinning Circle Animation End: \(self.animator1.state) \(self.animator1.isRunning) \(self.animator2.state) \(self.animator2.isRunning)")
}
}
Here is my ViewController which sets up an instance of the SpinningCircleView and starts animating:
class ViewController: UIViewController
{
private lazy var spinningCircleView = SpinningCircleView()
override func viewDidLoad()
{
super.viewDidLoad()
// Setup the spinning circle and display the animation to the screen
spinningCircleView.frame = CGRect(x: view.center.x - 50, y: 100, width: 100, height: 100)
spinningCircleView.tag = 100
view.addSubview(spinningCircleView)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
print("Starting animation")
spinningCircleView.startSpinningCircleAnimation()
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
print("Pausing animation")
spinningCircleView.stopSpinningCircleAnimation()
}
}
I will answer each of your question
Why are several hundred calls being made to the startSpinningCircleAnimation() function when the app is put in the background?
I think because every time you called startSpinningCircleAnimation() in completion and you check state which is inactive, ... of animation before. The things which is wrong here maybe is in the logic of check state in your recursion.
I will not deep dive into your code to find the wrong one. But I will refactor your SpinningCircleView
First of all, don't need to use two UIViewPropertyAnimator just for making the view rotate continuously. Simple logic here is just make the view to be rotate and repeat it.
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = Double.pi * 2
rotation.duration = 1
rotation.isCumulative = true
rotation.repeatCount = .greatestFiniteMagnitude
rotation.isRemovedOnCompletion = false
self.layer.add(rotation, forKey: "rotateInfinityAnimation")
And for your second question
How can I correctly pause the animation and resume the animation as the app is moved between background and foreground?
You just need to catch the notification where your app will enter foreground or background from NotificationCenter.
private func addNotification() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(appEnterForground), name: UIApplication.didBecomeActiveNotification, object: nil)
}
For stop and resume animation, you just need to know that when animation happens the layer of the view is the one which occurs the animation. So you just need to add function to stop and resume on the layer of the view. Then every time you want to pause or resume just call from view.layer.resume() or view.layer.stop()
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
}
}
Your SpinningCircleView will be like this
class SpinningCircleView: UIView {
private lazy var spinningCircle = CAShapeLayer()
private var didStopAnimation = false
override init (frame: CGRect)
{
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
private func configure()
{
didStopAnimation = false
frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let rect = self.bounds
let circularPath = UIBezierPath(ovalIn: rect)
spinningCircle.path = circularPath.cgPath
spinningCircle.fillColor = UIColor.clear.cgColor
spinningCircle.strokeColor = UIColor.systemRed.cgColor
spinningCircle.lineWidth = 10
spinningCircle.strokeEnd = 0.25
spinningCircle.lineCap = .round
self.layer.addSublayer(spinningCircle)
self.addNotification()
}
private func addNotification() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(appEnterForeground), name: UIApplication.didBecomeActiveNotification, object: nil)
}
#objc func appEnterForeground() {
if didStopAnimation {
return
}
self.layer.resume()
}
#objc func appMovedToBackground() {
if didStopAnimation {
return
}
self.layer.pause()
}
func startSpinningCircleAnimation() {
if didStopAnimation {
return
}
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = Double.pi * 2
rotation.duration = 1
rotation.isCumulative = true
rotation.repeatCount = .greatestFiniteMagnitude
rotation.isRemovedOnCompletion = false
self.layer.add(rotation, forKey: "rotateInfinityAnimation")
}
func stopSpinningCircleAnimation() {
didStopAnimation = true
self.layer.removeAllAnimations()
}
}

Not all pixels clipped when using UIView.layer.clipsToBounds = true in conjunction with CABasicAnimation

What I'm doing:
I am creating a music app. Within this music app, I have a music player that links with Apple Music and Spotify. The music player displays the current song's album art on a UIImageView. Whenever the song is playing, the UIImageView rotates (like a record on a record player).
What my problem is:
The album art for the UIImageView is square by default. I am rounding the UIImageView's layer's corners and setting UIImageView.clipsToBounds equal to true to make it appear as a circle.
Whenever the UIImageView rotates, some of the pixels outside of the UIImageView's layer (the part that is cut off after rounding the image) are bleeding through.
Here is what the bug looks like: https://www.youtube.com/watch?v=OJxX5PQc7Jo&feature=youtu.be
My code:
The UIImageView is rounded by setting its layer's cornerRadius equal to UIImageView.frame.height / 2 and setting UIImageView.clipsToBounds = true:
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.clipsToBounds = true
//I've also tried the following code, and am getting the same behavior:
/*
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.layer.masksToBounds = true
albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
*/
}
}
Whenever a button is pressed, the UIImageView begins to rotate. I've used the following extension to make UIView's rotate:
extension UIView {
func rotate(duration: Double = 1, startPoint: CGFloat) {
if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = startPoint
rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
layer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
}
}
}
I also have the following extension to end the rotation:
extension UIView {
...
func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
if layer.animation(forKey: UIView.kRotationAnimationKey) != nil {
let animation = layer.animation(forKey: UIView.kRotationAnimationKey)!
let elapsedTime = CACurrentMediaTime() - beginTime
let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration
layer.transform = CATransform3DMakeRotation((CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle, 0.0, 0.0, 1.0)
layer.removeAnimation(forKey: UIView.kRotationAnimationKey)
return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
} else {
return nil
}
}
}
This is how these extension functions are used in the context of my view controller:
class MyViewController: UIViewController {
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
...
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}
So, all together, MyViewController looks something like this:
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.clipsToBounds = true
}
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}
I copy your code into the project and I can reproduce this issue. But if you add the animation to another CALayer, that seems to resolve the issue.
extension UIView {
static var kRotationAnimationKey: String {
return "kRotationAnimationKey"
}
func makeAnimationLayer() -> CALayer {
let results: [CALayer]? = layer.sublayers?.filter({ $0.name ?? "" == "animationLayer" })
let animLayer: CALayer
if let sublayers = results, sublayers.count > 0 {
animLayer = sublayers[0]
}
else {
animLayer = CAShapeLayer()
animLayer.name = "animationLayer"
animLayer.frame = self.bounds
animLayer.contents = UIImage(named: "imageNam")?.cgImage
layer.addSublayer(animLayer)
}
return animLayer
}
func rotate(duration: Double = 1, startPoint: CGFloat) {
if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
let animLayer = makeAnimationLayer()
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = startPoint
rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
animLayer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
}
}
func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
let animLayer = makeAnimationLayer()
if animLayer.animation(forKey: UIView.kRotationAnimationKey) != nil {
let animation = animLayer.animation(forKey: UIView.kRotationAnimationKey)!
let elapsedTime = CACurrentMediaTime() - beginTime
let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration
animLayer.transform = CATransform3DMakeRotation(CGFloat(angle) * (2 * CGFloat.pi) + startingAngle, 0.0, 0.0, 1.0)
animLayer.removeAnimation(forKey: UIView.kRotationAnimationKey)
return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
}
else {
return nil
}
}
Clip to bounds does not include rounded corners. Try using corner masking instead.
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}

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)

Creating raining code Matrix effect

Created a sub class of a CATextLayer within which I attached a fadeIn animation, which I than add to a CATextLayer to which I have attached a dropThru animation. The goal to try and create matrix movie raining code effect. Works reasonably well but for the fact that it slowly but surely drives itself into the ground, I suspect cause I keep adding more and more layers. How can I detect when an layer had left the screen so I may delete it.
Here is the code...
class CATextSubLayer: CATextLayer, CAAnimationDelegate {
private var starter:Float!
private var ender:Float!
required override init(layer: Any) {
super.init(layer: layer)
//UIFont.availableFonts()
self.string = randomString(length: 1)
self.backgroundColor = UIColor.black.cgColor
self.foregroundColor = UIColor.white.cgColor
self.alignmentMode = kCAAlignmentCenter
self.font = CTFontCreateWithName("AvenirNextCondensed-BoldItalic" as CFString?, fontSize, nil)
self.fontSize = 16
self.opacity = 0.0
makeFade()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
func randomString(length: Int) -> String {
let letters : NSString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let len = UInt32(letters.length)
var randomString = ""
for _ in 0 ..< length {
let rand = arc4random_uniform(len)
var nextChar = letters.character(at: Int(rand))
randomString += NSString(characters: &nextChar, length: 1) as String
}
return randomString
}
func makeFade() {
let rands = Double(arc4random_uniform(UInt32(4)))
let fadeInAndOut = CABasicAnimation(keyPath: "opacity")
fadeInAndOut.duration = 16.0;
fadeInAndOut.repeatCount = 1
fadeInAndOut.fromValue = 0.0
fadeInAndOut.toValue = 1
fadeInAndOut.isRemovedOnCompletion = true
fadeInAndOut.fillMode = kCAFillModeForwards;
fadeInAndOut.delegate = self
fadeInAndOut.beginTime = CACurrentMediaTime() + rands
self.add(fadeInAndOut, forKey: "opacity")
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.removeAllAnimations()
}
}
With the outer loop/View Controller ..
class ViewController: UIViewController, CAAnimationDelegate {
var beeb: CATextSubLayer!
var meeb: CATextLayer!
var lines = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
// Do any additional setup after loading the view, typically from a nib.
meeb = CATextLayer()
for bing in stride(from:0, to: Int(view.bounds.width), by: 16) {
lines.append(bing)
}
for _ in 0 ..< 9 {
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(makeBeeb), userInfo: nil, repeats: true)
}
}
func makeBeeb() {
let rands = Double(arc4random_uniform(UInt32(4)))
let beeb = CATextSubLayer(layer: meeb)
let randx = Int(arc4random_uniform(UInt32(lines.count)))
let monkey = lines[randx]
beeb.frame = CGRect(x: monkey, y: 0, width: 16, height: 16)
let dropThru = CABasicAnimation(keyPath: "position.y")
dropThru.duration = 12.0;
dropThru.repeatCount = 1
dropThru.fromValue = 1
dropThru.toValue = view.bounds.maxY
dropThru.isRemovedOnCompletion = true
dropThru.fillMode = kCAFillModeForwards;
dropThru.beginTime = CACurrentMediaTime() + rands
dropThru.delegate = self
beeb.add(dropThru, forKey: "position.y")
self.view.layer.addSublayer(beeb)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.view.layer.removeAllAnimations()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
As far as I understand your code you can remove the layer, when it's position animation ends. In this moment it should have left the bounds of the parent view.
Btw. Removing and adding layers costs performance. Instead removing you should reuse the layer for the next animation.
How can I detect when an layer had left the screen so I may delete it
You have already given the CABasicAnimation a delegate which is called when the animation finishes. That is your signal to remove the layer. (You are removing animations but not the layer itself.)

Re-Animate Gradient Background with different color in swift in ios

I want to re-animate gradient background with different color when animating.
I can successfully animate gradient color with this code.
let dayTopColor = CommonUtils.colorWithHexString("955EAC")
let dayBottomColor = CommonUtils.colorWithHexString("9F3050")
let dayToTopColor = CommonUtils.colorWithHexString("D15B52")
let dayToBottomColor = CommonUtils.colorWithHexString("CC4645")
let nightTopColor = CommonUtils.colorWithHexString("2D5E7C")
let nightBottomColor = CommonUtils.colorWithHexString("19337D")
let nightToTopColor = CommonUtils.colorWithHexString("21334E")
let nightToBottomColor = CommonUtils.colorWithHexString("101A55")
var isInSaudiArabia = false
var gradient : CAGradientLayer?
var toColors : AnyObject?
var fromColors : AnyObject?
func animateBackground(){
var layerToRemove: CAGradientLayer?
for layer in self.view.layer.sublayers!{
if layer.isKindOfClass(CAGradientLayer) {
layerToRemove = layer as? CAGradientLayer
}
}
layerToRemove?.removeFromSuperlayer()
self.gradient!.colors = [nightTopColor.CGColor, nightBottomColor.CGColor]
self.toColors = [nightToTopColor.CGColor, nightToBottomColor.CGColor]
self.view.layer.insertSublayer(self.gradient!, atIndex: 0)
animateLayer()
}
func animateLayer(){
self.fromColors = self.gradient!.colors!
self.gradient!.colors = self.toColors as? [AnyObject]
let animation : CABasicAnimation = CABasicAnimation(keyPath: "colors")
animation.delegate = self
animation.fromValue = fromColors
animation.toValue = toColors
animation.duration = 3.50
animation.removedOnCompletion = true
animation.fillMode = kCAFillModeForwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.delegate = self
self.gradient!.addAnimation(animation, forKey:"animateGradient")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.toColors = self.fromColors;
self.fromColors = self.gradient!.colors!
animateLayer()
}
CommonUtils.colorWithHexString() is a function which converts hex color to UIColor.
Btw when i try to change background color to day color while animating, background gradient color got flickered.
Is there anybody who knows solution.
The problem is that when you remove the layer, it stops the animation. But when the animation stops, animationDidStop is still getting called, which is starting a new animation, itself. So, you're removing the layer, which stops the animation, immediately starts another, but you're then starting yet another animation. You have dueling animations.
You can check flag to see if the animation finished properly before animationDidStop should call animateLayer.
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if flag {
toColors = fromColors;
fromColors = gradient!.colors!
animateLayer()
}
}
Personally, I'm not sure why you're removing and adding and removing the layer. And if you were, I'm not sure why you don't just gradient?.removeFromSuperlayer() rather than iterating through the layers.
Regardless, I'd just keep the gradient layer there, just check its presentationLayer and start the animation from there:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fromColors = [dayTopColor.CGColor, dayBottomColor.CGColor]
toColors = [dayToTopColor.CGColor, dayToBottomColor.CGColor]
gradient = CAGradientLayer()
gradient!.colors = fromColors!
gradient!.frame = view.bounds
view.layer.addSublayer(gradient!)
animateLayer()
}
let dayTopColor = CommonUtils.colorWithHexString("955EAC")
let dayBottomColor = CommonUtils.colorWithHexString("9F3050")
let dayToTopColor = CommonUtils.colorWithHexString("D15B52")
let dayToBottomColor = CommonUtils.colorWithHexString("CC4645")
let nightTopColor = CommonUtils.colorWithHexString("2D5E7C")
let nightBottomColor = CommonUtils.colorWithHexString("19337D")
let nightToTopColor = CommonUtils.colorWithHexString("21334E")
let nightToBottomColor = CommonUtils.colorWithHexString("101A55")
var gradient : CAGradientLayer?
var toColors : [CGColor]?
var fromColors : [CGColor]?
var day = true
func toggleFromDayToNight() {
day = !day
if day {
fromColors = [dayTopColor.CGColor, dayBottomColor.CGColor]
toColors = [dayToTopColor.CGColor, dayToBottomColor.CGColor]
} else {
fromColors = [nightTopColor.CGColor, nightBottomColor.CGColor]
toColors = [nightToTopColor.CGColor, nightToBottomColor.CGColor]
}
let colors = (gradient!.presentationLayer() as! CAGradientLayer).colors // save the in-flight current colors
gradient!.removeAnimationForKey("animateGradient") // cancel the animation
gradient!.colors = colors // restore the colors to in-flight values
animateLayer() // start animation
}
func animateLayer() {
let animation : CABasicAnimation = CABasicAnimation(keyPath: "colors")
animation.fromValue = gradient!.colors
animation.toValue = toColors
animation.duration = 3.50
animation.removedOnCompletion = true
animation.fillMode = kCAFillModeForwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.delegate = self
gradient!.colors = toColors
gradient!.addAnimation(animation, forKey:"animateGradient")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if flag {
swap(&toColors, &fromColors)
animateLayer()
}
}
#IBAction func didTapButton() {
toggleFromDayToNight()
}
}

Resources