Last time, we looked at the route from the ListOrders
scene to the ShowOrder
scene in details. This time, we’ll examine all the routes from all the scenes in the CleanStore app, so that you can see how the new and improved router handle other situations.
Routing in Clean Swift is a simple 3-step process:
- Getting a hold on the destination
- Passing data to the destination
- Navigating to the destination
Every route goes through these 3 steps. Step 2 is optional if you don’t have any data to pass. Step 3 is also optional if you use storyboard segue. So routing can be even simpler than this 3-step process.
In other words, if your route is based on storyboard segue and you don’t need to pass data, you can skip steps 2 and 3. If you skip them, that means you don’t need the destination references, and you can skip step 1 too. You don’t have to do anything!
When we look at each route using this 1-2-3 process below, even when some of these steps are optional, I still choose to implement them, so you can see the full picture.
Routes from the ListOrders
scene
From the ListOrders
scene, the user can tap the Add button to create a new order. The user can also tap an order row to view the order details. So the ListOrdersRoutingLogic
protocol declares these two routeTo
methods:
1 2 3 4 5 6 |
@objc protocol ListOrdersRoutingLogic { func routeToCreateOrder(segue: UIStoryboardSegue) func routeToShowOrder(segue: UIStoryboardSegue) } |
Create a new order
Step 1. Getting a hold on the destination (routeToCreateOrder(segue:)
. You can get the destination view controller from the segue as usual, and the destination data store from the view controller’s router.
1 2 3 4 5 6 7 8 9 |
// MARK: Routing func routeToCreateOrder(segue: UIStoryboardSegue) { let destinationVC = segue.destination as! CreateOrderViewController var destinationDS = destinationVC.router!.dataStore! passDataToCreateOrder(source: dataStore!, destination: &destinationDS) } |
Step 2. Passing data to the destination (passDataToCreateOrder(source:destination)
). There is no data to be passed, so the method is empty.
1 2 3 4 5 6 |
// MARK: Passing data func passDataToCreateOrder(source: ListOrdersDataStore, destination: inout CreateOrderDataStore) { } |
Step 3. Navigating to the destination (navigateToCreateOrder(source:destination
). Navigation is already taken care of by the segue. However, I implement this method with source.show(destination, sender: nil)
, which is the default, catch-all way of presenting view controller. When you trigger this route programmatically by invoking routeToCreateOrder(segue:)
directly, this will be used to present the destination view controller.
1 2 3 4 5 6 7 |
// MARK: Navigation func navigateToCreateOrder(source: ListOrdersViewController, destination: CreateOrderViewController) { source.show(destination, sender: nil) } |
Show an existing order
Step 1. Getting a hold on the destination (routeToShowOrder(segue:)
. You can get the destination view controller from the segue as usual, and the destination data store from the view controller’s router.
1 2 3 4 5 6 7 8 9 |
// MARK: Routing func routeToShowOrder(segue: UIStoryboardSegue) { let destinationVC = segue.destination as! ShowOrderViewController var destinationDS = destinationVC.router!.dataStore! passDataToShowOrder(source: dataStore!, destination: &destinationDS) } |
Step 2. Passing data to the destination (passDataToShowOrder(source:destination)
). The order of the selected table view row is passed to the destination data store.
1 2 3 4 5 6 7 8 |
// MARK: Passing data func passDataToShowOrder(source: ListOrdersDataStore, destination: inout ShowOrderDataStore) { let selectedRow = viewController?.tableView.indexPathForSelectedRow?.row destination.order = source.orders?[selectedRow!] } |
Step 3. Navigating to the destination (navigateToShowOrder(source:destination
). Navigation is already taken care of by the segue. However, I implement this method with source.show(destination, sender: nil)
, which is the default, catch-all way of presenting view controller. When you trigger this route programmatically by invoking routeToShowOrder(segue:)
directly, this will be used to present the destination view controller.
1 2 3 4 5 6 7 |
// MARK: Navigation func navigateToShowOrder(source: ListOrdersViewController, destination: ShowOrderViewController) { source.show(destination, sender: nil) } |
Routes from the CreateOrder
scene
From the CreateOrder
scene, the user can tap the Save button to create (if new) or update (if existing) the order, and go back to the previous scene. For a new order, the user goes back to the ListOrders
scene, whereas for an existing order, the user goes back to the ShowOrder
scene. So the CreateOrderRoutingLogic
protocol declares these two routeTo
methods:
1 2 3 4 5 6 |
@objc protocol CreateOrderRoutingLogic { func routeToListOrders() func routeToShowOrder() } |
Save a new order
Step 1. Getting a hold on the destination (routeToListOrders(segue:)
. Since you already know the source view controller needs to be popped off the navigation controller stack, the destination view controller is simply the view controller beneath the top view controller.
1 2 3 4 5 6 7 8 9 10 11 |
// MARK: Routing func routeToListOrders() { let index = viewController!.navigationController!.viewControllers.count - 2 let destinationVC = viewController?.navigationController?.viewControllers[index] as! ListOrdersViewController var destinationDS = destinationVC.router!.dataStore! passDataToListOrders(source: dataStore!, destination: &destinationDS) navigateToListOrders(source: viewController!, destination: destinationVC) } |
Step 2. Passing data to the destination (passDataToListOrders(source:destination)
). There is no data to be passed, so the method is empty.
1 2 3 4 5 6 |
// MARK: Passing data func passDataToListOrders(source: CreateOrderDataStore, destination: inout ListOrdersDataStore) { } |
Step 3. Navigating to the destination (navigateToListOrders(source:destination
). After the new order is saved, we pop the top view controller off the navigation controller stack.
1 2 3 4 5 6 7 |
// MARK: Navigation func navigateToListOrders(source: CreateOrderViewController, destination: ListOrdersViewController) { source.navigationController?.popViewController(animated: true) } |
Update an existing order
Step 1. Getting a hold on the destination (routeToShowOrder(segue:)
. Since you already know the source view controller needs to be popped off the navigation controller stack, the destination view controller is simply the view controller beneath the top view controller.
1 2 3 4 5 6 7 8 9 10 11 |
// MARK: Routing func routeToShowOrder() { let index = viewController!.navigationController!.viewControllers.count - 2 let destinationVC = viewController?.navigationController?.viewControllers[index] as! ShowOrderViewController var destinationDS = destinationVC.router!.dataStore! passDataToShowOrder(source: dataStore!, destination: &destinationDS) navigateToShowOrder(source: viewController!, destination: destinationVC) } |
Step 2. Passing data to the destination (passDataToShowOrder(source:destination)
). The updated order is passed to the destination data store so that the order details are refreshed.
1 2 3 4 5 6 7 |
// MARK: Passing data func passDataToShowOrder(source: CreateOrderDataStore, destination: inout ShowOrderDataStore) { destination.order = source.orderToEdit } |
Step 3. Navigating to the destination (navigateToShowOrder(source:destination
). After the new order is saved, we pop the top view controller off the navigation controller stack.
1 2 3 4 5 6 7 |
// MARK: Navigation func navigateToShowOrder(source: CreateOrderViewController, destination: ShowOrderViewController) { source.navigationController?.popViewController(animated: true) } |
Go back
When the user taps the Back button, the currently visible view controller is dismissed natively by iOS and no data needs to be passed.
Routes from the ShowOrder
scene
From the ShowOrder
scene, the user can tap the Edit button to update an existing order. So the ShowOrderRoutingLogic
protocol declares this routeTo
method:
1 2 3 4 5 |
@objc protocol ShowOrderRoutingLogic { func routeToEditOrder(segue: UIStoryboardSegue) } |
Edit an existing order
Step 1. Getting a hold on the destination (routeToEditOrder(segue:)
. You can get the destination view controller from the segue as usual, and the destination data store from the view controller’s router.
1 2 3 4 5 6 7 8 9 |
// MARK: Routing func routeToEditOrder(segue: UIStoryboardSegue) { let destinationVC = segue.destination as! CreateOrderViewController var destinationDS = destinationVC.router!.dataStore! passDataToEditOrder(source: dataStore!, destination: &destinationDS) } |
Step 2. Passing data to the destination (passDataToEditOrder(source:destination)
). The order is passed to the destination data store to be edited.
1 2 3 4 5 6 7 |
// MARK: Passing data func passDataToEditOrder(source: ShowOrderDataStore, destination: inout CreateOrderDataStore) { destination.orderToEdit = source.order } |
Step 3. Navigating to the destination (navigateToEditOrder(source:destination
). Navigation is already taken care of by the segue. However, I implement this method with source.show(destination, sender: nil)
, which is the default, catch-all way of presenting view controller. When you trigger this route programmatically by invoking routeToEditOrder(segue:)
directly, this will be used to present the destination view controller.
1 2 3 4 5 6 7 |
// MARK: Navigation func navigateToEditOrder(source: ShowOrderViewController, destination: CreateOrderViewController) { source.show(destination, sender: nil) } |
Go back
When the user taps the Back button, the currently visible view controller is dismissed natively by iOS and no data needs to be passed.
Repeatable
You probably notice from above that this is kind of boring and repetitive. You’re doing almost the same thing every time.
But that’s the point!
Routing is boring. It navigates to another scene and passes the data it needs. Every route needs to do these two things.
This 3-step routing process gives you a system that you can rely on. It saves your creativity juice for the more interesting thing such as business logic in your interactors and workers.
Flexible
Another benefit of following this 3-step routing process is its flexibility. You have the option to trigger a route in 3 different ways:
- Automatic segue
- Manual segue
- Programmatic
With an automatic segue, you drag from a UIControl
to the destination scene. The segue is performed by iOS when the user taps the UIControl
. So step 3 is skipped.
With a manual segue, you drag from the view controller in the source scene to the destination scene. And you invoke the perform(for:sender:)
method. The segue is also performed by iOS. So step 3 is skipped.
With a programmatic route, you call the router’s routeTo
method directly. This allows you the most freedom to do whatever you want. But you have to provide the how’s in steps 3.
This 3-step routing process works for any of these situations. It’s a rare one size fits all.
Thanks Raymond for awesome explanation & great improvement over the previous method of routing mechanism !!!
Please suggest if there is a better way to not pass ‘references’ in the following data passing call:
var destinationDS = destinationVC.router!.dataStore!
passDataToShowOrder(source: dataStore!, destination: &destinationDS)
Also suggest & explain if it’s better to keep the below logic in Router or in ViewController(since clicked UI component is in ViewController, the router could be passed the selected Order object, thus avoiding the clicked UI information in Router):
let selectedRow = viewController?.tableView.indexPathForSelectedRow?.row
destination.order = source.orders?[selectedRow!]
Thanks in advance.
Hi Arpit,
The premise of the new routing is to cleanly separate the two functions: navigation and passing data. Grabbing the references so that you can pass the source and destination objects into the
navigateToSomewhere()
andpassDataToSomewhere()
methods, and getting the relevant data through the view controller reference already in the view controller are the tradeoffs you have to make. The new routing system separates the functions into logical steps so that it’s clear what you need to do at each step. It’s this systematic approach that prevents you from doing the quick and dirty hacks.Can it still be improved upon? Absolutely. I’ve done a lot of experimentation before this latest template update. But they’re not ready for release. I had to scrap many of my ideas. When it becomes more appropriate, I’ll update the templates again.
Hi, Raymond:
I do like and learn a lots from your clean swift framework, but I encountered
a problem and would like to have some kindly suggestion from you if possible.
I am building a login page, and after login, the user would be redirected to a home view controller (HomeViewController) with 2 container views.
Now after entering correct email and password, I would like to fetch a corresponding user object and pass to a view controller in one of the container views inside the home view controller, below is the code inside the LoginRouter:
But I found that when this method was executed, the home view controller had not been initiated yet so the childViewControllers collection was empty, so there would be a fatal error when assigning the destinationVC variable.
I figured out a way to store the user data inside the UserDefaults and do not pass the data, but I still would like to ask you if there is a way to pass data in this kind of situation (The data receiver is a childviewcontroller of the destination of segue)
Thanks in advance
Hi Raymon:
I also figured out another way to do the data transfer, just to make the master view controller that contained the 2 container views a complete VIP cycle, so I will have a complete
HomeViewController
HomeInteractor
HomePresenter
HomeRouter
Instead of a single a old school Xcode generated
HomeViewController: UIViewController
but of course the HomePresenter was never used.
Is this approach a little overkill?
Hi Travis,
When using container views, both the parent and child scenes should have their own separate VIP cycles. So it’s totally fine.
Hi Travis,
Do you have this issue when using a segue? From what you described, it seems like it only happens in the
else
clause, which is the non-segue case. In yourHomeViewController
, you then need to make sure you set up your container views when it’s being initialized. That should resolve the issue you have.Hi Raymond,
I was having a problem on the destinationVC.router!.dataStore! in the router module. It displays Fatal error: Unexpectedly found nil while unwrapping an Optional value. Is there any way to avoid this error? Thanks!
I fixed it by adding
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
setup()
}
Thanks Raymond