Building a reusable UIViewController (a.k.a. one VC to rule them all)
View controllers share common simple traits that can be generalized, allowing them to be reused. This ends the need to create new UIViewController subclasses for each view and partially solves the "massive view controller" problem (or at least takes it somewhere else).
As an iOS developer, you have most likely found yourself spending tons of hours writing the same repetitive view controllers, just to accommodate your views.
The UIViewController
is, because of Apple's design choices, an essential part of UIKit
. To present anything on screen you need a view controller that embeds it.
Think of the UIViewController
as necessary container to any view in UIKit
. A container that has access to Life Cycle
events [1] such as viewDidLoad
, viewDidAppear
and so on and so forth. As of the time this article is being written, you simply can't escape from it (unless you are using SwiftUI
).
You write that sameviewDidLoad
and put all your setup there. Weirdly enough, view setup doesn't necessarily need to happen atUIViewController
's life cycle events.
In order to better organize and separate the actual view from life cycle logic you could just subscribe / listen to those events (via delegate or closures) and act accordingly when needed. You will also find out that when you are not fetching data through network requests, accessing databases nor performing other actions according to the life cycle, you won't even need to hook up anything to your view controller, only embed your content's view in it.
What if you want to use a view
that you built inside another view
using Autolayout
? If all your view
code is inside a view controller
things just don't work so smoothly and you end up having to add your view controller
as a child of it. If your view has a subclass of its own, you could have just added it as a subview (to the target superview).
Let's imagine for the purposes of this article that you have a simple scene that after the loading of the view retrieves from somewhere and displays a famous person's portrait photo and a (supposed) quote of theirs [2] (the actual view is irrelevant and won't affect at all the content of this article).
One way you could write this view programmatically is as follows [3]
There is a lot going on here…(and there could be a whole article dedicated to it *1) but this UIView
subclass only simply sets its own style, builds its hierarchy of subviews and constraints everything properly, having no knowledge of any life cycle events whatsoever. This separation has some benefits, including testability (which is not in this article's scope to explore).
Now we can create our reusable view controller… let's call it ...ReusableViewController
😅.
(Or maybe GenericViewController
, BaseViewController
or something… names are hard, if you think of a better one please let me know).
In a custom view controller, if you wish to set its view programmatically (our case) you must do so in the loadView
method. Setting it directly in the initialization (or at any other time) will cause your view to fail calling the viewDidLoad
method (it still "kinda" works but you will not receive that notification and any logic dependent on it will break).
So we must first store the desired view in a local property to subsequently use it. [4]
This is the minimum required for your view to be presented inside the view controller, but it still has no access to the life cycle events.
There are many ways one can go about notifying that these events were fired. Let's get into some of them.
0. Using closures
A very simple way is to use closures. Let's keep things simple for now and just use two of the life cycle events [5].
As there is already a method named viewDidLoad
and viewWillAppear
we need to change our closures a bit to avoid collision. Here I chose didLoadView
and willAppearView
to better match what you find on UIKit
APIs.
You could also give default values to your closures to prevent them from being optional and from having to unwrap every time you need to call them.
Although replacing optionals with default values is very convenient and you may be tempted to do so, these default values (in this case) have no meaning and are a mere placeholder for a non existing value, which can be misleading and may make debugging much more painful.
This is probably the simplest approach (and may be the best one already). The only two downsides that I can think of are maybe:
- The need to manually set each of these events (instead of setting a single delegate). This could crowd your composition root *2 (the place where you inject your dependencies to setup your scene) a bit but it is usually not so much of an issue (for most scenes).
- Naming. Since you cannot use the already existing methods names you may end up with some not so pretty alternatives.
The main alternative to this approach is using delegates. This could be done with one or an array of delegates (if you are going to notify many components, maybe instead try using a "mid-way "component to do the one-to-many relationship (and avoid multiple delegates)… or just use some reactive approach… with or without extra frameworks). By the way, if you end up using multiple delegates with single methods, the closure approach might just be better (as you essentially need to inject the same amount of properties and you don't create more data types and dependencies).
Here I'm just gonna add a single delegate that may or may not be set. [6]
Now you may begin to wonder if the delegate's protocol methods should all be mandatory.
As there are lots of events you might want to add to this interface the protocol will grow rapidly (as it would be adopted by every scene in our app). Not all scenes will need to listen to every event (to be honest, most scenes will probably just use the viewDidLoad
if they even need it).
Following the CRP (composite reuse principle), components should only need to conform to and add behavior that they need (preferring modular / more atomic protocols over god-like inherited classes that are the root of all evil and couple everything together).
There are, again, some ways one can go about addressing this issue. If you only ever need one method then you might just skip this and not worry at all about it. Otherwise, you may consider one of the following:
- Using multiple atomic delegates & protocols
- Using one delegate with a single method sending events through an enumeration
- Using one delegate with optional methods (via either objc optional methods or protocol extensions with default method implementations)
Let's briefly go through each approach.
1. Using multiple atomic delegates & protocols
Some may prefer using multiple delegates to multiple closures. Maybe (just maybe) the closure's synthax isn't as clean as the delegate's. [7]
The likely justification is the need to avoid name collisions on closures. Having to change your viewDidLoad
to something like didLoadView
isn't (at least for me) so appealing.
One thing to add is that not grouping your events together can allow for different listeners to each one of them (you can also achieve the same effect with a single delegate but you would need a component acting as middle-man to redirect those event calls).
2. Using one delegate with a single method sending events through an enumeration
I have seen this approach used in different ways and using different naming conventions (handle _
, act on
, listen to
, observe _
). If you end up adopting this approach, it is up to you and your team to agree on a convention that best suits your needs (considering each project may already use some of these names in other context).
This method can greatly decrease the size of your ReusableViewController
, at the expense of having to switch over your newly created data type somewhere else. There are several downsides of this approach because of the use of enumerations. If you are familiar with the SOLID principles and good software practices, just seeing this approach might have already set some red flags for you.
Generally speaking enumerations don't scale well. If your project continues to grow you will inevitably end up with huge files spanning thousands of lines to account for each enumeration case (lots of cases in definition and a big switch
). Everytime some new event is added you need to change each place the enumeration is used (to deal with it accordingly), including your test cases.
This means that any change in the enumeration may break client's code. This fails to comply with the "Open for extension and closed for modification principle" (letter "O" from SOLID).
Enumerations are convenient at first but it is just as easy to fall on their trap. And sometimes that can become a true nightmare (ok maybe an exaggeration here but you get the point).
An interesting way to avoid big enumerations if making use of protocols and polymorphism. Instead of sending an enumeration type you send an object that implements a protocol and just call the desired method (that may also be a getter).This solution doesn't work for every single case… and as far as I can perceive it doesn't work for this approach either. Again, this enumeraitons criticism is a big topic that could have an article of its own (don't get me wrong, many times enumerations are just fine) *3.
3. Using one delegate with optional methods
Being able to mark protocol methods as optional is a really nice added feature to Swift (that came from objc-c) and may be the solution if the drawbacks of exposing your method declaration (and your protocol) to the Objective-C runtime
aren't that meaningful. This will, for instance, allow your method to be swizzled through obj-C *4… basically letting the implementation of an existing selector to change at runtime (again, huge topic to further explore).
In advantage, this solution is very simple and requires only a few lines of code (although "compactness" is not a indication of good nor clean code by itself, and sometimes it is quite the opposite).
4. Using protocol extensions to provide default method implementations
You can avoid exposing your methods to the objc-c runtime by simplying providing default implementations. As these implementations are weirdly empty just for the sake of making them optional this approach may "break" code contract by masking the absence of conformance. This method may also hide some bugs as delegate calls could just invoke empty void functions without compiler warnings or anything of the kind.
Usage
You can place and call you ReusableViewController
anywhere you seem fit to. Nevertheless, here's and example, setting it up at the Composition Root
of our app (which just happens to be the SceneDelegate.swift
)
If you choose closures, always remeber to use [weak self]
when referencing the object you are in to avoid retain cycles & potential memory leaks.
Testing
It is always interesting to have your logic tested.
As a bonus, here is how I usually test the ReusableViewController
.
If you wish to have some guarantee that all your reusable view controllers are always working correctly then you could add similar (or not) tests to your project(s). Here I opted for the closures approach but it would work very similarly for any other.
Final version
If you wish to download the full project I used for this article, test, tweak and make suggestions, here is the repo.
It is open so I would love to see some PR with suggestions for improvements!
Conclusion
There are many ways one can go about generalizing view controllers and making use of their life cycle events. Only five (rather similar) approaches were discussed here.
There is no a “single best choice” amongst them. This may sound generic and repetitive but the best approach is the one that best fits your needs.
Having said that, from all the presented options the closures and single delegate with protocol extensions are my two favorites. Among them I usually prefer the former, as it is rare that you need to use all life cycle events in a scene… and, even so, if you do come a across a case in which you need to use all of them, setting it up in your composition root will only take about 5–7 lines at worse. This approach usually improves debugging as it is straightforward to test if the required closures are set up correctly (and therefore not nill).
This approach also uses almost exclusively primitive types. Wherever & whenever you can opt for primitive types it is often a good idea to. This means you don’t have to expose your inner data structures / protocols to anyone listening (in this case, the delegate type must be exposed in order for it to be implemented and used).
Nevertheless, the delegate approach (any of them) works fine as well and is in many cases very suitable.
Further Reading
I mentioned many topics that I didn't particularly explored deep enough here. To provide you with some direction, here are some great resources on each one of them!
Friendly (truly) last section
Well, if you made it 'till here alive I can only congratule and thank you for your time.
Was this article useful? I would love to see your comments and suggestions! If you happen to know someone this may be interesting to, feel free to share it with them as well.
I love talking about programming, paradigms, design patterns, good practices, principles and technoloy in general… so, if any of it resonates with you and you would like to have a friendly talk about it, just hit me up!
You can find me on most social platforms as @lucaswkuipers
Thanks again and see ya next time!