Categories
Generics iOS Swift UIKit

Navigating using flow controllers and responder chain on iOS

Every app consists of different flows for achieving a specific goal. For example, there is a sequence of views for sign up. When sign up flow ends, we need to move to so called main view what represents the main functionality of the app. There are definitely a lot of different ways how to handle app navigation and each of the approach have their own pros and cons. With this in mind, the approach I am going to demonstrate this time, is how to use flow controllers and using responder chain to connect flows to each other.

Navigating from flow to flow

Flow controller is a UIResponder coordinating a single flow in an app. It handles showing views, injecting dependencies and storing intermediate values required to pass from one view controller to another. Navigation from one flow to another happens using responder chain. This also enables us to use sending actions to nil first responder in story boards and xib files. Using responder chain adds extra flexibility when restructuring an app or changing flows a lot. There is only a little code needed to set up new flows. It is a kind of lightweight approach to coordinator pattern where delegation is replaced with responder chain.

Setting up protocols

As a first step we define two protocols: one for defining entry points and the second one for accessing flow controllers from any responder in responder chain. Flow controllers are going to conform to specialised presenting protocol and responders (typically view controller), which trigger navigation, conform to one of the controlling protocols. FlowPresenting protocol is going to define a single function what is used for presenting a first view in the flow. I find examples to be the best way of learning new approaches, therefore let’s build a sample app with three flows: app, login and main flow.

protocol FlowControlling {}
protocol FlowPresenting {
func showInitialView()
}

Controlling app flow

AppFlowController is the controller handling presenting new flows. It conforms to AppFlowPresenting protocol what defines entry points to all the different flows. In addition, it handles inserting active flow to responder chain which enables easy access to flow controller from any presented view controller. Finding a flow controller from responder chain is implemented using a generic function. This enables implementing new getters for other flow controllers with just one line.

protocol AppFlowControlling: FlowControlling {
var appFlowController: AppFlowPresenting { get }
}
protocol AppFlowPresenting: FlowPresenting {
func showMainView()
}
extension AppFlowControlling where Self: UIResponder {
var appFlowController: AppFlowPresenting {
return flowController()
}
}
extension UIResponder {
func flowController<T>() -> T {
var current: UIResponder? = self
repeat {
if let presenter = current as? T {
return presenter
}
current = current?.next
} while current != nil
fatalError()
}
}

In the current sample app, AppFlowController is going to handle representing all of the flows in the app. This is because all the current flows are consisting of one branch in a so called tree of flows. If we would have a more complex application, other flow controllers would handle their own subset of flows and would insert those flows into responder chain (like AppFlowController is inserting LoginFlowController and MainFlowController into responder chain). This kind of architecture allows creating a tree like structure of flows and separating them from each other – there is not going to be a single controller handling all the flows.

final class AppFlowController: UIResponder, AppFlowPresenting {
private let dependencyManager: DependencyManager
private let window: UIWindow
init(window: UIWindow, dependencyManager: DependencyManager) {
self.dependencyManager = dependencyManager
self.window = window
}
// MARK: Presenting Flows
func showInitialView() {
let controller = LoginFlowController(window: window)
controller.showInitialView()
activeFlow = controller
}
func showMainView() {
let controller = MainFlowController(window: window, dependencyManager: dependencyManager)
controller.showInitialView()
activeFlow = controller
}
// MARK: Managing the Responder Chain
var activeFlow: FlowPresenting? = nil
override var next: UIResponder? {
return activeFlow as? UIResponder
}
}

In the example app we do not use storyboard for initialising the first view (“Main Interface” field in target settings is empty). Instead, we set up a window ourself and use AppFlowController for presenting the first view. In addition, we inject a manager storing a set of dependencies view controllers might require. Having approach like this, we do not need singletons and instead, flow controllers insert dependencies into view controllers. Using dependency injection keeps the overall dependency graph nice and clean.

