Improved routing and data passing

Routing in Clean Swift is the one thing that I wanted to improve the most from the previous version. I’ll walk through a specific, concrete use case first, because it’s easier to read and understand when given a specific context. Then, I’ll generalize it to make it useful for any route in your app.

The specific use case we’ll look at is routing from the ListOrders scene to the ShowOrder scene when the user taps on an order in the table view.

The old way of passing data

While the old way of passing data worked, it didn’t make me proud when writing:

In the above code, we first get the selectedIndexPath of the table view. Next, we retrieve the selectedOrder from the orders array. Finally, we get the destinationViewController from the segue, cast it to the proper UIViewController subclass. The actual data passing is done by setting order to selectedOrder.

This works but there are a couple annoyances.

Having to go through the output

The router doesn’t have a direct reference to the output, the interactor. It only has a reference to the viewController:

In order to get to the orders array defined in the interactor, we have to write viewController.output.orders. Likewise, to get to the order variable, we have to write showOrderViewController.output.order.

We could add a direct reference to the interactor in the router. But I don’t like that. If every time we need access to something, we just add that something, it’ll soon turn into a monolithic class. A class having direct access to many things is destined for some serious abuse. This is the kind of ad-hoc programming that we absolutely want to stay away from.

Having to know the exact type of the destination view controller

We also need to cast segue.destinationViewController to ShowOrderViewController first. All the methods about presenting other view controllers in the UIViewController class reference don’t need to know the specific class type. The destination can be any UIViewController subclass.

Apple’s sample code does this type cast only because it uses the MVC pattern and stores data right in the view controller itself. But in Clean Swift, we don’t store data in the view controller. We store data in the interactor because business logic acts on the data.

If there’s no data in the view controller, why do we still need to cast to a specific UIVIewController subclass? After all, we simply need to be able to present a view controller using the built-in methods.

This old way of passing data worked. But I think we can do better.

A data store protocol just for your data

The interactor contains the scene’s business logic AND stores the data the business rules act on. They really are two separate things. Let’s break them free.

The source

For the ListOrders scene, the ListOrdersBusinessLogic protocol specifies both the business rule fetchOrders() and the data orders:

Let’s break it up into two protocols:

The interactor still defines the data as it did before, but it also needs to conform to the new ListOrdersDataStore protocol:

The destination

We’ll do the same for the ShowOrder scene. Pull out the data:

And add the new protocol conformance:

Attach the data stores to the routers

I mentioned above I didn’t like the idea of having a direct reference to the interactor in the router. But now we don’t have to. We can add a new reference to the data store in the router.

This is a good example of restricting access by the use of protocol. The dataStore variable is typed to ListOrdersDataStore. But the underlying object is really the interactor. But we can’t access anything else in the interactor except those explicitly defined in the ListOrdersDataStore protocol. This means we can only access the data that we intentionally specify.

The ListOrdersDataPassing protocol wraps the data store so that the router in the view controller doesn’t need to know too much about the router. It only needs to know the router has routing logic and data passing capability. A router is not typed to a specific type. Rather, it’s composed of a list of protocols to convey its functions:

The ListOrdersViewController and ShowOrderViewController‘s setup() methods attach the data stores to the routers.

Now, in the router, we can get to the orders array in the source scene, and the order variable in the destination scene, without involving the view controller.

Presenting view controllers in iOS

First, a quick overview of how presenting view controllers work in iOS.

When presenting a view controller, you can trigger it programmatically using one of the many UIViewController methods such as show(_:sender:) as in:

Or, you can use storyboard segues. You can perform a segue manually like:

But you can also create segues right in the storyboard. When you do so, you need to provide a segue identifier. Then, in the view controller’s prepare(for:sender:) method, dispatch to an appropriate route based on this segue identifier:

If your view controller has 10 outgoing routes, your prepare(for:sender:) method will have 10 conditional clauses. You can already see where this can lead to.

Clean Swift makes performing segues automatic

The ListOrdersRoutingLogic protocol defines two outgoing routes:

But ListOrdersViewController‘s prepare(for:sender:) method doesn’t get any longer even if it has 10 routes. That’s because it relies on convention over configuration:

If you name your segue identifier the same as your scene name, it makes use of Objective-C selectors and dynamic method dispatching to send an appropriate routeToSceneWithSegue(segue:) message to the router. (Note the same selector is written a little differently for Swift and Objective-C.)

In the case of ListOrdersRouter, if you name your segue identifier:

  • “ShowOrder”, when a segue is performed, it’ll invoke routeToShowOrder(segue:).
  • “CreateOrder”, when a segue is performed, it’ll invoke routeToCreateOrder(segue:).

Now you know why the router inherits from NSObject, and ListOrdersRoutingLogic is made @objc. Objective-C selectors only work when the object is an Objective-C class.

Routing = Data Passing + Navigation

The 3 steps of the routing process are:

  1. routeToNextScene(segue:)
  2. passDataToNextScene(source:destination:)
  3. navigateToNextScene(source:destination:)

