Teeps

How to Architect Your iOS App

27 March 2015 by James Smith

Novice iOS developers often fall into the trap of placing too much functionality in the AppDelegate. It’s convenient to add functionality there because it’s a globally accessible instance that comes for free. Eventually the app delegate becomes bloated with functionality that belongs in other classes. Below I’ll outline an architecture that alleviates this problem and isolates the different behaviors previously held by the app delegate. I didn’t invent it - I took the ideas that Gordon and Mark described in their podcast, Build Phase, and built upon them.

It all starts with an AppController

    class AppController {
      public let viewController = AppViewController()
    }

    class AppViewController: UIViewController {

    }

The app will use a single AppController instance which holds a reference to an AppViewController instance. This view controller serves as the root view controller for your entire application. All other view controllers will be added as children.

In order for your app to use these classes, you can wire up your app delegate like so:

    // AppDelegate.swift
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
      window = UIWindow(frame: UIScreen.mainScreen().bounds)
      window?.rootViewController = appController.viewController
      appController.viewController.view.frame = window?.bounds ?? CGRectZero
      window?.makeKeyAndVisible()
      return true
    }

appController is just a property on your AppDelegate:

    // AppDelegate.swift
    class AppDelegate: UIResponder, UIApplicationDelegate {
      var window: UIWindow?
      let appController = AppController()
    ...

Now your app delegate is complete and your application logic can reside in the AppController. The AppController is a simple construct with the sole purpose of delegating tasks to other specialized components. For example, the responsibility of managing the view hierarchy is delegated to the AppViewController.

User Authentication

The real value of this approach is the ease in which you can negotiate the different states of your application. For example, many apps have a login and sign up flow which is separate from the main application. To achieve this, your AppController can query the local store to see if the app has previously cached a user session and then tell the AppViewController what it should display.

    // AppController.swift
    override public init() {
      super.init()

      if let user = userFromKeychain() { viewController.transitionToMainInterfaceWithUser(user) }
      else { viewController.transitionToLoginInterface() }
    }

If the user is already authenticated, because she previously logged in and her session was cached, we can proceed straight to the main app interface. Otherwise, we tell our AppViewController to display the login interface.

Our `AppViewController` instance is responsible for handling transitions between UI flows. Here’s what a basic implementation might look like.

    public func transitionToLoginInterface() {
      transitionToViewController(LoginViewController.create(), direction: .Left)
    }

    public func transitionToMainInterfaceWithUser(user: User) {
      let rootViewController = SomeViewController.createWithUser(user)
      transitionToViewController(rootViewController, direction: .Right)
    }

Inside the transitionToViewController method is the animation logic to transition between two child view controllers (or to add a child view controller if there are no children).

We can even extend our AppController further to support logging in and logging out.

Let’s define a new class, AuthenticationController, which is responsible for handling login and logout actions.

    // AuthenticationController.swift
    class AuthenticationController: NSObject {
      var onLogin: (User -> Void)?
      var onLogout: (() -&gt; Void)?</code></pre>

      override public init() {
        super.init()
        beginListeningForLoginNotifications()
      }

      deinit { endListeningForLoginNotifications() }

      func beginListeningForLoginNotifications() {
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "userDidLogin:", name: UserLoggedInNotification, object: nil)

        NSNotificationCenter.defaultCenter().addObserver(self, selector: "userDidLogout:", name: UserLoggedOutNotification, object: nil)
      }

      func endListeningForLoginNotifications() {
        NSNotificationCenter.defaultCenter().removeObserver(self, name: UserLoggedInNotification, object: nil)

        NSNotificationCenter.defaultCenter().removeObserver(self, name: UserLoggedOutNotification, object: nil)
      }

      func userDidLogin(notification: NSNotification) {
        if let user: User = (notification.object as? Box&lt;User&gt;)?.value {
          saveUserToKeychain(user)
          onLogin?(user)
        }
      }

      func userDidLogout(_: NSNotification) {
        clearKeychain()
        onLogout?()
      }
    }

The AuthenticationController simply listens for authentication notifications and alerts interested parties via the onLogin and onLogout closures.

    // AppController.swift
    ...
    let authenticationController = AuthenticationController()

    override public init() {
      super.init()

      if let user = userFromKeychain() { viewController.transitionToMainInterfaceWithUser(user) }
      else { viewController.transitionToLoginInterface() }

      authenticationController.onLogin = userDidLogin
      authenticationController.onLogout = userDidLogout
    }

      func userDidLogin(user: User) { viewController.transitionToMainInterfaceWithUser(user) }
      func userDidLogout() { viewController.transitionToLoginInterface() }

In our AppController we can create a new AuthenticationController and tell our AppViewController to perform transitions based on authentication callbacks.

Now when a user completes the login flow and creates an account in the backend a notification can be fired and the AuthenticationController will alert the AppController to tell the AppViewController what to display.

This architecture creates a foundation for the entire application. It can be extended with more components such as a push notification handler or an in-app error message handler. In addition, the login flow is now completely isolated from the main app interface.

If you’d like to provide feedback or would like to share your favorite design patterns hit me up on Twitter @jmecsmith. Thanks for reading!

Let's build your next big thing.

Contact Us