My Journey on the UI Design Patterns in the iOS World

Here’s my way across verious design patterns to my current shelter

Nikita Lazarev-Zubov
Mar 21 · 6 min read

There are a lot of posts about how to implement one design pattern or another. I’d like to clarify immediately that it’s not one of them. This post doesn’t explain patterns, but instead, requires reader to have some basic knowledge about them.

What I’m talking about here is my way across various design patterns to my current shelter. And the trip begins with a rather infamous one.

Model-View-Controller (MVC)

As the most of iOS developers, I started my journey in MVC. Everything seemed limitless at first. Apple was providing me with code snippets and class blanks. But the sun hid behind the clouds very soon, right after I needed to switch the UI and wanted to unit test the presentation logic.

The pattern has been known since the 1970s and origins from the Smalltalk world. I can’t say that the pattern is bad. The main problem is the way Apple separated Controller and View layers (or rather, didn’t). How many times have you seen the UI configuration inside UIViewController‘s viewDidLoad() callback?

overrie func viewDidLoad() {
super.viewDidLoad()
// Create and configure subviews.
// Add subviews.
// Add constraints.
// Etc.
}

Of course, this code could be moved into the corresponding UIView subclass and called through the API:

override func viewDidLoad() {
super.viewDidLoad()
customView.attach(videoPlayer: videoPlayer)
customView.updateTextView(with: textData)
}

But it’s all the same: presentation logic stays in the same place. Why is it bad? A controller should keep presentation logic, shouldn’t it? Yes, but UIKit’s view controllers create and store views for themselves. Views are part of controllers. That makes impossible, or at least extremely clumsy, to separate the view code from controllers. This tight coupling makes hard to switch UI.

The same design problems complicate testing. It’s hard to test presentation logic without calling the view code. Moreover, the initial design of Apple’s view controllers encourages statefulness. Everything happens internally.

A common way to get along with that is to create a handful of satellite objects — various routers, handlers, validators, helpers, etc. — as a controller’s dependencies and test them all separately. Extracting as much pieces of logic out of controllers as possible. As for me, I don’t like this approach because it compromises readability: every new programmer has to sort it out from scratch. Furthermore, you can’t move all the view controller’s code from the view controller.

I was looking for a more conventional solution, one the sctructure of which would be familiar to the most of developers. So, my next stop on this journey was another obvious choice.

Model-View-ViewModel (MVVM)

Since Apple strongly coupled UIViewControllers and UIViews, considering these as a single layer is a good option. That’s exactly what the MVVM pattern does.

MVVM is much more young pattern, it was introduced by Microsoft engineers in 2005 as an MVC variation.

The pattern encourages to keep UIViewController/UIView classes as dumb as possible, factoring out all the logic to the ViewModel layer. Although it sounds good, the pattern still keeps View in a somewhat active state. So, that returns us to the testability. On the one hand, the most of data processing is now easily testable. On the other hand, the view binds itself to the ViewModel fields and handles updates of that fields.

(Another peculiarity of the MVVM is the binding between the View and the ViewModel. It’s very common to use various reactive technologies for that, or sometimes KVO. In simple cases the Delegation or the Observer patterns can do the trick.)

So we still have something like this in view controller classes:

import RxSwift // Or whatever.override func viewDidLoad() {
super.viewDidLoad()
viewModel
.totalScore
.asObservable()
.bind(to: totalScoreLabel.rx.text)
.disposed(by: disposeBag)
// More observing setup goes here.
}

(Honestly, I’ve never been a fan of Rx. I’ve always preferred to write a bit more of boilerplate code, having a bit more simplicity in testing and a bit more control on lifecycle of the corresponding objects.)

Thus, although view controllers don’t have many logic within, they are still active. Their activity is logic, too, which I wanted to see covered by tests. So, my journey continued.

Model-View-Presenter (MVP)

Despite the fact that the MVP pattern is older than the MVVM, my next stop was there. The pattern was introduced in the early 1990s for C++ systems, later for Java.

My first thought was that it’s just the MVVM, but turned inside out. Later I realized that this is exactly what I was looking for.

Finally, it keeps the view layer entirely passive. It only requires attaching the link to a view object. The presenter in turn receives signals from view and pulls its strings.

Being frank, I met halfway to make the pattern make friends with UIKit. Although it’s usually supposed that presenter owns view, I injected a presenter into a view:

init(presenter: Presenter) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
}

This let me to easily attach view at the right moment:

override func viewDidLoad() {
super.viewDidLoad()
presenter.setView(self)
}

This is it! Now the presenter can initiate any view events. Having a view hidden behind an interface makes substitution of views/view controllers rather trivial:

protocol View {
func display(scoreData: String)
}
final class ViewController: UIViewController, View { func display(scoreData: String) {
scoreLabel.text = scoreData
}
// ...

Another trick I resort to for factoring the presentation logic out of the view layer is the delegation of the view lifecycle events to presenter when needed:

protocol Presenter {
func viewAppeared()
}
final class ViewController: UIViewController, View { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presenter.viewAppeared()
}
// ...

This was the last piece of the puzzle because it made view controller events easily tesatable:

@testable
import App
import XCTest
final class ViewControllerTests: XCTestCase { func testPresenterCallOnViewDidAppear() {
let presenter = MockPresenter()
let view = ViewController(presenter: presenter)
view.viewDidAppear(false)
XCTAssertTrue(presenter.viewAppearedCalled)
}
// ...

Thus, this is the last stop on my journey so far. But I’m still reflecting and sometimes a thought or another visits me…

View, Interactor, Presenter, Entity, Router (VIPER)

Of course, MVP couldn’t exist in isolation. Each UI module (e.g., a screen) must be presented somehow and has to handle related navigation events — hence, the Router. Data may lay somewhere beyond and need to be retrieved and handled, too — hence, the Interactor. Thus, keeping the MVP in mind I often find myself in the middle of the way to the VIPER pattern.

Initially, the latter is an application of the Uncle Bob’s Clean Architecture, designed specially for the iOS apps. Therefore, it’s the youngest of the mentioned patterns. Born in 2014, to be exact.

I’ve never generated clean VIPER modules, religiously. I’ve never used the term Interactor. But I’m happy enough adding a router dependency to a presenter. And having specialized model entities to implement an interaction between a data source layer and a specific UI module.

And of course, I enjoy good testing oportunities and well-established terminology for using between fellow developers.

Conclusion

I don’t claim that my journey is the only right way towards the perfect design. I’m deeply convinced that it doesn’t matter which pattern you are using. But you must choose one deliberatedly, taking into account your current circumstances and having a clear idea of what you want from a pattern and what exactly you get.

May your code always be perfectly maintainable and testable!

The Startup

Medium's largest active publication, followed by +607K people. Follow to join our community.

Nikita Lazarev-Zubov

Written by

Software (iOS) Development Engineer @ Dream Broker, Helsinki, Finland

The Startup

Medium's largest active publication, followed by +607K people. Follow to join our community.

More From Medium

More from The Startup

More from The Startup

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade