Interactive transitions in a modular iOS architecture
This article explores using Flow Controllers to navigate a modular multi-project iOS Swift application, and proposes using Flow Interactors to trigger flows and drive their transitions.
It assumes you know how to setup your application into a workspace with multiple sub-projects — using their frameworks as dependencies. If not, take a peek at this example to catch up.
Your app is going to be the next big thing. You’ve dedicated sleepless nights to its development — having developed a coffee-habit that contributes a considerable sum to Central America’s GDP.
Your well-planned sprint towards world-domination has given you the foresight to compose your codebase into layered, isolated feature modules.
With your app’s modularised codebase:
- Features are encapsulated
- Code is re-usable
- Code is robust — errors are caught before they propagate into dependent layers
- Features are easily A/B tested and canary-released
- Yada yada yada…
The benefits are clear.
However, with features being unaware of each other, how do I express controlling navigation and transitions between them?
ENTER FLOW CONTROLLERS
A Flow Controller’s job is to control the sequencing of transitions between your feature’s view controllers — navigating from one feature to the next — transferring state across feature boundaries.
It controls the flow.
A Flow Controller owns a UINavigationController that controls a view controller / view stack and provides methods to mutate that stack.
ENTER FLOW INTERACTORS
What the Flow Controller does not do, is interact with the user to trigger when and how a flow transitions to the next scene. That is the role what I call a Flow Interactor.
It is a class that binds user interaction (generally a gesture recognizer) to the view controller’s view and calls the given flow delegate method — triggering the Flow Controller to sequence a flow.
Flow Interactors are extended to conform to UINavigationControllerDelegate in order to inject custom animations and interactivity.
This object is the key that binds everything together — it is easy to conditionally inject different Flow Interactors for different environments.
So, if the UX team says, “we now want this screen to move to this screen on a double-tapped zig zag swipe — but only when they are using an iPhone XR” and you’ve only just setup a flow transition for a tap — you won’t need to blink, just say: “sure”.
After all, you’ll only adding/swapping a Flow Interactor, and maybe a Flow Interactor extension and Animator.
WHAT IT DOES
Our example application has two view modules — HomeScene and SecondScene — and a shared SharedEntites module containing the Colour entity.
HomeScene is the first scene that loads when the app starts. Tapping the screen will push the next module — SecondScene — onto the navigationController stack. The transition uses the default sliding animated transition built-in provided by UINavigationController. The Colour entity is pushed to SecondScene.
SecondScene displays the web colour of the HomeScene stored in the Colour struct. Swiping from the left edge will interactively pop SecondScene from the navigationController stack, returning the user to HomeScene.
And that’s it.
Absurd, I know. But it does demonstrate the building blocks for making more complicated navigation flows. As ultimately, all you will be doing is navigating from one scene to another by pushing, popping, presenting and dismissing, and transitioning with a jump cut (no animation), a default animation, a custom animation or an interactive animation.
And most importantly it demonstrates Flow Interactors as a solution for cleanly managing transitions.
Because we want to make this implementation protocol-oriented, we start by defining a protocol for Flow Controllers.
Now, let’s define a Flow Controller.
The Flow Controller conforms to FlowController and to the flow delegate protocols — HomeSceneFlowDelegate & SecondSceneFlowDelegate — as defined in each scene’s module.
Of note, is the — HomeSceneToSecondSceneTapFlowInteractor — which has the HomeScene and the HomeSceneDelegate conforming FlowController injected as it dependencies.
FLOW DELEGATE PROTOCOLS
The fromScene: parameter exposes the Flow Interactor, potentially needed to provide the UINavigationControllerDelegate to the flow delegate.
The content: parameter is the payload pushed by the HomeScene to the SecondScene. In this case, the shared entity — Colour.
SecondScene’s flow delegate method only has one parameter — the transition’s triggering view controller. This will again be used to expose the Flow Interactor, if required.
FLOW CONTROLLER EXTENSIONS
Simply adding the protocols — FlowController, HomeSceneFlowDelegate, SecondSceneFlowDelegate — to a class gives it this default implementation. Yay, protocols!
Having tapped on the HomeScene’s screen, the delegate method is called and the next scene — SecondScene — is built.
The delegate injects a Flow Interactor to the SecondScene. This particular one adds an edge swipe gesture to the scene. Importantly, it is a subclass of UIPercentDrivenInteractiveTransition, so it can drive interactive transitions back to HomeScene.
Additionally, the content value is also pushed to SecondScene.
Finally, SecondScene is pushed onto the navigationController stack with a default animation, as navigationController.delegate = nil and animated = true.
This flow delegate is triggered within the secondScene.edgeswipeFlowInteractor, as an edge swipe begins.
Because the Flow Interactor has an extension — shown below — that conforms to UINavigationControllerDelegate, it can now be used to interactively drive the animated pop mutation that removes the SecondScene from the navigation controller stack.
This Flow Interactor encapsulates the scene’s gesture recognizer which call the UIViewControllerInteractiveTransitioning methods that drive the lifecycle of a navigation controller’s interactive transition. In this case, beginning a left edge swipe calls the flow delegate method to return to the HomeScene.
FLOW INTERACTOR EXTENSION
Extending the Flow Interactor by conforming to UINavigationControllerDelegate offers a point to designate the Animator — a basic crossfade animation — used in the transition back to the HomeScene. Because interactionInProgress is true when the Flow Interactor began its gesture, the animation will be driven by that same gesture recognizer.
If you don’t want the animation to be interactive — but want to use the same Animator — simply return nil in the method that returns UIViewControllerInteractiveTransitioning? — and the navigationController will just use the animation and ignore interactivity.
So, there you have it. You’ve now got a highly configurable, easy to implement method for navigating and transitioning between feature modules.
The example app can be found here https://github.com/markjarecki/FlowControllerExample
Sample code in this article has been stripped from a lot of comments, line spaces and public access control declarations — for readability. More accurate implementation can be found in the linked source code.
This code is provided as-is.