Recently I was playing with an ‘Product-Order’ app based on tab bar controller. The root controller of the application is a tab bar controller with 4 tabs (You can find this kind of app anywhere).

However, some pages require login check, which means only the logged in user can process to the next step. One example is when you want to order a product, you will need to login to the application first, then process to the order detail page.

In my app’s product detail page, there is an order button. Once you click it, it will check if the user has logged in. If yes, the user can proceed to the order page; otherwise, a login prompt will pop up.

Here is the problem I occured: after user enter their credentials, how to dismiss the login prompt, then process to the next step?

Problem: After log in, dismiss login page, and push the next view controller into the navigation stack.

Background:

  1. The login page is presented as a Modal Controller
  2. My controller hierarchy is like this:
    Root Tab Bar Controller
    –Navigation Controller
    –Product Detail (Top in the navigation stack)
    —Navigation Controller (Present Modally, root is Login VC)
    —Login View Controller

Approach
Common solution is using dismissViewControllerAnimated:completion:

Firstly find out the controller that is presenting this Login View Controller, then present what you want next in the completion closure.

1
2
3
4
5
// in login view controller, after clicking login, if login successfully:
let vc = self.presentingViewController
self.dismissViewControllerAnimated(true, completion: {
vc.presentViewController.... // present whatever you want
})

However, in my case, there are two issues with it.

  1. the Login View controller’s presenting view controller is not the Product Detail View Controller!
    When you print out the presenting view controller, you will get something like <UITabBarController: 0x7fdde2564970>, which is the tab bar controller rather than the Product Detail View Controller.

  2. Many pages require a login check, so which view controller will be presented and who will present it in the closure is not fixed. Therefore we cannot hardcode it in the Login View Controller.

My Solution

  1. Since the presenting vc is the tab bar controller, we could find out its selectedViewController, which is the navigation presented by the root tab bar controller itself. Then find out its topViewController, which will be the Product Detail Controller we want.

  2. Define a protocol for every view controller that may present this login page, named it AfterLoginAction. It contains a method afterLogin to be implemented. Then we could define the specific view controller we want to present after successful login without affecting generality。

    1
    2
    3
    protocol AfterLoginAction {
    func afterLogin()
    }

Therefore

Let Product Detail Controller conform the protocol, and implement it as

1
2
3
4
5
extension ProductDetailViewController : AfterLoginAction {
func afterLogin() {
self.navigationController!.pushViewController(ProductOrderViewController(), animated: true)
}
}

Then in the Login View Controller, just cast the view controller as an object conforming to AfterLoginAction protocol, and make the call:

1
2
3
4
5
6
let vc = self.presentingViewController as! UITabBarController
let parentVC = vc.selectedViewController as! UINavigationController
let previousVC = parentVC.topViewController as! AfterLoginAction
self.dismissViewControllerAnimated(true, completion: {
previousVC.afterLogin()
})

Here is the final effect achieved :D

This is definitely not the best solution, but it is working well so far. Another lesson learnt :)