MVC Layer Interaction Architecture

Every day at Macoscope involves working on multiple big projects, often of high complexity. Each application has to synchronize with various remote services, manage loads of data, and display deeply complicated views with nontrivial interactions. To manage all of these functions, we need to split our code into many smaller chunks which have to interact with each other and keep in sync.

These interactions often prove difficult to implement well without complicating the code or introducing unnecessary coupling between objects. We constantly try to develop solutions to these problems and look for universal interaction architectures that would keep the code decoupled.

After trying out a variety of different approaches in numerous applications and analyzing their strengths and weaknesses, we came up with a few general rules and patterns that seem to work quite well for us. Although these solutions aren’t ideal and there is still a lot of room for improvement, they are way better than the naïve approach. We’re constantly trying to enhance our patterns and it’s likely that in a few months some of the ideas will have evolved.

The iOS Flavor of Model-View-Controller

Before engaging in any further analysis, it’s important to restate the assumptions of the MVC architecture on iOS. The primary difference between the iOS flavor of this pattern and its classic version is the lack of direct interaction between the model and view layers. All the interaction between these layers should be performed through the controllers. This forces us to write more decoupled and reusable code for the view and model layers, but puts a lot of strain on the controller layer in its role as the glue in between. This, in turn, may lead to the dreaded Massive View Controller antipattern. Model-View-Controller Also, on iOS, both controllers and views usually form a large hierarchical tree-like structure (or, to put it more formally, a DAG) which isn’t necessarily the case in many common implementations of MVC. This makes the single controller and view objects smaller and more reusable, but forces us to implement more complicated interactions between the various objects. View and view controller tree-like structure There are a number of great resources on different approaches to MVC and other alternative patterns like MVP or MVVM available on the internet. The basic description of the MVC pattern used in Cocoa and Cocoa Touch can be found here, while further discussion on the subject is available here.

The Controller-Model Interaction

When implementing controller-model interactions it is very important to prevent the application from being able to reach any inconsistent states. Inconsistencies can arise in the model layer, in the controller layer, and between the model and the controller. All of these are often a result of too many different update paths and lack of consistency. It would seem that the simplest solution to this problem is to limit the number of ways in which various objects can be updated, and to a large extent this is precisely what we did.

The first big rule we try to stick to is accessing the model through a single model object. Using the facade pattern, we aggregate all the model methods into a single ModelManager object, available to the controllers through its sharedInstance. Even though this object could be accessed in every other part of the code, the general rule is that only controller objects are allowed to use it. Encapsulating the entire model in this way has a lot of advantages. For me, clarifying my mental image of the application would be benefit enough, but there are indeed many more. I feel that this approach forces us to better separate the controller and model code. It also allows us to make more changes in the model layer without affecting the controller code, to make the model more consistent as a whole, and to hide the model-model interactions behind a wall of API. ModelManager as a single point of access to the model layer Another interesting idea we recently introduced is callbackless requests. A lot of highly popular frameworks like AFNetworking, Mailcore2, and the Evernote API use callback blocks for asynchronous requests. The popularity of this approach initially led us to think that all asynchronous requests should be handled this way, especially by the ModelManager. However, using callbacks poses a wide variety of problems, thus making it difficult to scale the project well. Essentially, when the model notifies you about changes through the request callback, you are to some extent responsible for informing others about the update. If you do inform others, you significantly increase coupling and implicitly agree to add other controllers to the “inform me about this or that” list when needed. If you don’t inform others, they either won’t get updated, which will result in an inconsistency, or they will get updated through other update paths. The problem with the latter is that the more update paths there are, the more complicated things become. When the other controller performs the same request, will it listen to the “other update path” or the callback? Both? Some parts here and some there? Suddenly, you’re facing dilemmas, more complicated interactions, and a lot of doubts about different update circumstances and consequences. You end up making some mistakes, and an inconsistent state results in a crash.

Instead of supplying completion, success, or failure blocks to all request methods, we started pushing changes from the model down the view controller tree. This way we have a single path of updates which is a lot easier to analyze and keep bug-free. Every controller has a -modelUpdated: method which is called when the model changes. It can then analyze the change object and react accordingly. It doesn’t have to react differently in several separate update paths. The AppDelegate sets its root view controller as the ModelManager‘s update propagator, and the connection is complete.

