Rapture In Venice, LLC

:: Freelance iOS Development & Team Augmentation

Here’s Why: View Navigation Sucks in iOS

The “Here’s Why” series attempts to explain the what’s and why’s of basic iOS concepts and topics. What does this mean? Why is it this way? I’ll attempt to keep the focus on the theory rather than the minutiae of the code so we can learn the fundamentals of the issue to help in future understanding.

In the last few years, a new design pattern has emerged in the iOS space called the Coordinator. The Coordinator pattern is one that aims to remove all app navigation logic (push, present, pop, dismiss, etc.) from the View Controllers and handle it externally, from the outside. It’s separation of concerns as it should be.

But what’s really wrong with doing it in the View Controller?

Before I discovered the Coordinator pattern, I knew something was wrong with how we do navigation. Let’s start simple before we get into the problem areas.

Example #1: Simple Master-Detail (Acceptable)

Let’s say we have a UITableView of video games where, if we tap any one of them, a detailed view of that game is pushed into our navigation controller. The user interacts with that screen and then dismisses it by tapping the back button. When the user taps, we instantiate the detail view and then call navigationController?.push on it. This pushes the screen forward. We return to the list when the user taps the back button in the navigation bar.

This usage is fine. The detail view contains no dismissal logic since it never calls navigationController?.popViewController to dismiss itself. In this way, the detail view has no knowledge of how it’s presented.

Good to go.

Example #2: Not-So-Simple Master-Detail (Questionable)

Now let’s take that same Master-Detail example but, this time, the detail view lets you edit information and has two buttons, “Save” and “Cancel”. This seems similar, but now we’ve introduced a problem: the detail view needs to dismiss itself when one of these buttons are pressed.

We can do this in one of several ways, but here are the primary options:

  1. We call a delegate or closure on tap.
  2. We perform an exit segue on tap.
  3. We call navigationController?.popViewController on tap.

If we use the first method, we’re good to go. In this case, the detail view need not have any knowledge of how it was presented. What I mean here is that the list view presents the detail but the detail view need not care how it’s presented because it isn’t dismissing itself directly. The delegate is defined by the calling code and can decide how to dismiss the view on its own.

Awesome.

Using exit segues isn’t as hot an idea. While the dismissal code is abstracted out of the detail view, it relies on any view that presents it to provide an exit segue route somewhere in the hierarchy. You’ll get this right the first time you use this view in your storyboard, but later on someone who reuses this detail view in a different place will be surprised when they click “Cancel” and nothing happens, for example. The exit segue logic is always a hidden implementation detail which is why I’ve never cared for it. This is why I would prefer adding an onCancel closure so that I can easily check for it (type on<Escape> to see my options) and provide my own logic.

The third option, putting the dismissal code right in the detail view, is the worst idea. Sadly, this is extremely common to see as well. I ran into this problem on an app a while back when dismissals weren’t working because the specific view I was reusing was originally being presented by a UINavigationController but now I was using it as a modal. Calling popViewController directly in the view controller wasn’t dismissing it at all!

You never notice the error in doing things this way until you’re building an app that reuses view controllers a lot, but once I ran into this problem I vowed never to include self-dismissal logic in my view controllers again. When a view was presented, it would always have a closure such as onCancel or onSave that the presenting view controller should implement to handle it. If you tapped a “Cancel” button in my view controller, it would simply call onCancel?() and that’s all.

But even that wasn’t foolproof…

Example #3: Nesting View Controllers (Insanity)

On the most recent app I worked on, I had some complex view navigation logic that threw me into the fires of hell. The hierarchy looked like this:

A -> B -> C -> D

Here, A is on a UINavigationController that pushes B which presents C modally which presents D modally. The trick here is sometimes C needs to reset its data and sometimes it should go away. Also, sometimes B needed to be able to dismiss C or D. Not to get too deep into the weeds, each view controller only knew about its presented view controller and so the logic got hairy fast. I could call closures back one level at a time, but the logic became increasingly opaque and when the wrong thing happened it was hard to trace the logic from beginning to end. (Or in this case, end to beginning.) I’d have to trace through the whole chain of views each time to debug and it was taking up a ton of time.

The problem was that while I was doing the right thing by not having each view controller have its own dismissal logic, the chain as a whole (A, B, C, D) was a conglomerate and, therefore, housing its own dismissal logic anyway. Somehow, this view controller group needed to be treated as one thing and the logic needed to be factored out.

Enter the Coordinator Pattern

Using the Coordinator pattern, this group of view controllers can be completely controlled by one object, a coordinator. I won’t explain the details of the pattern here, but it’s best explained by Will Townsend. Quite simply, when A is presented, a coordinator is created that becomes the delegate to A, B, C, and D. When an action is taken, such as when D needs to dismiss back to B, we do so explicitly by calling a delegate method, perhaps something named onDismissReward(self.reward).

Yeah, that method is long, but it’s clear. :-) Not only is the view controller not having to handle its dismissal logic, but it’s also not relegating that task only to the one before it. It’s asking the coordinator to do it, and the coordinator is intimately familiar with all of A, B, C, and D.

The coordinator then implements this method and, having access to all of the view controllers in the chain, can decide the best way to dismiss back to B having full control of everything. The alternative, having each view controller only know about the next in the chain, would’ve required sequential and nested calls to closures to accomplish the same thing. With a Coordinator, the logic acts in one place and the view controllers merely have to describe what it is they want to have happen. This not only clarifies how this series of view controllers should present and dismiss as a group, but each individual view controller can be controlled easily in other contexts using the same delegate or an additional one. (Extending protocols is a great way to keep the delegate calls to a tight minimum.)

Additionally, because the Coordinator is based off protocols, it’s easy to discover the right thing to do. You don’t have to rely on hidden exit segues nor, really, storyboards at all. In fact, if your entire app uses the Coordinator pattern, you could easily build all your screens in individual XIB’s if you like. You won’t need any segue logic at all.

Conclusion

The Coordinator pattern isn’t for everybody and isn’t needed by every app. For most small apps, inline navigation code is perfectly fine. As your app gets bigger and, most importantly, view controllers get reused in various places, the Coordinator pattern is worth learning. Keep it in the toolbelt, you’re going to find some great uses for it!

  • Print
  • Facebook
  • Twitter

John Blanco

John Blanco is a freelance iOS developer living in Lakewood, Colorado. He's been developing mobile apps for over 15 years, beginning in the medieval days of Java ME and Blackberry and all the way through iPhone and Android! He's led development on dozens of apps across a wide variety of domains such as retail, vision, orthotics, games, sports, and more!

More Posts - Website

Follow Me:
LinkedIn

, , , ,

Comments are currently closed.