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 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 строк кода.
Поделиться публикацией

Похожие публикации

Комментарии 25

    –3

    Реализовывать роутинг в UIView, это как минимум не логично, появится у вас новый переход и вот вы уже изменяете UIView!? Работать должно это приблизительно так: у UIViewController есть метод convenienceInit, который на вход принимает UiViewControllerParams. Затем, когда вам нужно отрисовать экран вы вызываете метод showAnimated куда передаете тип анимации для этого контроллера.

      0
      Как минимум не логично писать комментарий про роутинг в UIView, если в публикации говорится про роутинг в UIViewController extension.
        –1

        Я не iOS разработчик. Так что с терминологией могу немного путаться. Но в остальном, вопрос остается прежним, почему роутинг перекочевал во ViewController? ViewController не должен содержать такую логику. Он должен содержать логику для получения данных. А саму анимацию контроллера так же лучше вынести в отдельные сущности.

          0
          Если вы откроете Storyboard, то Segue это в чистом виде роутинг между двумя UIViewController. И ручной вызов этого происходит именно в UIViewController и Segue с ним тесно связано. Так сделали в Apple. В этой публикации по сути говорится о том же, только об удобной группировке всех вызовов переходов.
            0
            В публикации ни слова о том, что кастомная анимация должна быть в чистом виде реализована в роутинг слое. Приводятся примеры лишь анимации «из коробки». Роутинг слой здесь представлен как набор стратегий переходов собранных в одном месте. Как вы распорядитесь переходами это уже совсем другой вопрос
        +3

        А мне идея сама понравилась — она достаточно красивая для внешнего использования.


        Но проблема идеи — она не универсальна и плохо сочетаема с ограничением доступа — по факту теперь можно вызвать переход с любого экрана на любой.


        Если идею расширить чтобы можно было: передавать параметры и ограничить доступ по переходам — то сталобы хорошо.


        На самом деле то что описано в статье, это почти эквивалент того что я называю Навигатором.
        Навигатор: Некий класс содержащий набор методов с переходами между экранами. Он наследуеться от десятка протоколов, в которых кокраз таки и содержаться эти методы.
        После чего этот класс, но по протоколам, мы подсовываем во все ViewController-ы.


        Такой подход не решает одной проблемы — не видно карты всех переходов целиком, что на мой взгляд кокраз таки и отличает Роутер от Навигатора — по первому видны все "маршруты" которые есть.


        Сторибоард на картинке, показательный пример — в нем отображены только переходы, и именно это обязанность роутинга — давать подробную информацию обо всех переходах. Если можно былобы легко передавать параметры с помощью сторибоарда и его представление былобы поудачнее, думаю не у кого не возникло желания делать свой роутер.

          +3
          Отличные замечания! Попробую их прокоментировать:

          1. Карта переходов. Если перейти в публикации к словам: "Введя новый параметр type у UIViewController можно детализировать функцию routing, в которой будут собраны все вызовы переходов между экранами приложения и разбиты по типу вызывающего UIViewController", то код приведенный под ними является той самой картой всех переходов в приложении. Ну и вроде как это достаточно наглядно.

          2. Ограничение доступа. Вызвать переход с любого экрана на любой можно (но только вызвать). Если в карте переходов не будет прописано для конкретного типа контролера конкретного действия, то просто ничего не произойдет. Поэтому просто так перейти куда угодно не получится. В публикации, если вы вызовете для контролера с type = .navigation переход с параметром selectedCityTransport, то ничего не произойдет, т.к. этого нет в карте.

          3. Передача параметров. Проблемы с передачей параметров и так нет. Внутри enum Routing можно передавать что угодно
            +1

            Да пожалуй, первый пункт на самом деле проглядел. Ну и по второму и третьему пункту соглашусь.


            Так что да, идея на самом деле отличная. Возможно попробую у себя как-нибудь в проекте, но сходу больше проблем не вижу.
            Есть правда одна — сочетание со storyboard, так как у нас в компании принято разрабатывать через него.


            Покрайней менее, это в разы проще моего сложного VCRouter который решает туже задачу.

          0
          Как удалить комментарий?
            +1

            Добро пожаловать в клуб. Просто пиши точку или какую-нибудь мантру, например "Я буду обновлять комментарии".

            0

            Еще раз перечитал статью.
            А почему не использовать в качестве type реальный тип класса?


            тогда switch будет похож на:


            switch type(of: self) {
              case ViewController1.self:
               ...
              case ViewController2.self:
               ...
            }

            Правда именно так не разрешено сделать, но всегда есть String(describing:) который можно красиво обернуть через расширение в какой-нибудь статический метод/свойство.


            В этом случае не нужно добавлять enum и инициализировать его во всех ViewController + интеллисенс будет позволять легко переходить на каждый из них.

              +1
              Вдруг получится так, что один и тот же класс созданный при разных условиях должен будет себя вести по разному при одинаковом типе перехода. Тогда придется вводить дополнительный флаг, который будет отвечать за тип инициализации. А если делать через type, то можно сделать что-нибудь типа . transport_in_navigation, . transport_presented_modally и легко развести логику.
              Но на самом деле, как и было сказано в публикации: Можно добиться адекватного сопоставления UIViewController и перехода разными способами. Предложенный вами один из таких. И, если для ваших задач его будет достаточно, то можно пользоваться и им. Тут любой подход будет оправдан если вписывается в рамки поставленной задачи
              0
              Некоторые решения и комментарии в статье неактуальны для последних SDK.

              Ваш Storyboard стал настолько большим, что добавляя каждый новый экран вы испытываете различные сложности.
              Маломощные машины просто не в силах открыть Storyboard проекта.


              Именно для этого Apple еще в iOS 9 ввели Storyboard Reference. Который позволит создавать segue между ViewController'ами которые находятся в разных сторибоардах. Т.е. теперь для того чтобы манеджить весь уровень навигации не обязательно писать код, и не обязательно хранить все VC в одном сторибоарде.

              В последних проектах только их и используем. Segue позволяет отлично инкапсулировать зависимости отдельных VC, а метод prepareForSegue для каждого VC позволяет без труда быстро понять кто и что вызывает.
                0
                Если вас попросят написать приложение с использованием AsyncDisplayKit, помогут ли вам Storyboard Reference?
                  0
                  Нет, так же как и если писать его используя любой другой framework, который не поддерживает IB. Но по моему это недостаток самого AsyncDisplayKit.
                    0
                    Получается, что приведенные в публикации решения могут использоваться в таких ситуациях, в которых Storyboard не подойдет по какой-либо причине. Поэтому назызвать неактуальным можно только коментарий.
                      –2
                      Нет, получается вы игнорируете ссылку на конкретные пункты статьи, которые я в изначальном комментарии посчитал неактуальными.

                      Ваш Storyboard стал настолько большим, что добавляя каждый новый экран вы испытываете различные сложности.
                      Маломощные машины просто не в силах открыть Storyboard проекта.


                      Т.е. эти пункты подразумевают наличие и использование Storyboard в проекте изначально. И соответственно подразумевают отсутствие AsyncDisplayKit. И именно к этим пунктам я привел комментарий, о возможном использовании нативных Storyboard Reference, вместо написания лишнего кода.
                        0
                        Это может точно так же подразумевать наличие Storyboard, который был создан до появления Storyboard Reference. Или нет?
                          0
                          Может подразумевать, и вместо применения подхода описанного в данной статье, логичным выглядит проведение рефакторинга сторибоарда на разбиение, который по личному опыту занимает очень мало времени.

                            +1
                            Публикация решают одну конкретную задачу, с которой, как вы уже сами сказали, вы сможете справиться не всегда используя Storyboard. Думаю вам следует смириться с тем, что не все используют Storyboard. И, наверное, вам следует радоваться тому факту, что люди пишут интерфейс кодом, тратят на это больше времени и пишут подобные статьи завлекая других в этот омут программирования кодом. Вы используете более современный подход и будете более востребованы как специалист и сможете зарабатывать больше $. Ну я бы точно этому радовался
                              0
                              Ого, а у вас неплохо получается переходить на личности в споре, аргументами который вы поддержать не в силах.

                              Публикация решают одну конкретную задачу

                              Публикация приводит так же и комментарии, которые я справедливо подвергнул критике.

                              Думаю вам следует смириться с тем, что не все используют Storyboard

                              Думаю, что вы понятия не имеете о том с чем я смирился, а с чем нет, но переходить на личности в любом споре — это низко.

                              И, наверное, вам следует радоваться тому факту, что люди пишут интерфейс кодом

                              Вы не поверите, но я и сам пишу интерфейс кодом, когда этого требует проект, а вот чему радоваться мне или нет — решать далеко не вам.

                              Вы используете более современный подход и будете более востребованы как специалист и сможете зарабатывать больше $

                              Вы мало зарабатываете и поэтому у вас пассивная агрессия на этой почве? Если нет, то к чему этот комментарий? Он абсолютно не вносит конструктива в дискуссию.

                              Ну я бы точно этому радовался

                              Чему радоваться лично вам, я указывать естественно не буду, практически в каждой конституции — это закреплено как основная свобода человека.

                              После этого комментария сложилось впечатление, что вы один из людей который считает свой подход единственно верным в любой ситуации, но к справедливости всего сказанного выше, Storyboard и
                              программирования кодом
                              применяются в разных ситуациях и по разным причинам.
                              В данной ветке дискуссии вы прыгаете от аргумента к аргументу, от позиции к позиции при этом не пытаясь вести диалог, а просто пытаясь доказать свою правоту, например в предыдущем комментарии вы писали:

                              Это может точно так же подразумевать наличие Storyboard, который был создан до появления Storyboard Reference.


                              Т.е. вы все таки признаете наличие в проектах Storyboard как таковых, зачем же вы дальше пишите:

                              Думаю вам следует смириться с тем, что не все используют Storyboard. И, наверное, вам следует радоваться тому факту, что люди пишут интерфейс кодом, тратят на это больше времени и пишут подобные статьи завлекая других в этот омут программирования кодом.


                              В общем я не вижу смысла далее продолжать с вами дискуссию, вы изначально не поняли смысла моего комментария и вступили в демагогию, поводом для которой насколько я могу судить было упоминание Storyboard и IB.
                                0
                                Прочитайте свой изначальный пост. Там явно указано про комментарии и решения. Я, между прочим, с вами сразу согласился на тему того, что комментарии неактуальные и поставил под сомнение лишь слово решения и привел довод, где описанное в публикации решение будет работать. И после того, как я вами согласился на тему неактуальности комментариев автора, вы опять упрекнули меня за игнорирование комментариев.
                                1. Может вы что-то другое имели ввиду под словом решения?
                                2. Может вы думали, что я ваши комментарии назвал неактуальными?
                    0
                    Мировозрение Facebook по поводу IB выглядит вот так:

                    Storyboards are a useful prototyping tool, but they're bad for performance — especially when paired with Auto Layout — and they add substantial long-term debugging and development costs. AsyncDisplayKit intentionally doesn't support them. (You could make ASTableView work with Interface Builder by implementing -initWithCoder:, but you'd still have to create and configure its cell nodes in code.)


                    И для самого Facebook, а так же других проектов, которые рассчитаны жить и поддерживаться более 2-3х лет это абсолютно справедливо, но если же вы пишите проект на релиз + 1-2 версии баг фикса, то я думаю отказ от Storyboard в пользу AsyncDisplayKit не слишком обоснованное решение.
                  0
                  Не могли бы вы объяснить, как в этом случае происходит кастинг Keys.key во ViewType? Или это просто заглушка для Keys.key, а type задается уже в целевом контроллере?

                  Скрытый текст
                  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)
                          }
                      }
                  }
                  

                    +1
                    Все верно, type задается уже в конкретном контроллере. А Keys необходим для runtime.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое