Managing Massive View Controller Mayhem

Imagine having a single view controller for each screen in your app taking care of everything from catching user interactions, setting up views, creating network sessions, to parsing received data and handling network errors — all in all it is a recipe for disaster. This article will describe our approach to trimming down the MVC by using Lifecycle Behaviours.

Andrew Chevozerov
Revolut Tech
5 min readNov 7, 2017

--

Credit: freepik

The Massive View Controller

Every iOS developer will have come face to face with one of most common platform-specific architectural patterns — the MVC. The abbreviation stands for Model View Controller, but some would argue a more accurate interpretation should be Massive View Controller, as it more faithfully describes what it is: a huge pile of densely-packaged code that no-one really knows how to change or safely fix.

Over the last few years, the community has developed many approaches to deal with this problem. These range from separating huge screens into smaller inter-connected pieces with fewer responsibilities, to adopting enterprise-class architectures and related frameworks. Most of these solutions require a significant amount of time just to plan and to develop, let alone to implement them. Another issue is that these improvements usually don’t mix well with a generally old and messy codebase. That said, there are always some techniques and approaches that could be used in almost every project, regardless of it’s age, toughness or size.

Behaviours are one of those quick improvements you could easily implement and try out in a project at any stage, without having to rewrite the entire codebase. When used wisely, behaviours will significantly improve the readability and maintainability of your code. Let me explain.

While the original post introduced a very generic object following SRP and left a wide space for implementation fantasy, the most useful variant is a Lifecycle Behaviour.

The lifecycle behaviour

In modern apps, certain tasks are featured multiple times throughout the app. For example, a common action might include adjusting the scrollable screen area while the keyboard is displayed or displaying the network status change handler.

As you may already know, both examples rely on Notifications and since any notification or key-value observer should be added in the viewWillAppear method (and consequently removed in theviewDidDisappear method), a straightforward approach is to include these methods inside a BaseViewController and to then call the necessary methods for the custom behaviour — so the only point of creating separate objects is to slightly reduce the amount of logic built into the view controller.

Fortunately, there is a solution for this case: every child view controller will receive the same lifecycle messages as its parent. So we could simply write a fake subclass of the UIViewController which will automatically forward all lifecycle messages to the actual behaviour objects. Here is an example of the public interface of behaviours core used in the Revolut App:

The full source code with a usage example can be found here.

Usage example

Let’s look at how to use behaviours in an example showing one of the most common use cases for keyboard handling. Let’s assume we have the following screen featuring:

  • an image
  • some text
  • an input field
  • a button

All the elements are placed into the UIScrollView , allowing the user to scroll up and down when the keyboard presentation state is changed.

The standard approach would require you to subscribe to a corresponding keyboard notification and to use the system info to adjust your interface accordingly. But if your app has more than one screen, you’d probably need to implement this more than once. Thus, keyboard notification handling is a very good candidate to be extracted into its own separate universal object — the KeyboardObserverBehaviour:

As you can see, the KeyboardObserverBehaviour holds very common boilerplate code that registers to (and unregisters from) system keyboard state notifications and also receives information from the notification object. Then, the KeyboardObserverBehaviour simply calls the corresponding blocks for either the static or animated action. Let’s see how this behaviour approach is used inside the real view controller:

And that’s about it — for convenience, I have also used UIScrollView extensions to set the bottom content inset as well as to animate scrolling. The full implementation can be found here.

Disadvantages

Although using behaviours can greatly reduce the complexity of your code, you should also be aware of some pitfalls.

First of all, use them wisely! It’s really easy to find yourself creating many different objects for small tasks. This runs the risk of ending up with dozens of files with only 20–30 lines of code, which is very inefficient!

Secondly, you should always keep in mind the core implementation of lifecycle behaviour. Since its container is a subclass of the UIViewController which is also being added as a child, a small mistake can lead to long hours of debugging — for instance, adding a behaviour to a current navigation controller, or even worse, adding it to a custom container controller from one of its children!

Thirdly, it is generally not a very good idea to use behaviours for certain shared objects. We are using the behaviours technique described above to control the current navigation bar style. Our app has four different styles, including two dynamic cases for when the navigation bar’s appearance should be changed. This happens as the user scrolls through the content on screen.

The main problem is that, in general, all controllers want to have their own navigation bar appearance behaviours, which is ok in most cases. However, once we’re dealing with a more complex screen with two nested child screens, we will most certainly have a conflict between the different behaviour objects that simply manage the same tasks.

Unfortunately, there is no easy way to gracefully resolve these type of conflicts, which is why you need to always have a clear structure when coding your own projects.

Conclusion

There are many different approaches for maintaining clean and solid app architecture. Using the behaviours techniques described above is a great approach to add to your arsenal.

As always, reach out in the comments below with any questions or suggestions and keep your code clean!

Revolut launched in July 2015 with a punchy mission: to turn the financial banking sector on its head. With Revolut, users can set up an app-based current account in 60 seconds, spend abroad in over 120 currencies with no fees, hold and exchange 25 currencies in-app and send free domestic and international money transfers with the real exchange rate.

--

--