MVC: Many View Controllers

Feb 20 2020 3:00 PM

This article is basically the script from the talk I presented at dotSwift in Paris. If you prefer, you can watch the video.

INTRO

Today I want to talk about MVC. Model View Controller is the architecture iOS developers love to hate. I will be focusing most of this talk on the “C” part and what leads to single controller files getting out of hand.

But before I do, let’s address the elephant in the room: SwiftUI. Apple announced SwiftUI last year, which got the developer community understandably excited. I love SwiftUI, and I’ve been using it more and more for my internal projects, but I also need to keep supporting old versions of iOS - at least iOS 12 - with my apps, and SwiftUI still has lots of rough edges. So, regardless of whether you believe SwiftUI is the future or not, you probably still need be able to work in UIKit land for the foreseeable future. Besides, the way I like to split view controllers can probably be adapted to work in SwiftUI, even though it doesn’t have view controllers.

STORY TIME

1

I want to tell you my friend Elliot’s story. They’re quite young in the iOS development world and have been working on an app for their business for the past few months, using MVC. But now they’re facing some problems because view controllers are starting to become really massive, screens are coupled to other screens - which makes changing flows complicated - and they are having trouble writing good unit tests. Elliot believes the problem is their app’s architecture, so they decided to start searching for “the perfect architecture”.

3

Even though Elliot loves testing, they aren’t really a TDD developer - they’re an MDD developer. They search the web until they find a blog post on Medium that seems to address their problems. So they read some company’s post talking about this amazing new architecture called “PIE”: Presenter Interactor Entity, where everything in the app is represented as an Entity that can be manipulated by an Interactor and shown on screen by a Presenter. It’s composable, testable, functional, protocol-oriented… all the buzz words. There’s no mention in that blog post on whether such company is actually using the architecture, but they guarantee it’ll solve all of Elliot’s problems.

So naturally, Elliot rewrites most of their iOS app in this shiny new architecture. They also spend most of their time while they wait for their code to compile talking about app architecture with people on Twitter and on iOS development Slack groups.

A month later, WWDC happens, and Apple announces a bunch of new frameworks and features, but Elliot is still busy rewriting their app to use the PIE architecture, so they don’t have time to implement any of the new things.

September comes and Apple announces a new iPhone with TWO NOTCHES instead of one, which obviously changes the way things are presented on screen, but the framework Elliot uses to implement the PIE architecture will take a while to implement support for that.

Fast forward to the end of the year. Elliot is finally done with that rewrite, they’re quite happy about their architecture now, but they still haven’t shipped the app, and it still doesn’t support the latest devices and iOS features. Then, Elliot finds out that the company that published the PIE architecture abandoned it in favor of another one, and won’t be maintaining their framework anymore.

This is of course a fictional story about “Medium Driven Development”, but you know developers who work this way. By the way, the PIE architecture was made up by my iOS Architecture Generator.

Real artists ship

I love this quote, and I think many developers have forgotten about it. I’ve always been very focused on user experience as a developer, if something is not adding to the user experience, it’s probably not worth doing. I’ve found through experience that I’m the most productive and can iterate more quickly, thus providing a better user experience, when I use the MVC architecture… with some sugar on top.

Let’s talk about this sugar.

Not every view controller is created equal, I like to separate what Apple calls a ViewController into four categories:

I’ll be giving you an example of each type of view controller and how it can be used, you can check out a Swift Playground I made with the full example for each view controller on my Github.

CONTAINERS

2

Pop quiz: how many view controllers are on this screen?

I know that many developers have a single View Controller for every screenful of content, but actually the answer is 8. This is one of the things you can do to make your app more composable and decoupled: use child view controllers.

The way you do it is very simple:

let child = MyViewController()

addChild(child)

child.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child.view)

NSLayoutConstraint.activate([
    child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    child.view.topAnchor.constraint(equalTo: view.topAnchor),
    child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])

child.didMove(toParent: self)

You can make it even simpler with an extension on UIViewController:

public extension UIViewController {
    func install(_ child: UIViewController) {
        addChild(child)

        child.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(child.view)

        NSLayoutConstraint.activate([
            child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            child.view.topAnchor.constraint(equalTo: view.topAnchor),
            child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])

        child.didMove(toParent: self)
    }
}

It’s a very simple, but powerful technique. UIKit itself has lots of container view controllers, such as UINavigationController and UIPageViewController, but we can also create our own.

Let’s say you have a screen in your app where you need to show four discrete states: loading, loaded, empty and error. If you do all of them in a single view controller, it already becomes massive. What you can do is create your own, custom container view controller, that can be switched between those different states.

public final class StateViewController: UIViewController {

    public enum State {
        case loading(message: String)
        case content(controller: UIViewController)
        case error(message: String)
        case empty(message: String)
    }

    public var state: State = .loading(message: "Loading") {
        didSet {
            applyState()
        }
    }
	
    // ...
	
}

This way, you have a container view controller you can use for every time you need that behavior, saving you from having that code duplicated all over the place by using a reusable container.

GENERIC CONTROLLERS

When I say generic in this case I really do mean Generic<T>. We use generics in our model code all the time, so why not apply the same technique to our visual code? This is a very good way to provide common functionality to different parts of your app. In one of the apps I work on, we have generic table view and collection view controllers that let you create lists of dynamic content very easily.

