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?- You opened an old project (maybe even yours) and cannot understand the whole picture of transitions between screens.
- You are working on a large project and want to immediately make it available and transparent for all project participants.
- You are reading an article about VIPER and planning to plunge into the
wonderful world of architectural discussions. - Your Storyboard has become so big that you’ll experience various difficulties adding each new screen.
- Low-powered machines are simply unable to open the Storyboard project.
- You don’t use Storyboard at all ( for this reason? ) And pushViewController calls are scattered throughout the project.
- Your unique case and other situations.
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 transitionsIf 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?- All application transitions are described in one place, ordered and not duplicated.
- The transition call is concise and easy to use.
- If necessary, you can safely refuse to use Storyboard , if there are any reasons. For example, you decide to use AsyncDisplayKit .
- No new managers, services or singltons 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, 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.