iOS apps are built according to a widespread programming pattern: the model-view-controller pattern (or MVC). The MVC is a high-level pattern used to describe the global architecture of an application and to classify objects according to the general roles they play. In MVC objects tend to be more reusable and programs are more easy to change.
In the MVC pattern objects are subdivided in three categories. Model objects encapsulate the app data and the logic to manipulate them. Model objects have no explicit connection to the user interface or knowledge of it.
The user interface is represented by view objects. The role of these objects is to present information to the user. They know how to display data and might allow the user to interact with it, but are not responsible for its storage.
What ties these two layers are controller objects, making sure that views have access to the model objects they need to display, and propagate changes in both directions. Because of this role, they tend to be the least reusable objects in an app architecture.
iOS uses a special kind of controller objects, called view controllers. These special controllers don’t only take part in the MVC pattern by mediating between the model objects and the view objects, but also take additional responsibilities. One of these responsibilities is that they are, like views, part of the responder chain, so they receive events too (like, but not limited to, touch events). View controllers also receive memory warning notifications from the OS and are responsible of memory management.
The problem of massive view controllers
Since view controllers have additional responsibilities in iOS, they have to handle quite a load of work. For this reason it is really easy for other code that would not belong to view controllers to creep inside of them. So they often become big monoliths, containing all sort of functionality, because it is easier to just add another method to a view controller, until you find yourself with a bloated object that becomes really hard to modify or refactor, making view controllers even less reusable than they would be. This is why they are jokingly called “massive” view controllers by developers.
I know this firsthand, because I’ve been guilty of this behavior and I still prone to it, despite being here telling you to avoid it. It’s something so ingrained in the way we make iOS apps that it requires developers to be constantly vigilant to avoid it. I personally use lines of code as a metric to measure if a view controller is growing too big. If a view controller goes above 500 lines it means that it is probably including functionality that should not be there, so I review it with more attention.
Try not to let a view controller grow too much before refactoring it. Although it’s true that you can always extract functionality from it later, if you keep postponing it, all the code you write in a view controller is going to be entangled to some other code, thus making it harder to extract it. Also, there is usually the psychological barrier of not wanting to go into big refactorings and touch code that already works for fear of breaking it.
How to write smaller view controllers
There are a few guidelines I follow to keep my view controllers small and manageable and they all come from the role a view controller has in the MVC pattern, which is to only allow transition of information between the model and the views. View controllers, due to their special role in memory management, can also perform set-up and manage the life cycles of other objects.
The domain (or business) logic for an app is not listed in these duties. This is the first place where actually code that should not be in view controllers creeps in. View controllers often take part in the business logic of an app, manipulating objects, incorporating algorithms, dealing with the network or the disk. They in fact absorb most of the code that should go into the model layer. This is all logic that should be put into separate, specialized classes, while the view controller itself should only initiate changes responding to user input.
A specific case for this is represented by delegates and data sources. In iOS, complex views like table views or collection views, get configured by external objects, called delegates and data sources. Very often this functionality is implemented inside view controllers. This happens a lot also in many tutorials and even in Apple documentation. There is even a specific subclass of UIViewController, the UITableViewController class, that automatically sets itself as the delegate and data source of its table view.
A better approach here is instead to actually separate these behaviors into separate classes. The data source is especially a very good example for this. If, for example, the data for a table view is provided through an array, as often happens, you can write a data source objects that handles the array and provides all the behavior specified by the UITableViewDataSource protocol: returning the count of the array as the number of rows in the table view, fetching the object at the appropriate index when the table view requires it, inserting and deleting objects in the array and so on. This removes a lot of code from a view controller and has also the benefit of being much more reusable: the next time you will have a table view that gets filled by an array of objects, you will have to just create a new instance of your array data source, assign it to the table view and you are done. This data source class is even portable to other apps. You can have a data source to deal with Core Data, or a data source that deals with objects fetched from the network. Once you have written and tested them, they will be readily available to all your future apps. In addition, your view controllers become more general themselves and can be reused with different kinds of data sources across your app.
The other side from where unrelated code creeps in a view controller is the UI side. Many times view controllers get hooked through outlets to every single view on the screen and then proceed to set their appearance and position. But this is actually code related to presentation, so it is better to isolate it, for example in subclasses of UIView. Your view controllers will be again smaller and more general, and these views that you create might also be reused in other parts of the app or other apps. After all, this is exactly what a table view already does: incorporate specific UI code so that you have a ready to use component that you can use in many different places.
Speaking of table views, another place where UI code creeps into a view controller is when dealing with table view cells. The culprit here is the -tableView:cellForRowAtIndexPath: delegate method. In this method, model objects get fetched from the data source and used to set the content of table view cells, either through outlets or, even worse, accessing the cell subviews directly using the -viewWithTag: method of the UIView class. Unfortunately this code is even present in the Apple documentation, so many people follow it (I did that in the past too, not knowing any better). What should really be done here is to create a subclass of UITableViewCell that takes care of populating all the cell fields. In this way the -tableView:cellForRowAtIndexPath: will only have to fetch a record from the data source an pass it to the cell, which can be done in just two lines of code. By the way, as you might have noticed, the cell itself becomes is a more generic and reusable class. Once you have created a custom class for your table view cells, other logic will fit in it naturally, like, for example, calculating the cell’s height when it is dynamic.