Reputation: 3064
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:
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
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