📜 ⬆️ ⬇️

Routing layer in iOS applications

Has it happened to you that you opened a Storyboard and from what you see you begin to overwhelm positive emotions?

At this point, you may think that well-designed navigation between screens (hereinafter Routing ) in large projects can be an extremely important task, the solution of which will help save time and nerves to everyone who participates in the project.

What is meant by the word Routing in this publication?

In general terms, this can be described as a path from one screen to another. And everyone has his own way. Someone will immediately submit a Storyboard Segue , and someone like this challenge:
')
self.navigationController?.pushViewController(UIViewController(), animated: true) 

At what point may there be a need to rethink the routing layer?


How can I start rethinking the Routing layer?

It is important to decide what things fall into Routing . These can be UIViewController , UINavigationController and other functions that perform various transitions : pushViewController , popViewController , popToViewController , popToRootViewController , present , dismiss , setViewControllers . Also in Routing can display various pop-up windows like alert , action sheet , toast , snackbar .

An equally important step will be to decide how the transition will be triggered. Ideally, the transition code will be fairly brief and clear even to those who see it for the first time.

 func someFunction() { ... routing(with: .dismiss) } 

Preparing UIViewController for transitions

If you try to implement the example above, then routing will be a function that UIViewController implements :

 extension UIViewController { func routing(with routing: Routing) { ... } } 

Next, you need to create an element Routing , which will fall into the function as a parameter. In the swift language, the listings turned out to be very flexible and will work best in this situation:

 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, with such calls different transitions should occur. Accordingly, the application must know from which specific UIViewController the transition was triggered. You can achieve adequate UIViewController mapping and transitions in a variety of ways. However, ideally, I would like to have a certain parameter for UIViewController for this purpose. For example, using the forbidden magic runtime :

 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 entering a new type parameter for UIViewController, you can detail the routing function, in which all transition calls between application screens will be collected and 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 } } } 

A specific implementation of each transition can, for example, be put into 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?


Was this approach used by the authors of the publication?

In two projects written in swift from scratch, we managed to implement the presented implementation of the Routing layer. One of the projects was written using RxSwift and the routing call was wrapped like this:

 extension Reactive where Base: UIViewController { var observerRouting: AnyObserver<Routing> { 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 sizes of files in each project, which contained the entire Routing layer, were approximately the same and amounted to about 600 lines of code.

Source: https://habr.com/ru/post/321842/


All Articles