When developing applications on iOS, View Controllers tend to become cumbersome and confusing which causes a real headache for a programmer who likes correctness and order in the coding experience. This post tells about our iOS developer’s experience of working with View Controllers and not only the actual development, but also trying to figure out what problems are in the way of the graceful implementation of these classes and what steps can be taken to solve them.
Model. View. Controller. What is the View Controller?
When you get down to designing a class for an iOS app, it can’t harm to identify what category it belongs to from the point of view of the Apple suggested MVC architecture: model, view or controller?
Model View Controller (MVC) is a concept of building and organizing code that Apple has been actively encouraging us developers to use. If start with a definition Apple gives, MVC is a design pattern that assigns objects in an application one of three roles: model, view or controller. They communicate across the abstract boundaries separating one from another. If we choose to use MVC pattern the custom objects will be required to play one of the MVC roles. In other words, if view, model and controller are involved in building an app that consists of classes, each of them is assigned the role of a model, a view or a controller.
Read also: Uber Api Integration Into Your App
But here comes a View Controller composed of those 2 words that can’t but confuse me. How come does the word View Controller make such a mess in the classification established by Apple? Getting back to Apple documentation, read:
«View Controllers are traditional controller objects in the MVC design pattern. Their job is to provide many behaviors to iOS apps.»
But according to MVC logic each class of objects has to correspond to a certain type of the MVC interconnected parts (model, view and controller). So what type does the View Controller eventually belong to if it sounds like it has 2 classes inside?
Running ahead I’d like to break it down for you: View Controller is definitely a controller. Now let’s try to figure out why exactly View Controllers are so cumbersome!
Read also: Event applications dev in UK
Why View Controllers aren’t perfect
Developing an app we treat View Controller as a screen controller. When adding functionality into the app we often add it directly to the «screen», that is a View Controller. Such View Сontroller becomes a delegate of tables, pickers, collection views and other objects. However, this straightforward approach gives birth the following potential problems:
- Duplicating code. This touches upon not only logic, that should be extracted to separate classes, but also boilerplate delegate methods, that you have rewrite over and over again.
- Code structure becomes more complex in case of a custom solution (e.g. unusual solutions like 2 table views on one screen). I think all know those difficult <If then> constructions in the delegate methods used to only determine the cell of which table has been selected.
- View Controller becomes a data source as it stores the data itself and not a model object.
- Code isn’t batched which makes it hard to allocate the code corresponding to one object.
- View Controller stores states. ...Not that it’s too bad but we could do without it.
How to handle those drawbacks?
- Inherit. If you have ever gone through Stanford iOS lectures, you might have paid attention to the CoreData ViewController. It encapsulates the code that relates to the Data Fetch from the persistent storage, representing it in the UITableView. Inheritance from this parent class will help you get rid of the boilerplate code. But if you have the UICollectionView instead of the UITableView, you will have to invent the same parent class with quite similar logic.
- Extract the boilerplate code into a separate class.
TableViewDataSource as a separate class
Let’s imagine that we have extracted all the TableView related code into a separate class and called it a TableViewDataSource. What happens is that ViewController becomes only responsible for passing our class as a delegate and a data source of the TableView. TableViewDataSource gets set up with an array of the source data and decorated with blocks.
This approach is fine but TableViewDataSource has two problems:
- This data source doesn’t suit other classes that present collections
- What is going to retain the data source? If there is going to be a lot of data sources, then View Controller will turn into a storage of the data sources.
Read also: Real estate app development
Solving the matter by separation
We can continue writing source classes for each separate view class (UITableView, UICollectionView, UIPickerView etc.), which will lead us to subsequent understanding of the regularity in them. Each of the classes can be divided into 2 parts:
— DataSource is a part responsible for assigning object group by sections and rows. It is universal.
— Adapter includes methods in charge of interaction with a particular class. They are class specific;
We can create a class called ArrayDataSource responsible for returning data by rows and sections. This class will be able to interact with UITableView, UICollectionView, etc. with the help of adapters.
Let’s say initialization would look like that:
_myArray > dataSource > adapter > _tableView [_tableView setAdapter:[TableViewAdapter new]]; DataSource *dataSource = [DataSource sourceWithArray:_myArray]; [_tableView.adapter setDataSource:dataSource];
We don’t always need the adapter explicitly. By using lazy initialization and some helper methods, we can get rid of calling the adapter at all. This way we get a one liner-code:
[_tableView setDataSource:[DataSource sourceWithArray:_myArray]];
Any object implementing the protocol <DataSource> can become a data source.
Coming back to the initialization:
[_tableView setDataSource:[DataSource sourceWithArray:_myArray]];
As you can see, it lacks a specific thing — cell’s set up. In order to unify this process we propose to use <ObjectConsuming> protocol.
It is simple and demands to implement a @
property id object:
- (id)object - (void)setObject:(id)object
We can implicitly set up any cell that accepts ObjectConsuming protocol. This way all table coding comes down to one line:
[_tableView setDataSource:[DataSource sourceWithArray:_myArray cellClass:[MyCell class]]];
NOTE: You still have to use your own hands to register cell classes or nibs in the view class.
As we have set up the cell with a single object then it makes sense to get the height of the cell using that object.
NOTE: It’s a class method not an instance.
DataSource is completely unbound from the view. We can create any data sources we like. I got four data sources:
- ArrayDataSource accepts an array of objects
- FetchedResultsDataSource accepts NSFetchedResultsController
- SegmentedDataSource is the same as the arrayDataSource but provides objects separated by segments like UIPickerView
- ComponentsDataSource is meant for solving a common problem.
Why not take a closer look at it?
Sometimes there is a necessity to allocate all the cells of different classes. One common example is a profile screen with user information. The type of a table can be different. It can include various blocks in various orders depending on the user type (e.g. a player or a viewer). Implementation of this table seems to be a bit confusing, right?
Why don’t we represent each of the possible blocks as a separate component? This component retains a class of the cell that represents it and an object for the cell setup. Of course, a cell that modifies model data by itself is a bad decision. So you can also assign a cell’s delegate in the Component class. ComponentsDataSource accepts the array of such objects. This way the adapter of the view class will get a different cell class for each row.
and the formalization:
We managed to make the code easy readable. The set up can be implemented in simple ways, just like bitmasks. After defining the array of the components, we pass it to the table using ComponentsDataSource.
Developing this scheme I added one convenient option. Most often when picking up a cell we want to know which object was chosen. For that we need to turn to the source array and find the object using indexpath of the selected cell. So why not go and get this object?
Let’s skip searching through the array for a needed object and allow the adapter to return the object in the callback <didSelectCell> right away:
didSelectCell> right away:
void (^didSelectCell)(UITableView *tableView, NSIndexPath *indexPath, id object);
With this unusual approach you don’t even need to store array properties in the View Controller.
Data Source and MVC pattern. Conclusion
As you can see we have significantly changed the View Controller class and created a few additional ones. Let’s take a look at the architecture we have gotten from the point of view of the design patterns.
There is a quite curious FSUC pattern — so called Fat Stupid Ugly Controllers. It isn’t difficult to distinguish in them familiar to us view controllers. According to the classical definition of the MVC, model is not only data, but the whole logic of the application without any connection to the view. So it turns out that Fat Stupid Ugly Controllers work as ....a model! Interesting, isn’t it?
By implementing the scheme Controller + DataSource + Adapter + View, we left the controller its only job — receive data and transfer them to the View layer.
DataSource we got is an abstract model of user interaction with data. Thus it perfectly fits into the definition of a Model. Adapter which knows all about the implementation of a view class, undoubtedly refers to the View layer.
Trying to simplify view controllers, we have come to a much deeper understanding of the MVC concept as well as the architecture that fits this concept like a glove. Let’s get back to the MVC and try to define the place for the new classes.
Having gotten rid of the business logic ViewController became exactly what it has to be — a controller. It’s nice to call it glue between the layer of the data (model) and window on the screen (view). Just to remind you, before that we had FSUC, which included too much of logic for a ViewController.
In addition, you can check out some handy categories to the common view classes implemented with the same approach.
Other Classes ACUtils
Using the same approach as before, namely implicit use of the runtime attribute, I made convenient categories for some of the view classes.
UIActionSheet + Blocks
- (void)ac_setDestructiveBlock:(ACActionSheetActivityBlock)destructiveBlock; - (void)ac_setCancelBlock:(ACActionSheetActivityBlock)block; - (void)ac_setOtherBlocks:(ACActionSheetActivityBlock)firstBlock, ...
Also blocks willPresent, didPresent, willDismiss, didDismiss
UIAlertView + Blocks
- (void)ac_setCancelBlock:(ACActionSheetActivityBlock)block; - (void)ac_setOtherBlocks:(ACActionSheetActivityBlock)firstBlock, ...
Also blocks willPresent, didPresent, willDismiss, didDismiss.
UIPageViewController + ACPageController
Let to set a definite number of pages:
- (void)ac_setViewControllers:(UIViewController *)firstController, …
After that we can switch either to the page according to the index:
- (void)ac_showPage:(NSInteger)page; - (void)ac_showPage:(NSInteger)page animated:(BOOL)animated;
Or a definite controller from those transmitted before:
- (void)ac_showViewController:(UIViewController *)viewController; - (void)ac_showViewController:(UIViewController *)viewController animated:(BOOL)animated;
The block of the transition closure is also provided.
Demo project is here.
Check the presentation I made for my speech covered in this article (the slideshare presentation is in #Russian).