Routing шар в iOS-додатках

чи Траплялося з вами, що ви відкрили 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() {
...
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 рядків коду.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.