David Chopin
David Chopin

Reputation: 3064

CABasicAnimation Sometimes Works, Sometimes Fails

I am making an app that walks the user through account creation in a series of steps. After each step is completed, the user is taken to the next view controller and a progress bar animates across the top of the screen to communicate how much of the account making process has been completed. Here is the end result:

enter image description here

This is accomplished by placing a navigation controller within a containing view. The progress bar is laid over the containing view and every time the navigation controller pushes a new view controller, it tells the containing view controller to animate the progress bar to a certain percentage of the superview's width. This is done through the following updateProgressBar function.

import UIKit

class ContainerVC: UIViewController {
    var progressBar: CAShapeLayer!

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        progressBar = CAShapeLayer()
        progressBar.bounds = CGRect(x: 0, y: 0, width: 0, height: view.safeAreaInsets.top)
        progressBar.position = CGPoint(x: 0, y: 0)
        progressBar.anchorPoint = CGPoint(x: 0, y: 0)
        progressBar.backgroundColor = UIColor.secondaryColor.cgColor
        view.layer.addSublayer(progressBar)
    }

    func updateProgressBar(to percentAsDecimal: CGFloat!) {
        let newWidth = view.bounds.width * percentAsDecimal
        CATransaction.begin()
        CATransaction.setCompletionBlock({
            self.progressBar.bounds.size.width = newWidth
        })
        CATransaction.commit()
        let anim = CABasicAnimation(keyPath: "bounds")
        anim.isRemovedOnCompletion = true
        anim.duration = 0.25
        anim.fromValue = NSValue(cgRect: CGRect(x: 0, y: 0, width: progressBar.bounds.width, height: view.safeAreaInsets.top))
        anim.toValue = NSValue(cgRect: CGRect(x: 0, y: 0, width: newWidth, height: view.safeAreaInsets.top))
        progressBar.add(anim, forKey: "anim")
    }
}

The view controllers in the navigation controller's stack will call this updateProgressBar function when pushing the next VC. This is done like so:

class FourthViewController: UIViewController {

    var containerVC: ContainerViewController!

    ...

    @IBAction func nextButtonPressed(_ sender: Any) {
        let storyboard = UIStoryboard(name: "Main", bundle: .main)
        let fifthVC = storyboard.instantiateViewController(withIdentifier: "FifthVC") as! FifthViewController
        fifthVC.containerVC = containerVC
        navigationController!.pushViewController(fifthVC, animated: true)
        //We pass 5/11 because the next step will be step 5 out of 11 total steps
        self.containerVC.updateProgressBar(to: 5/11)
    }
}

Similarly, when pressing the back button, we shrink the container VC's progress bar:

class FourthViewController: UIViewController {

    var containerVC: ContainerViewController!

    ...

    @IBAction func backButtonPressed(_ sender: Any) {
        navigationController!.popViewController(animated: true)

        //We pass 3/11 because the previous step is step 3 out of 11 total steps
        containerVC.updateProgressBar(to: 3/11)
    }
}

My problem is that this animation only sometimes works. The progress bar always works when moving forward in the process, but sometimes, when a user navigates back, the bar gets stuck and will no longer move in either direction until an unreached view controller is presented. See the video below:

Video of Bug (Bug begins around 0:23)

I have confirmed that the presentation of an Alert Controller is not the cause of the failure to animate, and have also made sure that the animation is occurring on the main thread. Any suggestions?

Upvotes: 2

Views: 250

Answers (1)

Fabrizio Scarano
Fabrizio Scarano

Reputation: 106

As very well explained in this answer here, viewDidLayoutSubviews() gets called more than once.

In your case, you're ending up instatiating a new CAShapeLayer every time you push or pop a view controller from the navigation stack.

Try using viewDidAppear() instead.

Upvotes: 4

Related Questions