When the routeToNextScene(segue:) method is called, it in turn invokes the passDataToNextScene(source:destination:) method, and then, if necessary (i.e. not a segue route), the navigateToNextScene(source:destination:) method.

This warrants an explanation.

A segue route is triggered by either the storyboard, or manually by calling performSegue(withIdentifier:sender:). The prepare(for:sender:) method will be invoked regardless. Next, the segue will be performed by iOS. That means you don’t need step 3. If you do make a call to navigateToNextScene(source:destination:), your destination view controller will be presented twice! You certainly don’t want that.

A non-segue route means you need to programmatically present your destination view controller, such as by calling show(_:sender:). This method call should be moved inside the navigateToNextScene(source:destination:) method.

Makes sense?

The new way of passing data

The route from the ListOrders scene to the ShowOrder scene is a segue route, so we only need to perform steps 1 and 2. The routeToShowOrder(segue:) method first grabs a reference to the destination store. Yes, we still need to jump through some hoops here to get to the ShowOrderDataStore instance. But we’re passing data here, so we must know the specific data store type to access the data within. And this hoop jumping is done in the routeToNextScene(segue:) method only. Your passDataToNextScene(source:destination:) and navigateToNextScene(source:destination:) methods are isolated from this detail.

Here’s the routeToShowOrder(segue:) method:

The code to pass a selected order from the ListOrders scene to the ShowOrder scene looks like:

Notice the inout? Since we’re changing data in the destination data store, we need to make it an inout parameter in the method signature. In effect, this makes it a var instead of a let.

After we retrieve the selected order from the source scene, passing it to the destination scene becomes very straight forward.

You can also create separate objects to be the data stores. But I find it easier to work with when the interactor implements both business logic and data stores. The business logic rules can act on the data in the object, instead of having to establish another API.

What about presenting view controllers?

This particular route is a segue route, so we don’t need to implement the navigateToNextScene(source:destination:) method. But if we were to make this a programmatic route, the navigateToShowOrder(source:destination:) method would look like:

We’re not changing the destination view controller. We simply present it. So we don’t need inout here.

If you like, you can also perform your own custom transitioning animations here.

You’ve just seen a specific route to understand how the new routing works. Next time, I’ll write about how to generalize this so this can work for all routing scenarios that you’ll encounter in your app. Stay tuned.

Get the Clean Swift Xcode Templates

Subscribe below to get my Xcode templates and learn how to apply the VIP cycle to your projects, extract business and presentation logic into interactor and presenter, navigate to different scenes using multiple storyboards, and write fast, maintainable tests with confidence to make changes.

I promise I'll never send you spam. You can unsubscribe at any time.

Raymond

I’ve been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

4 Comments

  1. Does this pattern work with unwind segues?
    If I’m not mistaken, unwind segues are run through the receiving (destination) view controllers… so this wouldn’t catch the ‘routeToXXXX’ in sending (source) view controller which would end up bypassing sending data backwards. right?

    1. Hi Gregory,

      An unwind segue is still a segue, right? The same thing happens.

      With a forward segue going from scene A to scene B, the sourceVC and sourceDS is A, and the destinationVC and destinationDS is B. With an unwind segue, the sourceVC and sourceDS is B, and the destinationVC and destinationDS is A.

  2. Why should router mine data from store? Why should router care, what indexpath is selected? You (well, we 🙂 ) use models to pass data structs between members of the VIP cycle. Shouldn’t controller just simply inject data struct as a parameter to the router (e.g. router.navigateToShowOrder(data: ShowOrderData)? Router than just hands the data to the next scene. Controller has all the necessary information for the next scene, and knows what to send up the next scene. Let the controller just hand the data to the router, instead of bothering the router with indexpath and (possibly complex) model data structure. I have done this in last few apps, and it seems to work well so far. This way the router is totally decoupled from interactor and only manages routing and passing the data. Not preparing the data. But maybe I am missing something. What is your opinion on this?

    1. Hi Vilém,

      Why does the view controller need to do order = interactor.orders[index]? A view controller should only deal with display logic. Finding an order to show in the next scene is not really a responsibility of the view controller. It’s the job of the interactor.

      Assume we choose not to use segue, but instead use IBAction to trigger the route. We can create a SelectOrder use case and have the interactor save this selectedOrder. When the VIP cycle completes and comes back at displaySelectOrder(), we can then trigger the route: router.routeToShowOrder(). In your passDataToShowOrder() method, you only need to do destination.order = source.selectedOrder.

      This makes total sense. You can certainly do it this way. I would even argue you should do it this way, especially when you need to do more than just remembering the selected order to show, such as when you want to refresh the order to make sure it has the latest attributes, or when you need to pull associated data such as payment info that’s not available in the ListOrders scene.

      However, in the case of the CleanStore app, I use storyboard segue and don’t need to do anything extra. So I choose to stick this little bit of logic in the router. After all, it’s a two-liner in the passDataToShowOrder() method. It’s already abundantly clear to me what it’s trying to do by the name of the method.

Leave a Comment

Your email address will not be published. Required fields are marked *