Seaworthy mobile applications know how to navigate

Christoffer Winterkvist
Fink Oslo
Published in
12 min readApr 21, 2022
alonsoreyes - https://unsplash.com/photos/mG_rp41aYqM

Ahoy fellow buccaneers! You might have been in this game a long while or you might just be starting out, whichever rings true, I think we can agree that the following statement is universally true for mobile applications:

I reckon we can all agree that navigation be a cornerstone o’ any application. We often loot it fer granted ‘n don’t really try t’ scale it ’til th’ needs occurs which often comes as a curveball ‘n gives us wee time t’ iterate or optimize.

I think that is quite enough pirate speak for one blog post 🏴‍☠️
What I intended to say was:

I think we can all agree that navigation is a cornerstone of any application. We often take it for granted and don’t try to scale it until the need occurs, which often comes as a curveball and gives us little time to iterate or optimize.

What if we took a different approach to navigation and thought about it as a feature rather than a system-provided action. This article is not meant to force you into a direction, the following will show abstract implementations written in Swift and is meant as inspiration rather than forcing your hand. Keep in mind that new technologies like SwiftUI will flip the table, with that said there are still lessons to be learned here even if you are bleeding edge.

Don’t let the Swift language throw you off, the things we are going to go through here can be applied to other platforms with the help of your imagination.

What this means is that if the examples contain any holes, it is up to you to fill them in based on your current requirements, constraints, and needs. However, we will try to add some inspirational new features that you might not have thought about earlier that can open new doors when it comes to developing your application.

We can start by making a small list of criteria and requirements for making the feature successful both for our users and ourselves.

Mission statement aka pirate manifesto

  • Should be type-safe
  • Decouple and create clear boundaries between features
  • Benefit the users and the developers
  • Increase re-writability of our application

What makes navigation type-safe and why is it important? Well, if we play our cards right, this should add clarity to our navigation stack as both new and old developers should be able to figure out how UserA got from A to Z.

And what about decoupling, why is that relevant? Imagine having a ship with the bolts wedged together, it will make it unnecessarily hard to make repairs. The same goes for your application, with clear boundaries, it will be easier to repair or refactor (whichever word floats your boat).

The last two points are more side-effects and we will get to those later in this article. If things still sound vague, don’t worry, let’s get the show on the road so that we gain more clarity.

tjump - https://unsplash.com/photos/rkFIIE9PxH0

Destination

Let us start with a very basic model, we will start with making an enum and giving it a proper name. When we navigate the seven seas, we want to be able to put an X on the map to illustrate where we are going, however, X doesn’t make for a very descriptive model name so lets instead go with Destination. Yeah, I think that will do nicely. So our barebones model will look something like this.

Tidy. Now let us plot some X:es into our application.

But before we do that, let us take a quick step back and think about why an enum would be an appropriate choice instead of a regular struct or class. Enums are bound to a finite set of cases which would translate perfectly to our applications navigation stack as it is also finite. Enums are also super easy to extend by simply adding more cases to them. In addition, at least for the case of Swift, we can add one or more associated values to each case. This is super useful if we need to pass information from one place to another.

From a developer perspective, when using a decent editor, we will get code completion for all available cases which are both great for discoverability and overall developer happiness.

Last but not least, when we intend to use the enum, we do get the option to either unwrap the current value using an if statement but the preferred way would be to simply switch over the enum to cover all the cases for our current use case (pun intended). So if we leave one case unfulfilled, it will result in a compiler error rather than a runtime crash.

Can you smell that? That is the delicious fragrance of type-safety that we aim for, let us keep cooking!

This would tick off at least three (if not all) boxes in our mission statement.

To make this a bit less abstract and more fun, imagine that the application that we are building is a Pirate trading app, let’s call it Pirate Bay.

The user… err I mean… the scoundrel (or scruffy-looking nerf herder for all our space pirates out there) should be able to do the following:

  • Have a home screen, a place to collect their thoughts and get an overall overview of current trades, etc.
  • Scout the marketplace for treasures
  • Look at individual items
  • Check sellers and buyers profiles
  • Manage their profile
  • A login page
  • A log out action
  • Download user information
  • And last but not least, a way to opt-out from the service

