How to make friends with UIKit

Bohdan Orlov
Bumble Tech
Published in
7 min readOct 26, 2017

--

It has been years since iOS community started to fight UIKit, and many people have come up with sophisticated ideas about how to bury the UIKit internals under layers of abstraction in their fancy architectures. Some teams rewrite UIKit, fulfilling their engineers’ ego but leaving behind a humongous amount of code to maintain.

Make UIKit work for you

Since I am a lazy person, I always aim only to write code which is necessary and essential. I want to write code which is good enough to fulfil both the product requirements and the code quality standards we have in our team, but a minimum of the code to support the infrastructure or boilerplate of an architectural pattern. Thus, I believe that instead of fighting UIKit, we have to embrace it and make use of it as much as we can.

UIKit-friendly architecture choice

Every problem can be solved by adding another layer of abstraction. That is why VIPER is a popular choice; it has a lot of layers/entities to spread out all the work. Writing an app in VIPER is not hard. What is hard, is to write an MVC app which has the same virtues with a smaller amount of boilerplate to support.

If we start a project from scratch, we can choose an architectural pattern and make things “right” from the beginning. However, in most cases, we do not have this privilege, and we have to work with an existing codebase.

So, let’s do a thought experiment:

You are joining a team with a large codebase. Which approach do you hope to see inside? Plain MVC? Some MVVM/MVP with Flow controllers? Is it a VIPER or Redux-based approach on some FRP framework? I hope to see the simplest one which does the job. Moreover, after my work is done, I want to leave behind me some code which anyone can read and amend.

That said, let’s see how we can build on top of View Controllers instead of trying to replace and hide them.

Let’s say you have a set of screens represented by View Controllers presenting each other. These View Controllers fetch some data from the internet and display it. From a product perspective, everything works perfectly, but you do not know how to test the code inside, attempts to reuse code end up in copy-pasting and View Controllers become too big.

It becomes evident that you have to start separating code, so what is the minimum effort action you can do to achieve this? Extracting code which fetches data into a separate object leaves the View Controller with only one responsibility displaying it. So, let’s do this:

Now that this looks very similar to MVVM, so let’s use its terminology. So, we have a View and a View Model, and now we can test the View Model easily. The next step is extracting repetitive tasks like networking and persistence into Services.

By doing this, you achieve two things simultaneously:

  1. you reuse your code
  2. you obtain a source of truth which is not attached to the UI level.

How is this related to UIKit?

Let me explain:

A View Model is retained by a View Controller, and a View Model does not care if the View Controller exists at all. Thus, if a View Controller deallocated, the underlying View Model is deallocated as well.

On the other hand, if we choose an approach where a View Controller is retained by the other object like a Presenter in MVP, we might end up with a dangling Presenter and View Controller if something accidentally dismisses your Controller. If you think it is hard to accidentally dismiss a wrong Controller just carefully read the description of UIViewController.dismiss(animated:completion:).

Thus, I believe that the safest option is to admit that a View Controller is king, and thus limit the lifetime of non-UI objects to two simple options:

  1. objects with a lifetime equal to the lifetime of a UI element (e.g., a ViewModel).
  2. objects whose lifetime is equal to the lifetime of the application (e.g., a Service).

Exploiting the View Controller Lifecycle.

Why is it so tempting to put all the code into a View Controller? It is because in a View Controller we have access to all the data and the current state of the view. If we want to have access to the View Life Cycle in a View Model or a Presenter, we have to forward it manually, and it is not wrong, it is just more code which we have to write.

Nevertheless, there is another option. Since View Controllers are designed to work easily with other View Controllers Soroush Khanlou, suggested exploiting this fact by splitting all work between smaller view controllers.

We can take one step further and introduce a universal way of hooking into the View Controller Lifecycle — the ViewControllerLifecycleBehaviour.

public protocol ViewControllerLifecycleBehaviour {
func afterLoading(_ viewController: UIViewController)
func beforeAppearing(_ viewController: UIViewController)
func afterAppearing(_ viewController: UIViewController)
func beforeDisappearing(_ viewController: UIViewController)
func afterDisappearing(_ viewController: UIViewController)
func beforeLayingOutSubviews(_ viewController: UIViewController)
func afterLayingOutSubviews(_ viewController: UIViewController)
}

The easiest way to explain is an example: let’s say we want to detect screenshots in a Chat View Controller but only when it is on screen. After extracting the routine into a VLCBehviour, the usage is super simple:

