
Данная статья — моя попытка разобраться и объяснить архитектурный паттерн 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/
Добавить комментарий