This article is part of a 4-part series on Passing data between View Controllers
- The Problem With Apple's View Controllers
- Create Concise View Controllers by Using the Delegate Pattern
- String Many View Controllers Together Using Coordinators
- Avoid Global State by Using the Responder Chain
At some point in our iOS careers, we’ve written code like this:
At first glance this seems fine. I mean, even Apple’s sample code uses this pattern. However, as we peel away what’s going on, we’ll uncover why this approach scales so poorly.
In this article, I want to help you build a better understanding of coupling and how this makes objects, like
UIViewController , difficult to use and brittle to change.
Let’s get started!
The Pitfall Of Segues
Segues force you to design bad software. There, I said it.
We strive to have small, composable objects that we can fit together to build a large application. In fact, being able to break down big problems into small ones is perhaps the most useful skill a programmer can have. (I’m positive @davedelong tweeted this, but I can’t find the original tweet. Oh well, thanks Dave! 👋)
We achieve this by applying principles like Encapsulation and the Single Responsibility Principle because we know that small objects with few responsibilities are easier to manage and reason about.
But now this begs the question, what’s the responsibility of a
We can all agree that it should handle events from the UI. It is a view controller after all. But what about network requests? Or navigation? And what about persisting data?
For instance, let’s take a look at the snippet above. Do you see anything out of place?
Notice how this view controller just assumes that it’s going to receive segues and transition to
ItemDetailViewController. Isn’t that a little odd? It’s as if it knows its place in our navigation flow. The whole rest of the app needs to exist around it in order for this to make sense.
What’s more, it apparently knows how to configure
ItemDetailViewController. Try justifying that in court.
(And let’s not get started on stringly-typed APIs and force unwraps.)
So here we are, with a view controller — whose responsibility is taking care of the view — that cares a little too much about navigation, and what’s around it.
This view controller should only care about itself. It shouldn’t care about the other view controllers around it. And as such, it certainly shouldn’t care about the next view controller it’ll navigate towards.
How Can We Fix This?
Of course, our problem above can be fixed by decoupling our
ListViewController from our
ItemDetailViewController. But why though? What’s the point of decoupling view controllers from each other and the rest of our app?
When you have view controllers that are truly isolated, you’ll be able to use them everywhere. You’ll be able to add them as child view controllers. You’ll be able to use them in completely different contexts. You might take a view controller from your app and use it in a Today widget without changing a single line of code. These view controllers become the visual building blocks that you assemble in order to build your application.
The more comfortable you are at keeping things separate, the more empowered you’ll be to add new features to your app. Your view controllers won’t be working against you.
Is this a bit dramatic? Perhaps, but I know you’ll see a huge boost in productivity if you take this to heart.
Ok, now let’s move on: what is coupling and how do we avoid it?
What Is Coupling?
Coupling is a measure of how two classes are dependant on each other.
For example, our view controller not only assumes to be navigating to
ItemDetailController, but it also reaches in and configures it. It’s creating many assumptions about the world around it and how it should behave. For example, it assumes:
- Its navigation is done via segues
- It’ll navigate to
ItemDetailViewControllerwill have an
Imagine if the implementation details were to change inside
ItemDetailViewController? That could break our third assumption. At best, our code no longer compiles and we need to fix the issues. At worst, our app crashes.
Or what if we decide to wrap
ItemDetailViewController in another view controller (like the loading container from this article)? We break our second assumption and our typecast will fail.
In other words, changes made to a different view controller can break this one. This is why we want to avoid coupling.
So let’s take a step back and consider the root cause of all this – the hierarchy of these objects, as well as who owns who.
The Patterns to Minimize Coupling in View Controllers
Now this is the heart of the issue. What guidelines can we give ourselves to make better decisions?
General software development disclaimer (GSDD? 🤔) : these are only suggestions, not a list of hard and fast rules. Use these to reflect on the code you’re writing and develop your own sense of taste to make good decisions.
1. Children shouldn’t be coupled to their parents
More often than not, a child should be able to exist on its own, independent of its parent. Why would a child object be bound to a single parent? Or in other words, why should your view or view controller only be useful in a single place in your app?
A simple way to ensure that child objects are decoupled from their parents is to use the delegate pattern. Apple’s frameworks provide many examples of this, particularly in UIKit. This is something we look at more closely in part 2 of this series.
2. Siblings shouldn’t be coupled to each other
When moving from your
ListViewController to your
DetailViewController, they shouldn’t know about each other. In fact, they shouldn’t make any assumptions about how the other operates.
Segues completely break this rule. I would recommend to avoid using segues completely, no matter the complexity of the app. It’s just not worth the tradeoff.
Instead of having sibling objects talk directly to each other, you can move them under a common parent and communicate through it. We’ll be looking at how to do this using the Coordinator pattern in part 3 of this series.
3. Reduce coupling between parents and their children based on context
Parent objects need to rely on their context to define how coupled they should be to their children. If you have a view controller that coordinates a specific flow of other view controllers, you’d have to jump through too many hoops in order to make things completely decoupled. That’s not surprising, since that parent serves a very specific purpose.
On the other hand, if you’re building a container view controller that can contain any
UIViewController, then you’ll definitely want to keep that implementation as generic as possible. In the case that you need special behaviour from its children, use a protocol to define that behaviour and force them to conform to it.
4. (Bonus Round) Object dependencies should be loosely coupled and injected.
Decoupling classes from their dependencies can have massive wins for code clarity and testing. Knowing which parts of the universe an object needs in order to work is massively useful information, and exposing that upfront will give you immense clarity on how coupled your object is to the world around it.
The catch-all term for this is Dependency Injection. You can bet your socks that we’ll be looking at this in a future article as well.
Let’s Wrap It Up!
We deciphered what was wrong with Apple’s sample code, we talked about coupling, and we looked at guidelines on how to reduce it.
I hope you’ve developed a better understanding of what coupling is, and more importantly, what you can gain in being intentional about limiting the scope of your objects.
But much like anything in software development, managing coupling doesn’t come easy at first. In the beginning, it’ll seem like a lot of ceremony just to push a view controller. Over time, though, I’m convinced you’ll build the habits that make these decisions practically effortless.