Как стать автором
Обновить

Router и Data Passing архитектуры Clean Swift

Время на прочтение5 мин
Количество просмотров13K
Привет, читатель!

В предыдущей статье я рассказывал про VIP цикл архитектуры Clean Swift. Теперь мы затронем одну из самых важных тем — переход и передачу данных между сценами.



Теория


За логику навигации и передачи данных отвечает компонент Router, который является частью сцены (опционально конечно). Он иницилизируется во ViewController’e, вместе с Interactor’ом и Presenter’ом.

Router реализует два протокола — RoutingLogic и DataPassing, которые мы будем наполнять своим функционалом. RoutingLogic должен содержать в себе методы, отвечающие за переход к конкретной сцене. DataPassing содержит в себе переменную dataStore, которая ссылается на протокол DataStore. Interactor сцены реализует протокол DataStore и работает с, хранящимися в нем, переменными. Сам Router содержит ссылку на ViewController своей сцены.

Используя ссылку на ViewController, Router производит переход между сценами. Для этого можно использовать Segue или же создавать сцену, на которую нужно совершить переход, программно. Какой способ используется — не важно, нам главное иметь ссылку на экземпляр класса ViewController’a, на который мы совершаем переход.

Используя ссылку на DataStore мы будем совершать передачу данных из Interactor’а одной сцены в Interactor сцены, на которую переходим. И, как было сказано ранее, именно Router должен знать, как это делать.

Практика


Для примера, мы будем передавать текст из TextField в Label другой сцены. Рассмотрим два способа перехода между сценами — по Segue и программно.

Класс Router содержит 3 смысловые группы методов:

  1. Методы из реализации RoutingLogic (routeTo)
  2. Методы, отвечающие за навигацию (navigateTo, переход без Segue)
  3. Методы, для передачи данных (passDataTo, если есть данные для передачи)



Если мы совершаем переход по Segue, к примеру, при нажатии на кнопку, то во ViewController’e мы должны переопределить метод prepare(for:sender:). Данное расширение позволит автоматически вызывать методы из Router’a по названию Segue’я.

Переопределение prepare(for:sender:) не является обязательным при работе с Segue. Вы можете исключить его из кода и вызывать performSegue(withIdentifier:sender:) в методе Router’a. Prepare нужен только в случае, если вам необходимо использовать Segue вместе с передачей данных.

final class HomeViewController: UIViewController {
  // ...
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    // Если у Segue есть идентификатор
    if let scene = segue.identifier {

      // Создаем селектор, для вызова метода с этим именем и параметрами в Router
      // router?.routeToNAME(segue:)
      let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")

      // Если есть метод с таким селектором,
      // вызываем его и передаем в него Segue
      if let router = router, router.responds(to: selector) {
        router.perform(selector, with: segue)
      }
    }
  }
  
  // ...
}

Теперь мы наконец то подошли к самому интересному — к коду Router’a. Пример содержит комментарии, поэтому мы рассмотрим только ключевые моменты.

В данном Router’e мы работаем с двумя сценами — Home и Detail. Переход со сцены Home обрабатывается так же двумя способами — по Segue и программно. Данные передаются со сцены Home на сцену Detail.

Все методы в протоколе RoutingLogic должны именоваться по принципу routeToNAME, где NAME является названием Segue (Identifier), которое мы задаем при работе со Storyboard’ом. Это нужно не только для удобства и красоты использования, но и для нашего селектора методов в prepare(for:sender:) ViewController’a, который мы переопределяли ранее.

Так же в классе HomeRouter есть методы начинающиеся на navigateTo и passDataTo. Первые отвечают за логику перехода, а вторые за передачу данных. Методы navigateTo создаются только в том случае, если переход осуществляется программно.

В примере мы имеем метод routeToDetail(segue:). Параметр segue опциональный, т.к. в методе содержится реализация, которая позволяет вызывать его без использования Segue. В обоих случаях перехода мы получаем неопциональные значения нашего HomeViewController’a и HomeDataStore’a сцены Home, а так же ссылки на ViewController и DataStore сцены Detail. Здесь стоит обратить внимание на то, что detailDS является переменной и передается в метод passDataToDetail с помощью сквозного параметра (inout). Это важно, т.к. без inout нам придется помечать все протоколы DataStore как “возможно реализовать только классами” (protocol DetailDataStore: class), а из этого вытекает множество сложностей, в том числе захват сильных ссылок.

import UIKit

/// Помечен как @objc для создания селектора
/// в методе prepare во View Controller'e
@objc protocol HomeRoutingLogic {
  /// Переход на Detail View Controller
  func routeToDetail(segue: UIStoryboardSegue?)
}

protocol HomeDataPassing {
  var dataStore: HomeDataStore? { get }
}

final class HomeRouter: NSObject, HomeRoutingLogic, HomeDataPassing {

  // MARK: - Private

  // MARK: - Public

  weak var viewController: HomeViewController?
  var dataStore: HomeDataStore?

  // MARK: - HomeRoutingLogic

  func routeToDetail(segue: UIStoryboardSegue?) {
    if let segue = segue {
      // Данный участок кода срабатывает
      // только при переходе по Segue

      // Получаем ссылку на Detail View Controller
      // И на его Data Store в Router'e
      guard
        let homeDS = dataStore,
        let detailVC = segue.destination as? DetailViewController,
        var detailDS = detailVC.router?.dataStore
        else { fatalError("Fail route to detail") }

      // Далее, полученные данные, мы пробрасываем в метод,
      // который "знает" как передавать данные
      passDataToDetail(source: homeDS, destination: &detailDS)
    } else {
      // Данный участок кода срабатывает,
      // когда вызывается переход без Segue

      // Иницилизируем Detail View Controller из Storyboard'a
      // И получаем ссылку на его Data Store в Router'e
      guard
        let viewController = viewController,
        let homeDS = dataStore,
        let storyboard = viewController.storyboard,
        let detailVC = storyboard.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
        var detailDS = detailVC.router?.dataStore
        else { fatalError("Fail route to detail") }

      passDataToDetail(source: homeDS, destination: &detailDS)

      // Метод, который отвечает за переход и "знает" как это делать
      navigateToDetail(source: viewController, destination: detailVC)
    }
  }

  // MARK: - Navigation

  private func navigateToDetail(source: HomeViewController, destination: DetailViewController) {
    source.navigationController?.pushViewController(destination, animated: true)
  }

  // MARK: - Passing data

  /// Параметр destination помечен как inout,
  /// что бы избежать мутации Data Store и захвата ссылки при передаче данных
  private func passDataToDetail(source: HomeDataStore, destination: inout DetailDataStore) {

    // Передаем текст сообщения из HomeDataStore в DetailDataStore
    destination.message = source.message
  }
}

Заключение


На этом все. Спасибо, что дочитали до конца! Ниже я оставлю ссылку на проект, если захотите опробовать статью в деле.

Серия статей


  1. Общее представление об архитектуре Clean Swift
  2. Router и Data Passing в архитектуре Clean Swift (вы здесь)
  3. Workers архитектуры Clean Swift
  4. Unit тестирование в архитектуре Clean Swift
  5. Пример простого интернет-магазина на архитектуре Clean Swift

Ссылка на проект
Помощь в написании статьи: Bastien
Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии2

Публикации

Истории

Работа

iOS разработчик
20 вакансий
Swift разработчик
27 вакансий

Ближайшие события