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:
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) | |
} |
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
References
Understanding event handling, responders and the responder chain (Apple)
Separation of concerns (Wikipedia)
Single responsibility principle (Wikipedia)