final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private var appFlowController: AppFlowController?
private let dependencyManager = DependencyManager()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UINavigationController()
self.window = window
appFlowController = AppFlowController(window: window, dependencyManager: dependencyManager)
appFlowController?.showInitialView()
window.makeKeyAndVisible()
return true
}
override var next: UIResponder? {
return appFlowController
}
}

Creating login flow controller

LoginFlowController has similar set up as AppFlowController with exception of not inserting a subsequent flow into responder chain. In the sample app, login flow does not branch into several other login related flows. At the end of login flow, AppFlowController is used to present main content view. Due to generic function we added to UIResponder extension, login flow controller accessor consists of a single line.

final class LoginFlowController: UIResponder, LoginFlowPresenting {
let window: UIWindow
init(window: UIWindow) {
self.window = window
}
func showInitialView() {
let viewController = UIStoryboard.main.instantiateViewController(withIdentifier: "login")
(window.rootViewController as? UINavigationController)?.setViewControllers([viewController], animated: false)
}
func showSignUp() {
let viewController = SignUpViewController()
(window.rootViewController as? UINavigationController)?.pushViewController(viewController, animated: true)
}
func showAccountDetails() {
let viewController = UIStoryboard.main.instantiateViewController(withIdentifier: "accountdetails")
(window.rootViewController as? UINavigationController)?.pushViewController(viewController, animated: true)
}
}
protocol LoginFlowPresenting: FlowPresenting {
func showAccountDetails()
func showSignUp()
}
protocol LoginFlowControlling: FlowControlling {
var loginFlowController: LoginFlowPresenting { get }
}
extension LoginFlowControlling where Self: UIResponder {
var loginFlowController: LoginFlowPresenting {
return flowController()
}
}

Let’s see how view controllers in the login flow trigger navigation. LoginViewController conforms to LoginFlowControlling. This makes loginFlowController accessor available and we can use it for triggering navigation. It should be noted that loginFlowController accessor returns an object conforming to LoginFlowPresenting and does not explicitly declare the type of LoginFlowController. This means that LoginViewController does not know about LoginFlowController, instead, it just knows that the returned object implements methods listed in LoginFlowPresenting protocol. Less coupling makes it easier to test and restructure app in the future.
Another important point to note here is that it is so easy to add navigation capability to any other view controller. Flow controller does not need to be injected and in the end, we just need to make two changes in the whole view controller – protocol extension takes care of adding getter to the interface and triggering navigation is just a single line of code.

final class LoginViewController: UIViewController, LoginFlowControlling {
@IBAction func createAccount(_ sender: Any) {
loginFlowController.showSignUp()
}
}

Navigating from login to main view

AccountDetailsViewController is the last view controller in the login flow and it should trigger navigation to the main flow. As seen in previous paragraph, triggering navigation requires only two changes. This is also the case here.

final class AccountDetailsViewController: UIViewController, AppFlowControlling {
@IBAction func goToFirst(_ sender: Any) {
appFlowController.showMainView()
}
}

Summary

This time we took a look on how to use flow controllers and responder chain to easily manage navigation from one view to another. In the end we implemented a scalable architecture where adding navigation trigger points just require a few changes. Scalability comes from the fact that flows can be arranged into tree like structure and there is no requirement to have a single object managing all the flows. Moreover, we added a dependency injection capability to flow controllers which make it so much easier to test components separately and not worrying about singletons.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example Project

FlowController (GitHub) Xcode 10.1, Swift 4.2.1

References

Using Responders and the Responder Chain to Handle Events (Apple)
Generics (Swift)
Tree (data structure) (Wikipedia)

Categories
iOS Swift UIKit

View coordination with responder chain

When building iOS apps it is important to think about how to structure an app and how to connect all the pieces. A typical app has multiple interaction flows and in this post I am going to represent a way of how to decouple views and how to navigate from one view to another using responder chain.

Coordinating view presentation

