Случалось ли с вами, что вы открыли Storyboard и от увиденного вас начинают переполнять положительные эмоции?
В этот момент, возможно, вы задумываетесь, что хорошо продуманная навигация между экранами (в дальнейшем Routing) в крупных проектах может стать крайне значимой задачей, решение которой поможет экономить время и нервы всем, кто будет участвовать в проекте.
Что подразумевается под словом Routing в данной публикации?
В общих чертах это можно охарактеризовать как путь из одного экрана в другой. И у каждого этот путь свой. Кто-то сразу представит Storyboard Segue, а кому-то по душе вот такой вызов:
self.navigationController?.pushViewController(UIViewController(), animated: true)
В какой момент может возникнуть нужда в переосмыслении Routing слоя?
- Вы открыли старый проект (возможно даже ваш) и никак не можете понять всей картины переходов между экранами.
- Вы работаете над крупным проектом и хотите сразу сделать доступно и прозрачно для всех участников проекта.
- Вы читаете статью про VIPER и планируете погрузиться в
чудесныймир архитектурных дискуссий. - Ваш Storyboard стал настолько большим, что добавляя каждый новый экран вы испытываете различные сложности.
- Маломощные машины просто не в силах открыть Storyboard проекта.
- Вы вообще не используете Storyboard (по этой причине?) и вызовы типа pushViewController разбросаны по всему проекту.
- Ваш уникальный случай и другие ситуации.
С чего можно начать переосмысление Routing слоя?
Важно определиться какие вещи попадут в Routing. Это могут быть функции UIViewController, UINavigationController и др. осуществляющие различные переходы: pushViewController, popViewController, popToViewController, popToRootViewController, present, dismiss, setViewControllers. Так же в Routing может попасть показ различных всплывающих окон типа alert, action sheet, toast, snackbar.
Не менее важным шагом будет принятие решения о том, как будет вызываться переход. В идеале код перехода будет достаточно краток и понятен даже тому, кто видит его в первый раз.
func someFunction() {
...
routing(with: .dismiss)
}
Подготовка UIViewController для осуществления переходов
Если попытаться реализовать пример, приведенный выше, то routing будет являться функцией, которую реализует сам UIViewController:
extension UIViewController {
func routing(with routing: Routing) {
...
}
}
Далее, надо создать элемент Routing, который будет попадать в функцию в виде параметра. В языке swift перечисления получились очень гибкими и в данной ситуации подойдут лучше всего:
enum Routing {
case dismiss
case preparedNavigation
case selectedCityTransport(CityTransport)
case selectedTrafficRoute(TrafficRoute)
...
}
Стоит заметить, что один и тот же Routing параметр может быть вызван разными UIViewController и, например, при таких вызовах должны происходить разные переходы. Соответственно, приложение должно знать из какого конкретно UIViewController был вызван переход. Можно добиться адекватного сопоставления UIViewController и перехода разными способами. Однако, в идеале, хотелось бы у UIViewController завести определенный параметр для этих целей. Например при помощи запрещенной магии 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)
}
}
}
Введя новый параметр type у UIViewController можно детализировать функцию routing, в которой будут собраны все вызовы переходов между экранами приложения и разбиты по типу вызывающего UIViewController:
extension UIViewController {
func routing(with routing: Routing) {
switch type {
case .navigation:
preparedNavigation(with: routing)
case .transport:
selectedCityTransport(with: routing)
default:
break
}
}
}
Конкретную реализацию каждого перехода можно, например, вынести в отдельный приватный 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
}
}
}
Чего удалось добиться в итоге?
- Все переходы приложения описаны в одном месте, упорядочены и не дублируются.
- Вызов перехода лаконичен и прост в использовании.
- При необходимости можно смело отказываться от использования Storyboard, если на то есть причины. Например, вы решаете использовать AsyncDisplayKit.
- Не появилось никаких новых менеджеров, сервисов или синглтонов… Вся логика остается внутри UIViewController extension.
Использовался ли данный подход авторами публикации?
В двух проектах, написанных на swift с нуля, удалось внедрить представленную реализацию Routing слоя. Один из проектов был написан с использованием RxSwift и routing вызов был обернут примерно таким образом:
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()
}
}
Размеры проектов составили 55к и 125к loc. Размеры файлов в каждом проекте, которые содержали в себе весь Routing слой, были примерно одинаковы и составили около 600 строк кода.
Автор: Dominion1