A Better MVC, Part 1: The Problems

Part 1 in a series on "fixing" Model-View-Controller:

  1. A Better MVC, Part 1: The Problems
  2. A Better MVC, Part 2: Fixing Encapsulation
  3. A Better MVC, Part 3: Fixing Massive View Controller
  4. A Better MVC, Part 4: Future Directions
  5. A Better MVC, Part 5: An Evolution

I recently ran across a great article by @radiantav called “Much ado about iOS architecture”. It addresses a topic that has been on my mind a lot. I gave a talk about it at the recent Swift by Northwest called “A Better MVC”. These blog posts attempt to capture the main points of my talk.

I apologize beforehand that there aren’t really any pictures in this. They’d definitely make the subject matter clearer, but I don’t feel like spending the time to make them. You’ll just have to use your imagination.

The “Problems” with MVC

The main reason that people decry MVC is that they tend to run afoul of two major problems when using it:

  1. MVC, as taught by Apple sample code, encourages you to violate encapsulation principles, which ends up leading to spaghetti code.
  2. Without proper discipline, your view controllers end up being huge, leading to the joke that “MVC” means “Massive View Controller”.

Violating Encapsulation

It’s pretty common to come across a UITableViewDelegate method that looks something like this:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {         let maybeItem = query?.object(at: indexPath)
    guard let item = maybeItem else { return }
    let detail = MyCustomDetailViewController(item: item)
    show(detail, sender: self)
}

There are two huge encapsulation violations that are occurring here.

The first is this line:

let detail = MyCustomDetailViewController(item: item)

The principles of encapsulation indicate that a thing should only really “know” about objects that it contains, and then only about one layer of abstraction down. However, this line, which constructs a subsequent screen-full of information, requires the view controller to know about the sequence of data flow in its parent abstraction.

In other words, this list isn’t just a list, but must also being aware of the context in how it’s used.

The second violation is similar to the first:

show(detail, sender: self)

Again, this violates the whole “don’t know your context” principle. And, it should be noted, these critiques hold true whether you’re manually creating and showing a detail view controller, or invoking a segue and using prepareForSegue:sender:.

Massive View Controller

It is sadly pretty common to come across a view controller that is thousands of lines long. Our view controllers quickly become cluttered with networking callbacks, delegate methods, data source methods, IBActions, trait collection reactions, model observation, and of course, our business logic. Heaven forbid if you’re doing any sort of progressive loading and want to swap in an indicator to show while things are loading… that will usually add in a couple hundred lines of code as you deal with swapping views around, managing yet another stateful property, and so on.

We end up with this situation because we don’t apply enough discipline to our view controllers. They’re convenient places to dump code. But they quickly become unmaintainable, fragile, and inherently untestable. Who wants to be the poor soul who has to debug some weird state going wrong in a 5,000+ line file?

Working around MVC

In my experience, these are pretty much the two fundamental reasons why developers find other architectural patterns so appealing. As a result, we turn to other architectural patterns to try and compensate. We reach for MVVM, or React, or MVP, or FRP, or VIPER, or insert-new-architecture-here.

To be clear, there is nothing inherently wrong with any of these patterns. They all solve problems in unique and interesting ways, and it is good to study them and learn the principles they teach. However, I’ve found that building apps based on these patterns tends to pay negative dividends in the long run.

All of these patterns tend to be “strangers” to UIKit and AppKit. Because of this difference, you end up with additional hurdles to clear when things change.

When your team members change, you have additional work to teach new developers about not just the business logic of your app, but often a whole new architectural pattern. This requires more up-front investment, and a longer lead time before team members can start being productive.

When the operating system changes, you have additional work to try and shoehorn new features in to architecturally-native concepts. You have to plumb through size classes, safe layout margins, dynamic type, localization, locale changes, preferred attributes (status bar style, etc), lifecycle events, state preservation and restoration, and who knows what else when new paradigms get inevitably added as iOS updates each year.

When your requirements change, you can sometimes be caught in the situation of having to fork someone else’s library, wait for them to catch up, or hope that they’ll accept your pull request. I won’t go in to the pros and cons of third-party dependencies here, but an architectural dependency has even more “gotchas” than a normal dependency, because of the way it can underpin your entire app.

Each step of the way, you may be celebrating that your code is clean and concise. However, you drift further and further away from what the system frameworks are providing, which means adopting new systems features requires more code to bring it to where you are.

Wouldn’t it be nice if you could just get it for free? Wouldn’t it be awesome if you could just use UIKit and AppKit and have it all “just work”? Wouldn’t it be nice to live in a world where you thought 300 lines in a view controller was excessively long?

In the next post, we’ll look at how we can fix the first problem.


Related️️ Posts️

A Better MVC, Part 5: An Evolution
A Better MVC, Part 4: Future Directions
A Better MVC, Part 3: Fixing Massive View Controller
A Better MVC, Part 2: Fixing Encapsulation