Flow coordinator pattern on steroids
By: Pavle Pesic
Tags: iOS, Swift, Programming, iOS App Development, Mobile

Flow coordinator pattern is excellent for structuring app navigation and making code clean and reusable. However, it has some flaws too. The coordinator is not working great with native navigation and back swipes.

The goal

The goal is to modify flow coordinator so we can use it both with custom and native back action, as well as swipe back. However, it’s a necessity to maintain clean code which is easy to read, reuse and upgrade.

The problem

The major problem with UINavigationController is that clicking on the default back button, or navigating back with a swipe, pops the view controller and the coordinator is not aware of it.

There are many answers to the problem. Here are Khanlou’s and Ian MacCallum’s. The solution you are about to see is a bit different. Look all three and choose what suits you the most.

The solution concept

As you already know, controllers don’t know anything about the coordinator. They only expose an interface that informs the coordinator when navigation should happen. That means only view controllers should notify coordinator. Default back action on UINavigationController isn’t informing anyone, so that needs to change.

If you use swipe as back action problem is a bit different. You need to catch the moment when a back swipe finishes the transition, to notify the coordinator. But unlike in the previous example, the coordinator shouldn’t do the popping only action after it if needed.

Implementing the solution

Handling default back action

If we want to change default back action, we can do it by changing the controller’s navigationItem. Every controller needs to implement this. We could either do this in BaseViewController (base class for every controller in the app) or custom UINavigationController. I think a better solution is to do this in custom UINavigationController because we can also change the design of a back button and controller title.

In the dependency container (more about it here), you should call the customizeBackButtonMethod. We need to save those values as we are going to use them more than once.

We need to customize every controller. We can do it by overriding the pushViewController method because it has a pointer to the next view controller. Here you should call setupCustomBackButton method.

There we are initializing custom back button and setting an action for it. After that, you have to change the view controller’s navigationItem by setting leftBarButtonItem as the custom back button.

The last thing to do is to create action for our custom back button. Clicking on the button, we need to inform the view controller, and view controller should inform the coordinator. The best way is to use the delegation pattern. So we should create CoordinatorNavigationControllerDelegate protocol and define didSelectCustomBackAction method. Now we can define weak delegate property in CoordinatorNavigationController.

Our custom button will call actionBack when clicked. There we use the delegate property we’ve just created and call didSelectCustomBackAction method.

CoordinatorNavigationController is completed, now we have to implement CoordinatorNavigationControllerDelegate in our view controllers. Because every view controller needs to extend the delegate, we should do it in BaseViewController.

In view controller’s viewDidAppear method we should set the delegate. We have to do this in the viewDidAppear method for two reasons: only one controller at the time can be CoordinatorNavigationController’s delegate, and when navigation does go back to a controller, we need to set the delegate again. We could set it in the viewWillAppear method, but we would have a problem with the swiping back. I will address this problem later.

Example

As you can see AuthCoordinator knows about four view controllers ChooseLoginRegisterViewController, LoginViewController, RegisterViewController and TermsAndConditionsViewController. WalktroughCoordinator knows only about WalktroughViewController. In real the app example, WalktroughCoordinator wouldn’t be a child of AuthCoordinator. We need to implement this flow.

App Structure

In RegisterViewController we need to override didSelectCustomBackAction and to notify the coordinator. Then the coordinator needs to pop the view controller.

In WalktroughViewController we again need to override didSelectCustomBackAction. In WalktroughCoordinator we are poping view controller and calling finish flow. AuthCoordinator then removes WalktroughCoordinator from its childCoordinators array.

There you have it. The coordinator is now working great with the native navigation bar. Moreover, as you can see in the example, you can customize the back button. Our code is still clean and reusable. For you who want to test the solution before we start swiping back, here is code example.

Handling swipe back

First of all, we have to make a public function for enabling back swipe. We are going to do it in CoordinatorNavigationController.

Now we need to add a new method to our CoordinatorNavigationControllerDelegate protocol. We should call that method when swipe back is finished to notify the view controller that change has happened. Then the view controller needs to notify the coordinator.

Next step is to catch the moment when a back swipe is over and to call transitionBackFinished method. We should use UINavigationControllerDelegate for catching that moment. That means we have to set the delegate in the viewDidLoad method. If you have read my previous articles about the coordinator, you remember that the Router used to extend UINavigationControllerDelegate. We have to change it.

In the delegate’s method navigationControllerWillShowViewController, firstly we have to make sure that transition isn’t canceled (we could start back swipe and then decide to stay on the same controller). If it isn’t, we call transitionBackFinished method.

Note: When I was talking about setting CoordinatorNavigationControllerDelegate property in BaseViewController, I’ve mentioned that we couldn’t set it in the viewWillAppear method. The reason is that the viewWillAppear is called immediately we start swiping back. We don’t want that.

When a transition is over, we set duringPushAnimation to false — that property we use in UIGestureRecognizerDelegate methods. We don’t want to allow the user to swipe back during push animation.

If we use custom transition, we want to disable back swipe because we are not sure how is the state of UI on the previous screen. This part is optional, so change it if you want to. For more information about flow coordinator and custom transitions, you can read it here.

Example

We will use the same flow from above.

RegisterViewContoller doesn’t need to override transitionBackFinished method. Back swipe will pop RegisterViewContoller, and we don’t need to inform the coordinator about it. There is no action needed for that use case.

We show LoginViewController with the fade transition. Swipe back doesn’t work on that controller as we mentioned earlier. Even if you enabled swipe back for it, we wouldn’t need to override transitionBackFinished method for the same reasons as above.

However, in WalktroughViewController we have to. When going back from WalktroughViewController, as we mentioned earlier, AuthCoordinator removes WalktroughCoordinator from its childCoordinators array, so we have to notify the WalktroughCoordinator that swipe back occurred.

Using shouldPopViewController property in the onFinishWalktrough variable, we will determine should we pop view controller before we call finish flow.

Finally, we’ve finished! The coordinator works excellent with the native navigation bar and the swipe back action. Again, here is the code example.

Conclusion

We’ve removed some of the flow coordinators most significant flaws. On the other hand, we’ve made the pattern even more complicated. However, I think the tradeoff is good.

I hope you enjoyed this approach and found some useful ideas. If you have questions, or you see some places for improvement, please let me know.

Resources & Reading list