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()
}
}