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 —
SecondScene — and a shared
SharedEntites module containing the
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
Colour entity is pushed to
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
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 —
SecondSceneFlowDelegate — as defined in each scene’s module.
Of note, is the —
HomeSceneToSecondSceneTapFlowInteractor — which has the
HomeScene and the
FlowController injected as it dependencies.
FLOW DELEGATE PROTOCOLS
fromScene: parameter exposes the Flow Interactor, potentially needed to provide the
UINavigationControllerDelegate to the flow delegate.
content: parameter is the payload pushed by the
HomeScene to the
SecondScene. In this case, the shared entity —
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 —
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
Additionally, the content value is also pushed to
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
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
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.