Celebrating 10 Years!

profile picture

Gateway View Controller: A Pattern for handling app login

June 14, 2015 - Roundwall Software

As Apple recommends in their Interface Guidelines, apps should try provide functionality without requiring a login. Ideally, an app would only ask for your login information when you get to features in the app that does need it.

Unfortunately, there are a great many apps that simply lock you out of the entire app until you have successfully logged in. Sometimes this is even necessary: like if you are making a private-messaging app. For the times when this is necessary, I'd like to explain a pattern to implement this.

Your main entry point to your app will now be a special controller I call the Gateway View Controller. It will be the parent view controller of all others in your app. It will need a few handy functions:

private func embedFullScreenController(controller: UIViewController){
        controller.willMoveToParentViewController(self)
        controller.view.frame = view.bounds
        view.addSubview(controller.view)
        addChildViewController(controller)
    }

Embedding a child view controller takes a few steps, so instead of typing them every time, why not have a function for it? Because this controller will leave the job of presenting any UI to it's children, we can just assume the embedded controller needs to take up the full frame of the gateway. If you wanted to be fancy, this would be your opportunity to add some fade-out or page-flip animation. I'll leave that up to you.

We'll also need a function for removing old children. Our Gateway won't stack up a pile of controllers, we want only one direct child at a time.

private func removeOldChildren() {
        for controller in childViewControllers as! [UIViewController] {
            controller.willMoveToParentViewController(nil)
            controller.removeFromParentViewController()
        }
    }

Next, we'll need something to represent the idea that your user is logged in. I chose a simple little enum, but your implementation will likely involve something that holds a bit more information:

enum LoginStatus {
    case LoggedOut
    case LoggedIn(apiKey: String)
}

Either the app is logged out or the app is logged in. If it is logged in, my example app remembers the user's API key that should be used in server requests while the app is running. This ability to attach some information to an enum case is one of my favorite Swift features.

Next we'll need a method to determine which controller should be shown based on our login status:

private func showAppropriateController() {
        removeOldChildren()
        
        switch status {
        case .LoggedOut:
            let controller = storyboard?.instantiateViewControllerWithIdentifier("loggedOut") as? LoggedOutViewController
            controller?.gatewayViewController = self
            map(controller, embedFullScreenController)
        case .LoggedIn(let apiKey):
            let controller = storyboard?.instantiateViewControllerWithIdentifier("loggedIn") as? LoggedInViewController
            controller?.status = self.status
            map(controller, embedFullScreenController)
        }
    }

If the app is logged in, we want to show something from the app's main UI and we should send along our information about the logged-in status of the app. That way the controller can use the API key we have stored for its business. If the app is not logged in, we want to show some login screen. The login screen needs a reference to the gateway controller much like all view controllers have a reference to their parent navigation or tab controllers. Otherwise we just use our embed function we wrote and we're all set.

This controller-showing function will get triggered in two ways:

var status = LoginStatus.LoggedOut {
        didSet {
            showAppropriateController()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        showAppropriateController()
    }

We'll want to show something as soon as your app loads, but also we'll want to change screens whenever the app's login status changes. Easy peasy.

Last thing we need to add is an event to make the app's login status change. Our login-screen controller is going to do some magic with a user's username and password and decide when we're all set. It will simply need to inform the gateway controller when it's done:

class LoggedOutViewController: UIViewController {
    var gatewayViewController: GatewayViewController?
    
    @IBAction func login() {
        // Assume login magically worked just fine.
        gatewayViewController?.status = .LoggedIn(apiKey: "someimportantapikey")
    }
}

There you have it! We've encapsulated the idea of deciding what gets shown based on login to it's own controller. Just like UIKit provides us UINavigationController which is responsible for a trail of view controllers and UITabViewController which handles toggling between view controllers based on tab bar buttons, we've created a parent controller that toggles between controllers based on login status.

This same kind of idea could be extended to do things like toggle what is displayed in your app based on network connectivity. This could even be used further down your navigation structure to lock out select portions of your app based on login status (which is way more friendly than locking the whole app).

Source code for this example can be found here

Enjoy!