Application Architecture with Material

Learn how to develop your application's architecture using Material's UI controllers.

Material is packed with many UI controllers, such as: NavigationController, PageTabBarController, ToolbarController, MenuController, SearchBarController, SnackbarController, StatusBarController, BottomNavigationController, and NavigationDrawerController. In this article I will describe how to combine multiple UI controllers, how to approach thinking about your UI in layers, and the potentiality of mixing and matching UI controllers with ease.

To begin Material’s architectural understanding we will explore the RootController. The RootController is a subclass of UIViewController, and adds management of a child view controller called the rootViewController. The RootController acts as the foundation for all UI controllers, with the exception of the NavigationController. The main architectural benefit of the RootController being a view controller itself is that it allows for RootController types to stack themselves in any order through the child rootViewController property.

Each RootController subclass manages a specific UI control, for example: the ToolbarController manages a Toolbar and the MenuController manages a Menu. The stacking behavior of the RootController allows us to stack these controllers in self contained layers that simplify maintenance of code surrounding each control, while giving the user a perception of a complete moving system.

Stacked Controllers

Stacking RootController types is when UI controllers are layered in order to organize code logic and maintain flexibility in a decoupled manner. Take a look at the following code snippets and images to get a sense of this design principle.

For example, let's build our application's architecture with an initial ToolbarController.

import UIKit
import Material

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func applicationDidFinishLaunching(_ application: UIApplication) {
        window = UIWindow(frame: Screen.bounds)
        window!.rootViewController = ToolbarController(rootViewController: ViewController())
        window!.makeKeyAndVisible()
    }
}

The above code snippet sets up a ToolbarController as the rootViewController of the application's UIWindow. The ToolbarController adds it's own rootViewController, which is displayed as the UI interface alongside the Toolbar.

What if we wanted to use another UI control, such as a Menu? We could modify our rootViewController from a ToolbarController to a MenuController in order to get the following:

window!.rootViewController = MenuController(rootViewController: ViewController())

Both of these UI controllers are great individually, and are very effective. What happens when our application demands more, the level of complexity increases and the simple UI controllers become a tricky coordination. In Material, the design principle is to stack the UI controllers by setting one of the RootController types as the rootViewController of the other. Take a look at the following to see how this is done:

window!.rootViewController = MenuController(rootViewController: ToolbarController(rootViewController: ViewController()))

We could have swapped the two UI controllers, like so:

window!.rootViewController = ToolbarController(rootViewController: MenuController(rootViewController: ViewController()))

The look would be the same, with a major difference to consider. When the MenuController is wrapping the ToolbarController, transitioning the ToolbarController's rootViewController property will keep the MenuController visible and available for any sub view controller entering the view hierarchy. If we placed the MenuController as the ToolbarController's rootViewController property, when we transition to a new view controller, the MenuController will be removed and no longer visible or available in the view hierarchy. Depending on the navigational flow your application requires, this change, although minor, would have a great impact. The same is true for the MenuController. If the MenuController has it's rootViewController transitioned to another view controller, the ToolbarController will be removed. The following section describes a solution to hierarchical transitions.

Hierarchical Transitions

A hierarchical transition is a RootController transition that is executed from a parent or child RootController within a stack of RootControllers. There are two directions to consider, transitioning a child RootController, and transitioning a parent RootController.

Transition Child RootController

To transition a child RootController within a stack of RootControllers, cast the rootViewController property to the child type and use the transition method. For example, to transition a ToolbarController's rootViewController contained within a MenuController, place the following code within a subclass of the MenuController.

class AppMenuController: MenuController {
    @objc
    fileprivate func handleMenuItemClick() {
        (rootViewController as? ToolbarController)?.transition(to: "MyNewViewController")
    }
}

Transition Parent RootController

To transition a parent RootController that is in a stack of RootControllers, call the appropriate controller property and use the transition method. For example, to transition a MenuController's rootViewController that wraps a ToolbarController, place the following code within the ToolbarController's rootViewController, or a subclass of the ToolbarController.

class AppToolbarController: ToolbarController {
    @objc
    fileprivate func handleButtonClick() {
        menuController?.transition(to: "MyNewViewController")
    }
}

It is common to have 3 or more RootControllers stacked together to solve complex architectural UI designs, while still maintaining code organization. To complete this article, let's look at the RootController properties and functions.

RootController Properties & Functions

rootViewController

The rootViewController is a child view controller of the RootController.

open fileprivate(set) var rootViewController: UIViewController!

isUserInteractionEnabled

The isUserInteractionEnabled property manages the rootViewController.view.isUserInteractionEnabled property, which enables and disables the user’s ability to interact with the UI.

open var isUserInteractionEnabled: Bool

transition

The transition function animates from one rootViewController to another.

open func transition(to viewController: UIViewController, duration: TimeInterval, options: UIViewAnimationOptions, animations: (() -> Void)?, completion: ((Bool) -> Void)?)