(Even if we are pirates, we need to align ourselves with GDPR to avoid walking the plank)

So let us take a look at what that might look like by extending our Destination enum.

That looks pretty neat.

By glancing at the enum, we get a sense of the size of our application. And if we are completely foreign and this is the first time we open the project, we can even read out the purpose of the app, all without adding a single comment to our very bare-bones implementation. Now that is some self-documenting piece of treasure right there.

But there is one thing that breaks the type-safety, can you spot it? Take a second before you continue, what could potentially go wrong here?

Both the item and profile case take raw String as its associated type, this could potentially lead to situations that we don’t want. Take for example if we try to navigate to an item using a profile identifier, this really shouldn’t happen, but with the current implementation, it could. Even if that would be highly unusual.

I’ve jinxed myself far too many times by uttering the following words: “That would never happen”.

So before we continue, let us quickly look at how that could potentially be fixed by taking our type safety to the next level and earning ourselves the pirates badge of honor.

The fix isn’t that hard to apply, we simply need to wrap our string-based navigation in another value type that is unique to that case (this might be a tad unique to Swift, but bear with us).

So instead of using String, let us introduce our value types, one for each case.

Here are some appropriate new types for our pirate application:

With the new types in place, let’s update our enum.

If we want to extend this even further, both Loot & Pirate could be protocols, that way our cases could tackle polymorphism without growing out of proportion and eventually becoming a Kraken.

Now that we know where we are going, we need someone to take us there. This brings us to the next piece of the puzzle.

seshareddy — https://unsplash.com/photos/Go5qDQJQSU4

Navigator

Every good ship needs a good captain

As many of you already know, vanilla navigation is done by either presenting on top of a view controller or relying on UINavigationController being the parent of your view controller.

Let’s take this vanilla example where we have a list of pirates and when you tap a pirate entry, you should display the details for the selected pirate. It might look something like this:

This works fine, however, remember rule number two in the pirate manifesto:

You don’t talk about pirate club, ops! … wrong franchise, I meant: Decouple and create clear boundaries between features, savvy?

As seen in the example, ListViewController needs to know about DetailViewController and its requirements. At first glance, this might not feel like a huge deal, the example isn’t a lot of code, but trust me land crab, the seas can be stormy and unfriendly. So when your application grows, and it will, you can quickly find yourself running out of ink while drawing your map. Making it feel hopeless to untangle, refactor, redraw or provide additional routes. Almost like working with something that is edged in stone, and we don’t want that.

So for our pirate application, we need the best of the best. We need the Navigator!

It doesn’t look like much yet so let’s fill in some blanks. As mentioned, the way to perform navigation is by using UINavigationController, so we will need one of those aboard our ship. However, we don’t need to expose that in our public API, the features shouldn’t really care as this is an implementation detail for our top dog class but alas one is needed.

Now that we have an opportunity to provide a solid public API for our app’s cornerstone, let’s make it easy enough that we can still reason about it even after a night of grog and looting.

We now have a super-easy way of handling navigation for our list and detail example, but before we move on, Let’s just take that example and apply our Navigator to it.

This example didn’t save us a lot of lines of code, but that is also not the point. What we did gain from this approach is that ListViewController no longer knows about DetailViewController without losing the ability to perform navigational tasks. In addition, just by reading the instance variable definition of the class, we can see without looking at any function bodies that this class can perform navigational tasks. This is highly underrated and the benefits become clearer as your code grows but the clarity of the controller will remain. The same might not be as true for the vanilla example that lacks the reference.

As for increased refactor-ability (if that is even a word), we could refactor the DetailViewController in isolation, without having to change anything in our ListViewController, as long as the contract between the view controller and use of Destination stays intact.

So far we have checked off the following criteria:

Type-safety
Leveraging from enum with associated value types, we can ensure that both ends uphold a contract when handing off information between controllers. It also defines a finite set of destinations that paints a beautiful map of our application.

Decouple and create clear boundaries

