MVP with Router

от автора

Данная статья — моя попытка разобраться и объяснить архитектурный паттерн MVP with Router.

Про сам паттерн MVP в на просторах интернета можно найти довольно много информации, например по следующей ссылкам: https://habr.com/ru/companies/badoo/articles/281162/ https://habr.com/ru/companies/croc/articles/549590/

А вот про разновидность данного паттерна, которая решает проблему сборки, возникающую при использовании MVP информации не так уж и много. Давайте попробуем разобраться что такое Router применительно к паттерну MVP, зачем он нужен и как его использовать.

Начнем с основ

Рассмотрение паттерна MVC with Router предлагаю рассмотреть на практике на примере создания простого приложения из двух экранов. Распределим все сущности нашего приложения согласно паттерну MVP,  осознаем возникающую проблему сборки и применим сущности Assembley и Router для решения данной проблемы.

Model

Для начала создадим простую модель. Она будет содержать свойство someData типа String? и методы получения и установки данных. Создадим протокол, а затем и саму модель, соответствующую указанным требованиям.

protocol FirstModelProtocol {     func getData() -> String     func setData(data: String) }  final class FirstModel {     private var someData: String? }  extension FirstModel: FirstModelProtocol {     func getData() -> String {         guard let someData = self.someData else { return "" }         return someData     }          func setData(data: String) {         self.someData = data     } }

View

В терминах MVP под View мы понимаем как отдельные View так и ViewController. Для начала создадим протокол, которому должна соответствовать сущность View.

protocol FirstViewProtocol: UIView {     var onTouchedHandler: (() -> Void)? { get set }     var goToSecondHandler: (() -> Void)? { get set }      func update(data: String)     func getTextFieldData() -> String }

Мы определили что у наш View должен содержать два свойства обработчика событий нажатия (одно мы будем использовать для отображения данных модели во View, а второе для перехода на второй экран), а так же методы для получения данных из текстового поля View и обновления нашего View.

Создадим нашу View. Добавим на нее две кнопки, текстовое поле и лейбл. В расширении View реализуем требования протокола.

