The Cocoa frameworks and the Model-View-Controller pattern go hand in hand. A typical iOS application, for example, is composed of models, views, and view controllers.
The view layer is responsible for presenting data to the user. Views are, and should be, dumb to increase reusability. A view doesn't know what it is displaying. It only knows how to display what it's given.
The model layer is in charge of the business logic of the application. It's responsible for handling the application's data. It knows how to create, read, update, and delete data. If data needs to be presented to the user, the model layer is in charge of providing that data.
The model and view layers are glued together by the controllers of the application. Those controllers are instances of UIViewController
(iOS) or NSWindowController
(macOS) or subclasses thereof. A controller is responsible for everything that isn't related to data handling and presentation. And that's quite a bit.
This often results in the Massive View Controller syndrome. View controllers are notoriously difficult to test and if your project contains a few massive view controllers, a large portion of your project is automatically difficult or impossible to test.
In this tutorial, I discuss three strategies for putting your project's view controllers on a diet. These strategies don't force you to drastically change your coding habits nor do they require you to write more code. They merely ask for a bit of planning and a few minor architectural changes.
Strategy 1: Model-View-ViewModel
You may already know that I'm a big proponent of the Model-View-ViewModel pattern. I consider the Model-View-ViewModel pattern an improved version of the Model-View-Controller pattern. MVVM is easy to pick up and has a gentle learning curve.
The problem I have with the Model-View-Controller pattern is simple, everything that isn't related to the view or model layer goes into the controller layer. While that may look great on paper, it inevitably leads to unwieldy view controllers.
The MVVM pattern solves this problem by tweaking the traditional recipe you're already familiar with. The pattern adds a fourth ingredient to the recipe, view models. A view model sits in between the model layer and the controller layer. It owns the model and it's usually owned by the controller. This is what that relationship looks like.
How does Model-View-ViewModel help you keep view controllers lean and mean? If a date needs to be formatted using a particular date format, the controller is no longer responsible for converting the date to a string. In fact, the controller shouldn't even know how the date is stored or where it comes from. The controller asks the view model for a string, which it uses to populate its view.
// View Model
func formattedDate() -> String {
// Initialize Date Formatter
let dateFormatter = DateFormatter()
// Configure Date Formatter
dateFormatter.dateFormat = "YYYY MM d"
return dateFormatter.string(from: profile.createdAt)
}
// View Controller
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = profileViewModel.formattedDate
}
Two important benefits are immediately obvious. First, testing the code that we move to the view model becomes trivial. The view model asks its model for data and outputs that data in a particular format.
Second, the controller loses some of its responsibilities. As a result, it becomes leaner, focusing primarily on user interaction and populating the view layer with data.
There's much more to the Model-View-ViewModel pattern, though. I'm only showing you the tip of the iceberg. I almost always use the Model-View-ViewModel pattern in combination with bindings, an amazingly powerful combination.
Strategy 2: Multiple View Controllers
A screenful of content can sometimes have a lot going on. This can quickly result in a view controller that knows too much about the application, burdened with too many responsibilities. This problem is easy to solve by breaking the user interface up, distributing responsibilities among multiple view controllers instead of one.
On iOS, this is better known as view controller containment. The idea is simple. The user interface is split into several sections and each section is managed by a view controller. A parent view controller is responsible for the user interface, managing the child view controllers that are responsible for the various sections of the user interface.
You're probably already using view controller containment without realizing it. Navigation, tab bar, and split view controllers are also container view controllers. They each manager one or more child view controllers.
By breaking the user interface up, you gain two important benefits. First, the view controller in charge of a complex user interface can shift some of its responsibilities to one or more child view controllers.
Second, view controller containment makes it easier to reuse user interface elements. If a section of a complex user interface is reused in several locations of the application, then view controller containment is a worthwhile investment.
Outsourcing
Table views are incredibly reusable because they outsource many of the tasks associated with presentation and user interaction to other objects. The table view doesn't know anything about the data it is presenting nor does it know how to respond to user interaction. For that to work, the table view makes use of two protocols you're probably already familiar with:
UITableViewDelegate
UITableViewDataSource
Any object conforming to the UITableViewDataSource
protocol can feed the table view the data it needs to present. For some reason, however, developers are often inclined to burden a view controller with this task. That's fine if that's the only responsibility of the view controller. But that's rarely the case. Is it?
This strategy is similar to the Model-View-ViewModel pattern we discussed earlier. The idea is to put an object in charge of feeding the table view with data it can present. A similar approach can be used for any protocol that isn't strictly tied to a UIViewController
subclass.
What's Next?
These three simple strategies are easy to adopt and immediately have an effect on the complexity and size of your project's view controllers. There really isn't a reason not to use them.