@implementation UIViewController (ModelUpdate)

- (void)propagateModelUpdate:(ModelUpdate *)update
{
  [self modelUpdated:update];

  for (UIViewController *childViewController in self.childViewControllers) {
    [childViewController propagateModelUpdate:update];
  }
}

- (void)modelUpdated:(ModelUpdate *)update
{
}

@end

The ModelUpdate object encapsulates enough information about the changes for the view controllers to check whether they need to update themselves or not. How these objects are created and what information they need to hold is something that has to be decided on an application-to-application basis.

As a result of this approach, we can safely access the model from any view controller without worrying about updating its parent. Thus we introduced another rule — all of the view controllers should perform as much work themselves as they can. That means child view controllers don’t delegate model changes to their delegate (which in most cases would be their parent), but instead perform the changes themselves. The idea may be obvious, but I find that it’s often useful to explicitly state the obvious instead of relying on my gut. It’s a lot easier to follow explicit rules than rely on general ideas like “the code should be well structured.”

The Controller-View Interaction

As I said before, the controllers and views form a hierarchical, tree-like structure. The more complicated the application, the deeper this structure will become. The deeper the structure, the more interactions between the components are needed. To prevent the formation of long delegate chains, we decided to use the responder chain to carry various events from deeper views and view controllers to the ones higher up. The UIResponder chain is ideal for event propagation, but by default only handles touch and motion events, with almost no metadata to go with it. Instead of using these built-in events, we created a category on UIResponder to propagate our custom events. The idea is in many regards analogous to model update propagation, but it goes through both views and view controllers, and there is only one nextResponder for each of the nodes.

@implementation UIResponder (InterfaceEvent)

- (void)sendInterfaceEvent:(InterfaceEvent *)event
{
  if ([self.nextResponder canHandleInterfaceEvent:event]) {
    [self.nextResponder handleInterfaceEvent:event];

  } else {
    [self.nextResponder sendInterfaceEvent:event];
  }
}

- (BOOL)canHandleInterfaceEvent:(InterfaceEvent *)event
{
  return NO;
}

- (void)handleInterfaceEvent:(InterfaceEvent *)event
{
}

@end

By default, the responders don’t handle any events. We override the -canHandleInterfaceEvent: and -handleInterfaceEvent: implementations for the objects that are interested in some of the events. When a view recognizes a gesture that should trigger an event it calls -sendInterfaceEvent: on itself with an object representing the event. The event object can store any required metadata which can be extended by responders higher up the chain.

You can read more about the responder chain here.

Why Not Use Notifications?

A solution similar to model update propagation and event bubbling could be achieved with notifications. It could be argued that NSNotificationCenter was created specifically for the purpose of simplifying such interactions. It has, however, a few serious drawbacks. One of the drawbacks is that with notification center you have no control over who receives the notifications. This may not seem like a serious problem, but it’s easier to have the rules you want to follow enforced. The more serious issue I have with notifications is that they essentially create a direct connection between two objects. When a leaf view decides to send a notification, its parent doesn’t take part in the interaction. Instead of the event going through the parent, it directly affects the observing object. This adds a lot of direct paths in the interaction graph of the application, making it a lot harder to think of composite views as encapsulated components. Notification-based interaction In the event bubbling approach, at any point a view or view controller in the chain can prevent unwanted events from reaching any further whenever it decides to. This makes the interactions look almost exactly the same as they would if we were using delegate chains, but without any of the superfluous code. Event-based interaction

Summary

Complex applications generally require a lot of thought regarding their architecture. Component interactions are among the most important parts of this analysis. We tried a wide variety of approaches: some of them worked well for us, while others didn’t. We use a few different rules and patterns that help us keep the code well-structured and decoupled. We hope that the ideas presented above will help you improve your code as well, but it’s up to you to decide whether any of these solutions can be employed in your own projects.

Previous Experience
Improving Debugging Workflow – Introducing MCSLLDBToolkit