The first step is to create a container collection view cell that can be populated with any view. This by itself is already kinda useful since you can now define your cell content as a regular UIView, which can be used outside a collection view, but can also be put in a collection using the container cell.

public final class ContainerCollectionViewCell<V: UIView>: UICollectionViewCell {

    public lazy var view: V = {
        return V()
    }()

    public override init(frame: CGRect) {
        super.init(frame: frame)

        view.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(view)

        NSLayoutConstraint.activate([
            view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            view.topAnchor.constraint(equalTo: contentView.topAnchor),
            view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }

}

Something else we need to cover is how to update the view that’s going to be inside the collection with its contents. There are lots of ways you could do it, but in my example I’m using a simple closure that takes the view as a parameter and modifies it - this closure is called when the cell is created or recycled.

class GenericCollectionViewController<V: UIView, C: ContainerCollectionViewCell<V>>: UICollectionViewController {

    init(viewType: V.Type) {
        super.init(collectionViewLayout: makeDefaultLayout())
    }

    var numberOfItems: () -> Int = { 0 } {
        didSet {
            collectionView?.reloadData()
        }
    }

    var configureView: (IndexPath, V) -> () = { _, _ in } {
        didSet {
            collectionView?.reloadData()
        }
    }

    var didSelectView: (IndexPath, V) -> () = { _, _ in }
	
    // ...
}

That’s just an example, but you can leverage generics to create some very powerful view controllers that can be reused in several places inside your app, through containment.

VIEW CONTROLLERS

This is probably the type of view controller you’re most familiar with. It’s the controller that manages a view, populating it with data (usually from a model) and responding to events.

But when I implement my view controllers, I avoid giving them too much responsibility. That’s why when I say “view controller” in this case I mean a view controller that simply lays out some views on screen and sets them up in the right way.

Maybe it also responds to some simple view events, but it usually doesn’t do anything with them directly, but relays that responsibility to a parent, either through a protocol or by using closures.

The point here is that these should be dumb. The dumber your view controllers are, the more ready you’re going to be when you want to transition to a different UI framework, such as SwiftUI.

Now, to address what’s in my opinion the worst problem with “traditional MVC”, which is the tight coupling between different flows in an app, I’d like to introduce the last type of controller.

FLOW CONTROLLERS

You can think of flow controllers as coordinators. They drive the flow of what’s happening with their children, so they can also be considered a type of container.

Many people use the coordinator pattern to handle things such as navigating between different screens. This approach is similar, but my coordinators are called “flow controllers” and they inherit from UIViewController. By inheriting from UIViewController we avoid fighting with UIKit and can take advantage of the responder chain and lifecycle events.

For instance, using this approach we don’t need to worry about what happens when a modal is closed. If you use something that’s not a UIViewController to coordinate a modal flow, you’re responsible for disposing of that after that flow is finished, but by using UIViewController, we can let UIKit take care of that for us.

So let’s say we have a screen in our app that downloads a list of geographical regions from an API, shows the regions to the user and then lets them select a region to see more information about it.

Instead of having a “regions controller” that is responsible for loading the regions, displaying them and reacting to selection to present the “detail controller”, we’ll have a “regions flow controller” that does the loading, then populates the “regions controller” when the data arrives. This flow controller will also react to selection by pushing a detail controller.

Since this controller manages that entire flow, it will not be inside a UINavigationController, but instead it will own a navigation controller that’s added as its child and push/pop view controllers inside of it.

With this approach, we managed to create a reusable flow, with view controllers that are completely decoupled from each other and can be used in different flows as needed. This also ensures changes to the flow are easy to make: if we want to present the detail as a modal or add another step in between, we only have to change the flow controller, its children are completely unaware of their environment.

What’s cool about this approach is that we can encapsulate extremely complex user flows - such as a checkout process in an e-commerce app - inside a flow controller, which can be presented or embedded in different ways to achieve the result we want.

With this, we have a reasonable way to structure our app without fighting with UIKit.

A FEW MORE THINGS

I hope you learned something about the way you can improve view controllers with the examples I gave. Now that we talked a lot about view controllers, I would like to focus on another common issue developers face when using MVC, which is more common with younger developers.

This issue happens when the developer thinks they must fit everything into either M, V or C, forgetting that they are allowed to create other types of constructs.

One that I’m a fan of is the view model, popularized by the MVVM architecture. I do use them a little differently tho. My view models are just like models, but they are tailored to a specific type of view or view controller.

They are usually created from a model, but they transform and format model data to something that’s more usable to a view or view controller. For example, a Post model in a blogging app would have a publishedAt property that’s a Date, but a PostViewModel would have the publishedAt date already formatted, ready to be displayed on screen. It’s something I do to prevent duplication of that type of code and also to keep views and view controllers as dumb as possible, it’s also great for testing.

So, no matter what you call them, try to create model-type classes that are tailored to your views and controllers, instead of moving models all over the place and duplicating untestable transformation and formatting code.

That’s yet another example, but there are many more types of entities you can leverage to make your apps more modular, testable and fun to work on. In fact, if I were to try and make a name for the architecture I use, it would probably be composed of way more than just three or four letters. And that’s why I prefer to just call it MVC.