Once in a while, I get errors like:
Warning: Attempt to present <Controller3> on <Controller1> which is already presenting <Controller2>
I understand that the next controller needs to be presented on the controller that's at the top of the stack (Controller2), not a controller somewhere below (Controller1).
Instead of fixing such errors one-off, how do we design our app to prevent this problem once and for all?
One clean solution to this problem is a navigation controller.
If you can't or don't want to use one, you can easily simulate it on a normal view controller:
extension UIViewController {
var topViewController: UIViewController {
return presentedViewController == nil ? self : presentedViewController!.topViewController
}
// If the topmost view controller is an instance of one of the given classes, it's popped.
// Then, the given view controller, if any, if pushed.
//
// This function can be called on any of the view controllers in the stack.
func pop(ifOneOf: [AnyClass], thenPush: UIViewController? = nil) {
if topViewController.presentingViewController != nil && topViewController.isKindOfOneOf(ifOneOf) {
topViewController.dismiss(animated: false, completion: {
self.pop(ifOneOf: [], thenPush: thenPush)
})
return
}
if thenPush != nil {
push(thenPush!)
}
}
// Pushes the given view controller onto the stack.
//
// This method can be called on any of the view controllers in the stack.
func push(_ child: UIViewController) {
topViewController.present(child, animated: true)
}
}
extension NSObjectProtocol {
func isKindOfOneOf(_ classes: [AnyClass]) -> Bool {
for clazz in classes {
if isKind(of: clazz) {
return true
}
}
return false
}
}
As you can see, this provides push() and pop(), similar to a navigation controller. Further, you can call these methods on any controller in the stack, and it will automatically redirect them to the topmost controller, preventing the error in the question.
This extension also fixes the problem that if you want to dismiss a controller and present another, you need to present only in the completion block, even if you're dismissing without an animation. Otherwise, you'll get the same error as above. This extension fixes all these problems.