open override func viewDidLoad() {
let screenshotDetector = ScreenshotDetector(notificationCenter:
NotificationCenter.default) {
// Screenshot was detected
}
self.add(behaviours: [screenshotDetector])}

The behaviour implementation is straightforward:

public final class ScreenshotDetector: NSObject,  
ViewControllerLifecycleBehaviour {
public init(notificationCenter: NotificationCenter,
didDetectScreenshot: @escaping () -> Void) {
self.didDetectScreenshot = didDetectScreenshot
self.notificationCenter = notificationCenter
}
deinit {
self.notificationCenter.removeObserver(self)
}
public func afterAppearing(_ viewController: UIViewController) {
self.notificationCenter.addObserver(self, selector:
#selector(userDidTakeScreenshot),
name: .UIApplicationUserDidTakeScreenshot, object: nil)
}
public func afterDisappearing(_ viewController:
UIViewController) {
self.notificationCenter.removeObserver(self)
}
@objc private func userDidTakeScreenshot() {
self.didDetectScreenshot()
}
private let didDetectScreenshot: () -> Void
private let notificationCenter: NotificationCenter
}

The behaviour is also testable in isolation since it conforms to our protocol for View Controller Lifecycle Behaviour.

Implementation details are available here.

Behaviour can be used for VLC dependent tasks, for example analytics.

Exploiting the Responder Chain.

Imagine you have a button deep in a hierarchy of views, and you need to perform a very trivial thing — the presentation of a new controller. The typical approach is to inject a View Controller from which to perform a presentation. Such an approach is indeed the right way to go. However, sometimes this means introducing a transient dependency, a dependency which is not used by those who are in the middle but used somewhere deep in the hierarchy.

As you guessed, there is an alternative, and we can use the responder chain to find a controller capable of presenting another view controller.

The solution might look like this:

public extension UIView {
public func viewControllerForPresentation()
-> UIViewController? {
var next = self.next
while let nextResponder = next {
if let viewController = next as? UIViewController,
viewController.presentedViewController == nil,
!viewController.isDetached {
return viewController
}
next = nextResponder.next
}
return nil
}
}
public extension UIViewController {
public var isDetached: Bool {
if self.viewIfLoaded?.window?.rootViewController == self
return false
}
return self.parent == nil &&
self.presentingViewController == nil
}
}

Exploiting View hierarchy.

Entity–Component–System pattern is a fascinating way to implement analytics in the application. Such an approach was implemented by my colleague and it has proven itself to be extremely convenient.

The Entity is a UIView.

The Component is a piece of Tracking Information.

The System is the Analytics Tracker Service.

The idea is to decorate UIViews with appropriate tracking information. Later, the analytics tracking service scans the visible part of the view hierarchy N times per second, recording tracking information which has not been recorded yet.

When such a system is in use, the only thing required from a developer is to add the tracking information, like screen and element names:

class EditProfileViewController: UIViewController {
override func viewDidLoad() {
...
self.trackingScreen =
TrackingScreen(screenName:.screenNameMyProfile)
}
}
class SparkUIButton: UIButton {
public override func awakeFromNib() {
...
self.trackingElement =
TrackingElement(elementType: .elementSparkButton)
}
}

Traversing a view hierarchy is a BFS ignoring views which are not visible:

let visibleElements = Class.visibleElements(inView: window)
for view in visibleElements {
guard let trackingElement = view.trackingElement else {
continue
}
self.trackViewElement(view)
}

Such a system obviously has performance limitations which cannot be ignored. So, there are a few approaches which can help not to overload main thread:

1) do not scan the view hierarchy too often

2) do not scan the view hierarchy while scrolling (use an appropriate run loop mode)

3) scan hierarchy only when a notification is posted in NSNotificationQueue with NSPostWhenIdle.

Takeaway.

I hope I managed to demonstrate that it is possible to cooperate with UIKit and that you have found something useful for your everyday life as a programmer or that at least that I have given you some food for thought.

Thank you for reading! If you liked this article, please hit ‘Recommend’ (the ❤ button) so other people can read it too :)

Hit me on Twitter if you want to chat.

--

--

Bohdan Orlov
Bumble Tech

iOS head. ex @IGcom 📈, @MoonpigUK 🐽, @Badoo 💘 and @chappyapp 🖤 Lets' grow together 🌱@bohdan_orlov http://arch.guru