Routing layer in iOS applications
Has it happened to you that you opened the Storyboard and positive emotions begin to overwhelm you from what you saw ?
At this moment, perhaps you are thinking that well-designed navigation between screens (hereinafter Routing ) in large projects can be an extremely significant task, the solution of which will help save time and nerves for everyone who will participate in the project.
What is meant by Routing in this publication?
In general terms, this can be described as a path from one screen to another. And each has his own way. Someone will immediately introduce the Storyboard Segue , but someone will like this challenge:
At what point may the need to rethink the routing layer arise ?
Where can I start rethinking the routing layer?
It is important to decide what things get into Routing . These can be functions of UIViewController , UINavigationController , etc. that perform various transitions : pushViewController , popViewController , popToViewController , popToRootViewController , present , dismiss , setViewControllers . Also, routing may show various pop-ups like alert , action sheet , toast, snackbar .
An equally important step is deciding how the transition will be called. Ideally, the transition code will be quite brief and understandable even to someone who sees it for the first time.
Preparing the UIViewController for Transitions
If you try to implement the example above, routing will be a function that the UIViewController itself implements :
Next, you need to create a Routing element that will fall into the function as a parameter. In swift, the enumerations turned out to be very flexible and in this situation are best suited:
It is worth noting that the same Routing parameter can be called by different UIViewController and, for example, different calls should occur during such calls. Accordingly, the application must know from which specific UIViewController the transition was called. You can achieve an adequate mapping of the UIViewController and transition in various ways. However, ideally, I would like to have a UIViewController have a certain parameter for these purposes. For example, usingforbidden magic runtime :
By introducing a new type parameter to the UIViewController, you can detail the routing function , which will collect all transition calls between application screens and are broken down by the type of the calling UIViewController :
The specific implementation of each transition can, for example, be moved to a separate private UIViewController extension :
What did you achieve in the end?
Was this approach used by the authors of the publication?
In two projects written in swift from scratch, it was possible to implement the presented implementation of the Routing layer. One of the projects was written using RxSwift and the routing call was wrapped in something like this:
Project sizes were 55k and 125k loc . The file sizes in each project, which contained the entire Routing layer, were approximately the same and amounted to about 600 lines of code.
At this moment, perhaps you are thinking that well-designed navigation between screens (hereinafter Routing ) in large projects can be an extremely significant task, the solution of which will help save time and nerves for everyone who will participate in the project.
What is meant by Routing in this publication?
In general terms, this can be described as a path from one screen to another. And each has his own way. Someone will immediately introduce the Storyboard Segue , but someone will like this challenge:
self.navigationController?.pushViewController(UIViewController(), animated: true)
At what point may the need to rethink the routing layer arise ?
- You have opened an old project (maybe even yours) and can not understand the whole picture of transitions between screens.
- You are working on a large project and want to immediately make it accessible and transparent for all project participants.
- You are reading an article about VIPER and plan to immerse yourself in the
wonderfulworld of architectural discussions. - Your Storyboard has become so large that adding every new screen you experience various difficulties.
- Low-powered machines simply can not open the Storyboard project.
- You don’t use the Storyboard at all ( for this reason? ) And calls like pushViewController are scattered throughout the project.
- Your unique case and other situations.
Where can I start rethinking the routing layer?
It is important to decide what things get into Routing . These can be functions of UIViewController , UINavigationController , etc. that perform various transitions : pushViewController , popViewController , popToViewController , popToRootViewController , present , dismiss , setViewControllers . Also, routing may show various pop-ups like alert , action sheet , toast, snackbar .
An equally important step is deciding how the transition will be called. Ideally, the transition code will be quite brief and understandable even to someone who sees it for the first time.
func someFunction() {
...
routing(with: .dismiss)
}
Preparing the UIViewController for Transitions
If you try to implement the example above, routing will be a function that the UIViewController itself implements :
extension UIViewController {
func routing(with routing: Routing) {
...
}
}
Next, you need to create a Routing element that will fall into the function as a parameter. In swift, the enumerations turned out to be very flexible and in this situation are best suited:
enum Routing {
case dismiss
case preparedNavigation
case selectedCityTransport(CityTransport)
case selectedTrafficRoute(TrafficRoute)
...
}
It is worth noting that the same Routing parameter can be called by different UIViewController and, for example, different calls should occur during such calls. Accordingly, the application must know from which specific UIViewController the transition was called. You can achieve an adequate mapping of the UIViewController and transition in various ways. However, ideally, I would like to have a UIViewController have a certain parameter for these purposes. For example, using
extension UIViewController {
enum ViewType {
case undefined
case navigation
case transport
...
}
private struct Keys {
static var key = "\(#file)+\(#line)"
}
var type: ViewType {
get {
return objc_getAssociatedObject(self, &Keys.key) as? ViewType ?? .undefined
}
set {
objc_setAssociatedObject(self, &Keys.key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
By introducing a new type parameter to the UIViewController, you can detail the routing function , which will collect all transition calls between application screens and are broken down by the type of the calling UIViewController :
extension UIViewController {
func routing(with routing: Routing) {
switch type {
case .navigation:
preparedNavigation(with: routing)
case .transport:
selectedCityTransport(with: routing)
default:
break
}
}
}
The specific implementation of each transition can, for example, be moved to a separate private UIViewController extension :
private extension UIViewController {
func preparedNavigation(with routing: Routing) {
switch routing {
case .preparedNavigation:
guard let view = self as? UINavigationController else { break }
view.setViewControllers([TransportView()], animated: true)
default: break
}
}
func selectedCityTransport(with routing: Routing) {
switch routing {
case .selectedCityTransport(let object):
navigationController?.pushViewController(RoutesView(object), animated: true)
default: break
}
}
}
What did you achieve in the end?
- All application transitions are described in one place, streamlined and not duplicated.
- The transition call is concise and easy to use.
- If necessary, you can safely refuse to use the Storyboard , for any reason. For example, you decide to use AsyncDisplayKit .
- No new managers, services or singletones appeared ... All the logic remains inside the UIViewController extension .
Was this approach used by the authors of the publication?
In two projects written in swift from scratch, it was possible to implement the presented implementation of the Routing layer. One of the projects was written using RxSwift and the routing call was wrapped in something like this:
extension Reactive where Base: UIViewController {
var observerRouting: AnyObserver {
let binding = UIBindingObserver(UIElement: base) { (view: UIViewController, routing: Routing) in
view.routing(with: routing)
}
return binding.asObserver()
}
}
Project sizes were 55k and 125k loc . The file sizes in each project, which contained the entire Routing layer, were approximately the same and amounted to about 600 lines of code.