final class FirstView: UIView {     var onTouchedHandler: (() -> Void)?     var goToSecondHandler: (() -> Void)?      private lazy var button: UIButton = {         let obj = UIButton(frame: CGRect(x: 100, y: 300, width: 200, height: 50))         obj.backgroundColor = .white         obj.setTitleColor(.darkGray, for: .normal)         obj.layer.cornerRadius = 10         obj.layer.borderWidth = 5         obj.layer.borderColor = UIColor.orange.cgColor         obj.setTitle("Push me", for: .normal)         obj.addTarget(self, action: #selector(self.touchedDown), for: .touchDown)         return obj     }()          private lazy var button2: UIButton = {         let obj = UIButton(frame: CGRect(x: 100, y: 400, width: 200, height: 50))         obj.backgroundColor = .white         obj.setTitleColor(.darkGray, for: .normal)         obj.layer.cornerRadius = 10         obj.layer.borderWidth = 5         obj.layer.borderColor = UIColor.orange.cgColor         obj.setTitle("Go to second", for: .normal)         obj.addTarget(self, action: #selector(self.touchedDownGoToSecond), for: .touchDown)         return obj     }()      private lazy var textField: UITextField = {         let obj = UITextField(frame: CGRect(x: 100, y: 100, width: 200, height: 50))         obj.backgroundColor = .systemGray6         obj.placeholder = "Type something cool"         return obj     }()      private lazy var label: UILabel = {         let obj = UILabel(frame: CGRect(x: 100, y: 200, width: 200, height: 50))         obj.backgroundColor = .systemGray6         return obj     }()      override init(frame: CGRect) {         super.init(frame: frame)         self.configView()         self.backgroundColor = .white     }      required init?(coder: NSCoder) {         fatalError("init(coder:) has not been implemented")     } }  // MARK: Private extension  private extension FirstView {          private func configView() {         self.addSubview(self.button)         self.addSubview(self.button2)         self.addSubview(self.label)         self.addSubview(self.textField)     }      @objc private func touchedDown() {         self.onTouchedHandler?()     }          @objc private func touchedDownGoToSecond() {         self.goToSecondHandler?()     } }  // MARK: FirstViewProtocol  extension FirstView: FirstViewProtocol {          func getTextFieldData() -> String {         guard let text = self.textField.text else { return  "" }         return text     }      func update(data: String) {         self.label.text = data     } }

Router

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

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

В нашем простом приложении указанные выше плюсы не очень заметны, но они приобретают важное значение в крупных проектах.

final class Router {     private var controller: UIViewController?     private var targertController: UIViewController?          func setRootController(controller: UIViewController) {         self.controller = controller     }          func setTargerController(controller: UIViewController) {         self.targertController = controller     }          func next() {         guard let targertController = self.targertController else {             return         }                  self.controller?.navigationController?.pushViewController(targertController, animated: true)     } }

Наш роутер довольно простой. Он содержит два поля типа UIViewController? Два метода установки текущего ViewController и UIViewController на который мы хотим переходить по нажатию кнопки, а так же непосредственно метод перехода.

Presenter

Теперь у нас есть все для создания презентера. Давайте создадим его.

protocol FirstPresenterProtocol {     func loadView(controller: FirstViewController, view: FirstViewProtocol) }  final class FirstPresenter {     private let model: FirstModelProtocol     private let router: Router     private weak var controller: FirstViewController?     private weak var view: FirstViewProtocol?      struct Dependencies {         let model: FirstModelProtocol         let router: Router     }      init(dependencies: Dependencies) {         self.model = dependencies.model         self.router = dependencies.router     } }   private extension FirstPresenter {     private func onTouched() {         guard let view = view else { return }          let modelData = view.getTextFieldData()         self.model.setData(data: modelData)          let viewModel = "The data: " + self.model.getData()         self.view?.update(data: viewModel)     }          private func onTouchedGoToSecondVC() {         self.router.next()     }      private func setHandlers() {         self.view?.onTouchedHandler = { [weak self] in             self?.onTouched()         }         self.view?.goToSecondHandler = { [weak self] in             self?.onTouchedGoToSecondVC()         }     } }  extension FirstPresenter: FirstPresenterProtocol {     func loadView(controller: FirstViewController, view: FirstViewProtocol) {         self.controller = controller         self.view = view          self.setHandlers()     } } 

В протоколе мы определяем метод loadView, который необходим нам для передачи в презентер ViewController и View.

Мы определяем в презентере свойства модель, контроллер, вью и роутер. При этом модель и роутер мы будем передавать при инициализации презента через структуру Dependencies.

Мы определяем два метода, которые будут вызываться в ответ на нажатие кнопок View и в методе setHandlers связываем их с соответствующими методами View.

В методе loadView, который мы реализуем в расширении как требование протокола мы устанавливаем контроллер и вью.

Assembley

Компоненты нашего MVP готовы. В данный момент мы и сталкиваемся с проблемой сборки. Сейчас нам необходимо собрать все компоненты воедино. Сделать что то наподобие:

let model = Model() let view = View() let presenter = Presenter(view: view, model: model) view.presenter = presenter

Классический MVP не определяет кто отвечает за сборку и где она должна происходить.

Тут нам на помощь приходит отдельная сущность Assembley в которую мы и вынесем нашу сборку.

final class FirstScreenAssembley {     static func build() -> UIViewController {         let model = FirstModel()         let router = Router()          let presenter = FirstPresenter(             dependencies: .init(model: model, router: router)         )                  let controller = FirstViewController(             dependencies: .init(presenter: presenter)         )                  let targetController = SecondScreenAssembley.build()          router.setRootController(controller: controller)         router.setTargerController(controller: targetController)          return controller     } }

Внутри Assembley в методе build мы создаем модель, презентер, контроллер, роутер и соединяем все воедино возвращая контроллер инициализированный со всеми зависимостями.

В классе SceneDelegate мы вызываем метод build у FirstScreenAssembley для того чтобы получить ViewController и сделать рутовым у NavigationController.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {      var window: UIWindow?     var navigationVc: UINavigationController?     var vc: UIViewController?       func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {                  guard let windowScene = (scene as? UIWindowScene) else { return }         self.window = UIWindow(windowScene: windowScene)                  self.vc = FirstScreenAssembley.build()         guard let vc = self.vc else { return }         self.navigationVc = UINavigationController(rootViewController: vc)                  self.window?.rootViewController = self.navigationVc         self.window?.makeKeyAndVisible()     } }

В целом, наше приложение с Router и Assembley готово.

Для того чтобы продемонстрировать работу роутера создадим простой второй экран и сборщик к нему.

class SecondViewController: UIViewController {      override func viewDidLoad() {         super.viewDidLoad()                  self.view.backgroundColor = .systemMint     } }
final class SecondScreenAssembley {     static func build() -> UIViewController {         SecondViewController()     } }

Запустим наше приложение.

При нажатии на кнопку Push me текст лейбла у нас отображает данные, введенные в текстовое поле:

А при нажатии на кнопку Go to second мы переходим на второй экран.

Надеюсь данная статья окажется полезной и поможет понять как мы можем применять сущности Router и Assembley в наших проектах с архитектурой MVP.

Полный код проекта можно найти по ссылке https://github.com/svgnovosibirsk/MVPWithRouter


ссылка на оригинал статьи https://habr.com/ru/articles/749700/