Adding the Navigator into the mix and giving it full control over what to present and where, offloads any excess knowledge that the controller used to have, just to get to where we were supposed to go. We can go as far as to state that navigation is a “fire-and-forget” type of deal for our controllers, which is well within the boundaries for any sea roaming scoundrel.

Increase re-writability of our application

By having clear boundaries between the cornerstone foundation and the features of the application, there is less to rewrite and fix up because we have fewer places where we create new instances of classes and structs.

Benefit the users and the developers

As developers, we gain type-safety, a more scalable foundation, something that is easier to rewrite or reroute. Everything mentioned should ultimately lead to a better application where there is more time to focus on the details.

This is all great, but I think that we can do better, don’t you?

For the users

Let’s revisit our previous implementations to see if we can’t take this to the next pirate-level.

All good things are three, so lets focus on the following things when trying to improve our already awesome application.

  • State restoration
  • Deep linking
  • Improved crash reports

State restoration

One feature that can set an application apart is the use of state restoration. So when the application is manually closed by the user or forced quit by the system, the previous application state can be obtained and restored when the application is launched again.

In order to do this, we need something that can represent the bread-crumbs of the navigation steps that we can perform throughout the application. Luckily for us, we have Destination that holds their own unique value and can be translated into a navigation action. There are multiple ways to skin a ferret but the sake of simplicity and not to argue one is better than the other, let’s just make this happen with the built-in Codable protocol.

Conforming with our current example is super easy, we simply add Codable to all of our current models, like this:

We now gain the ability to both encode and decode individual or collections of Destination’s.

It couldn’t be easier, and we can now pick if we want to store it as Data or String. We can also decide if we want to store it in UserDefaults or directly to disk, both are valid options.

This is only the first part of the puzzel, we also need to keep a source of truth of the current navigation stack so that the application can save it at a moment’s notice. A natural place to keep this current stack is on the Navigator. Let’s modify it ever so slightly just to populate the collection of Destination’s when a navigation action is performed:

“wolde you bothe eate your cake, and have your cake?”

Easy-peasy pirate squeezy. But wait, we also need to update the current stack when the user pops view controllers of the screen. Sadly, there are no good delegate calls on UINavigationController to hook into, but fear not dear sea captain, we can leverage from inheritance which can let us have our cake and eat it too.

Now, all we need is to use this freshly brewed class and become the delegate, back to the Navigator:

A tad more code but now we route all invocations of both push and pop to our Navigator so that it is always in sync. From here on, it is up to you to find the best fit for your application on where you want to store and restore the navigation stack as your application relaunches.

“Kill two parrots with one stone”

Deep linking

Let us move on to the next bonus feature, which is deep linking. Right now, our navigator can only open one Destination at a time based on the current API. So by adding one convenience method on Navigator, we can improve the call-site implementations by providing a new function that takes a collection of Destination’s. This method may look something like this:

So if we wanted to build up the entire stack again, we can simply call it with X amount of destinations and voilá, we now have the same view hierarchy as before.

As for navigating using a URL scheme, well here you need to implement your own string parsing methods that will return the correct destination or destinations based on the URL. When those method(s) have produced the correct output, then simply pass the result into the newly created function.

Improved crash reports

Based on the state restoration and the way you implemented deep linking, we can now sprinkle a bit of extra spice into our crash reports, and provide a set of deep links that the application can interpret. If we attach these to crash reports, we get both breadcrumbs and a way navigate in the same way as the user did up until the crash occurred. This can give you great insight into what was going on before your ship got blown out of the water, and relying what happened can even help pinpoint the exact location where the first cannon ball hit the hull.

I hope this was helpful and I can’t wait to see what glorious vessels you will produce in the future.

When I was one,
I sucked my thumb,
The day I went to sea.
I climbed aboard a pirate ship
And the Captain said to me:
‘We’re going this way, that way,
Forwards
backwards,
Over the Irish Sea.
A bottle of rum to fill my tum
A Pirates’ life for me’.

Have a good one Mateys!

--

--

Christoffer Winterkvist
Fink Oslo

random hero at @fink-oslo by day, cocoa vigilante by night, dad at dawn. my life is awesome. Previously @hyperoslo