Let’s take a look on the example app named “Planets” what has a structure:
Blogi - planets.001
There are a several view controllers and some of them pass information to the next one. In object oriented programming world, it is important to reduce dependencies between objects and having preferably only one responsibility for an object (see separation of concerns and single responsibility principle). Therefore, when a view would like to present another view (e.g. user presses on a button what triggers presenting a new view) it is preferred the view not to know the exact view controller what should be presented and how it should be presented. Instead, it should forward it to a separate entity what deals with loading views, injecting dependencies and presenting them. With that kind of pattern it is possible to forward required information to that entity and only it will know the type of the view controller what to present – reducing coupling between views themselves and giving a more modular architecture to an app.
In the sample app Planets the view presenter’s interface is defined by a protocol ScenePresenting.

protocol ScenePresenting {
func presentDetailedInfo(for planet: Planet)
func presentPlanetList()
func presentURL(_ url: URL)
}

SceneManager conforms to that protocol. It knows how to load views, what dependencies to inject and how to present views. Having presentation logic separated from the views, it is easier to later on change the interaction flows in the app due to reduced dependencies. Compare it with app architecture where view controllers themselves load and present view controllers.
What if app has way more views to represent? The presentation logic might get a quite long. One solution would be to have multiple protocols for different interaction ares of the app and having multiple objects like SceneManager specialised to a specific area.

Tapping into responder chain

Responder chain is a dynamic list of UIResponder objects where events are passed from responder to responder. If this is a new concept for you, then I suggest to quickly read through the overview section of responder chains here.
UIView and UIViewController are subclasses of UIResponder and are part of single or multiple responder chains (the list of responders in any chain are defined by the rules in UIKit). Responder chain always ends up in UIApplication object and if the app’s delegate is also UIResponder, the chain continues to its delegate as well.
This knowledge gives us an opportunity to expand the chain further and inserting SceneManager to the end of the chain allowing us to access it from any of the view controllers currently on screen.
It is required to just override the var next: UIResponder? { get } property in AppDelegate (AppDelegate needs be a subclass of UIResponder) and then returning an object what conforms to the protocol ScenePresenting. In the current case it is going to be an instance of SceneManager.

final class AppDelegate: UIResponder, UIApplicationDelegate {
private var sceneManager: SceneManager? = nil
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
guard let window = window else { fatalError() }
sceneManager = SceneManager(window: window)
return true
}
override var next: UIResponder? {
return sceneManager
}
}

With this change, SceneManager is now part of all the responder chains, but accessing it is a bit cumbersome. For solving this it is easy to add a convenience property to UIResponder what just goes through the chain and returns it. SceneManager is now available in all the responders and its type is erased (caller just sees an object conforming to the protocol and does not see the object’s type).
extension UIResponder {
var scenePresenter: ScenePresenting? {
var current: UIResponder? = self
repeat {
if let presenter = current as? ScenePresenting {
return presenter
}
current = current?.next
} while current != nil
return nil
}
}

Using protocol instead of SceneManager allows to hide the implementation details and breaks apart the dependency graph for users of this property. Moreover it allows to replace the SceneManager with any other object conforming to the protocol at any time in the future – separation of concerns.

Presenting new views

Presenting views becomes now much simpler to manage. In addition, the scene presenter object is accessible from any new view controller automatically. Compare it to injecting SceneManager to presented view controllers which requires all the view controllers to have a way of storing a reference to it and a way of injecting it. All this can be left out when it is part of responder chain.

@IBAction func goToList(_ sender: Any) {
scenePresenter?.presentPlanetList()
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
scenePresenter?.presentDetailedInfo(for: planets[indexPath.row])
}
@IBAction func show(_ sender: Any) {
scenePresenter?.presentURL(planet.url)
}
view raw Examples.swift hosted with ❤ by GitHub

Summary

We looked into a simple app consisting of several scenes. Having a presentation logic hidden into a separate object allowed us to decouple views from each other what reduces dependency graph for all the views. Inserting an object, responsible of presenting all the scenes, into responder chain gave us a simple, scalable and concise way of triggering view navigation.

Example

Planets (GitHub)

References

Understanding event handling, responders and the responder chain (Apple)
Separation of concerns (Wikipedia)
Single responsibility principle (Wikipedia)