Applications as State Machines

Applications tend to have a set of states with various transitions between them. Modeling them as finite state machines can help disassemble the problem of building the whole application into several separate smaller parts. This separation can be further improved by maximizing the hermetization of the resulting components. One possible way to reduce coupling between the modules is to abstract out the transition logic into a separate entity, which I call the TransitionController.

The TransitionController‘s job is to manage transitions between the application’s states. Each state is managed by a view controller. Controllers request transitions by telling the transition controller to show a certain view type, optionally providing some context to be handed over to the target view controller.

@protocol TransitionController

- (void)showViewType:(ViewType)viewType withContext:(Context *)context;

@end

ViewType is an enumeration of the core views available in the application. Since it’s possible for the views to get shown in a few different states, there won’t necessarily be a direct mapping between the view types and application states. It is also possible to have contextual view types like ViewTypePrevious and ViewTypeNext, which will trigger different transitions in different states. The context can be an NSDictionary, a custom class, or a protocol implemented by a few different model classes. The target view controller can, and probably should, have an assertion regarding the context it receives.

View controllers can access the TransitionController by going up the view controller hierarchy.

@implementation UIViewController (TransitionController)

- (id<TransitionController>)transitionController
{
    if ([self.containingViewController conformsToProtocol:@protocol(TransitionController)]) {
        return (id<TransitionController>)self.containingViewController;

    } else {
        return self.containingViewController.transitionController;
    }
}

- (UIViewController *)containingViewController
{
    return self.parentViewController ?: self.presentingViewController;
}

@end

The application’s RootViewController is probably the best place to implement the protocol. To do that it has to be able to infer the application’s state and know how to perform transitions based on that state and the desired view type. The implementation can decide whether to push or pop a view controller onto the navigation stack, display a modal or show a side view. It can initially use default transitions and later on replace them with custom animations. Transitions deemed impossible can be checked with assertions to make sure everything works the way it was designed to. There are many ways to implement these features. An example implementation is available here.

The idea may seem a little complicated, but it has its benefits. Since the core view controllers no longer have a fixed transition logic embedded inside, they become a lot more reusable. The same view controller could be used in the main navigation controller, a modal view, a side view or any other place. They become the building blocks of the application which can be combined in a few different ways by the TransitionController. They also become a little bit simpler since the transition logic is no longer their responsibility. They no longer have to manage navigation controllers or modals. The subsequent controller interaction becomes a lot more explicit, since they share information either through the context object or through some changes in the model. Because all the transition logic is in one place it becomes easier to check how the application’s state graph looks like and which transitions are possible for which states. I’m a big fan of self-documenting code, and the TransitionController class becomes a very useful resource in this regard. Maybe a custom DSL could be devised to encode the transitions in a more readable and concise format than Objective-C code.

Of course, as with probably any technique, there are some downsides as well. The whole approach adds some complexity, which especially for simple applications may be completely unnecessary. When there is little or no view controller reuse in the application the main benefit vanishes. On the other hand if the application is very complex the transition controller may become bloated and cryptic. The general idea is by no means a silver bullet, and may not be applicable in some situations. However, even without this approach, explicitly thinking about the possible states and transitions within your application may be beneficial and insightful.

How Do We Hire the Best of the Best?
Macoscope Objective-C Style Guide