Данная статья является переводом статьи от Bobby Pehtrus
Часть руководства по MVVM-C для IOS
В итоге, мне удалось выпустить еще одну статью на медиуме. Я очень признателен за благодарности и я удивлен что смог получить на этом деньги😁. Это был тяжелый месяц, поэтому задержал некоторые из моих статей. Надеюсь Вам понравится!
. . .
Так, ранее мы кратко говорили о введение в шаблон MVVM + C для IOS разработчиков, эту статью вы можете найти здесь.
Если вы посмотрите на структуру координатора которую я предоставлял, то увидим что это был дочерние свойства внутри.
protocol Coordinator { var parentCoordinator: Coordinator? { get set } var children: [Coordinator] { get set } var navigationController : UINavigationController { get set } func start() }
Для чего это?
Ну это возможно для соединения Логина, Регистрации, Главного экрана, Профиля или другого флоу приложения внутри одного координатора. Но вскоре он будет беспорядочный. Если вы хотите более аккуратный координатор, мы должны разделить некоторый функционал между координаторами. Один ответственный за Логин, Один ответственный за покупку продуктов, или ответственный за обработку главной навигации. Имея множество дочерних координаторов с более конкретным функционалом делает приложение более обслуживаемое и легко улучшаемое.
Создание дочернего Координатора
Давайте предположим что вы хотите разделить аутентификацию на Логин и Регистрацию функциональностей в разные отдельные координаторы. Давайте создадим AuthCoordinator.
class AuthCoordinator : Coordinator { var parentCoordinator: Coordinator? var children: [Coordinator] = [] var navigationController: UINavigationController init(navigationController : UINavigationController) { self.navigationController = navigationController } func start() {// Setup initial VC here!} }
в AppCoordinator, не забудьте создать и добавить AuthCoordinator в дочерние AppCoordinator и назначить родителем.
func goToAuth(){ // Create the coordinator let authCoordinator = AuthCoordinator.init(navigationController: navigationController) // Set the parent authCoordinator.parentCoordinator = self // Add to AppCoordinator's Child children.append(authCoordinator) // Do not forget to kickstart it! authCoordinator.start() }
Сейчас, ваш ChildCoordinator родился!
Вы можете видеть полное расширение возможностей AuthCoordinator
import Foundation import UIKit class AuthCoordinator : Coordinator { weak var parentCoordinator: Coordinator? var children: [Coordinator] = [] var navigationController: UINavigationController init(navigationController : UINavigationController) { self.navigationController = navigationController } func start() { print("AuthCoordinator Start") goToLoginPage() } let storyboard = UIStoryboard.init(name: "Main", bundle: .main) deinit { print("AuthCoordinator Deinit") } } extension AuthCoordinator : LoginNavigation, RegisterNavigation { func goToHome() { // Get the app coordinator let appC = parentCoordinator as! AppCoordinator // And go to home! appC.goToHome() // Remember to clean up parentCoordinator?.childDidFinish(self) } func goToLoginPage(){ // Instantiate LoginViewController let loginViewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController // Instantiate LoginViewModel and set the coordinator let loginViewModel = LoginViewModel.init(nav: self) // Set the ViewModel to ViewController loginViewController.viewModel = loginViewModel // Push it. navigationController.pushViewController(loginViewController, animated: true) } func goToRegisterPage(){ let registerViewController = storyboard.instantiateViewController(withIdentifier: "RegisterViewController") as! RegisterViewController let registerViewModel = RegisterViewModel.init(nav: self) registerViewController.viewModel = registerViewModel navigationController.pushViewController(registerViewController, animated: true) } }
Когда будет старт у AuthCoordinator’а, покажется экран Логина. Координатор так же управляет всеми зависимостями представления Логина и Регистрации.
Таббары и координатор
Концепт не такой сложный, Обычно таббары имеют много активных дочерних координаторов в одно и тоже время. Отношение между родителем могут быть разные.
Некоторые предпочитают левую сторону, другие могут выбрать правую сторону. Это зависит от потребностей проекта и вашего стиля написания кода. Хорошо, я часто использую левую сторону потому что я хочу легко переключать модули как от Home к Auth или от Profile к Auth. Итак, я использую только HomeTabbarCoordinator для инициализации других координаторов c вкладками. Смотри код ниже!
class HomeTabBarCoordinator : Coordinator { weak var parentCoordinator: Coordinator? var children: [Coordinator] = [] var navigationController: UINavigationController func start() { print("HomeTabbar Coordinator Init") initializeHomeTabBar() } deinit { print("HomeTabbar Coordinator Deinit") } let storyboard = UIStoryboard.init(name: "Main", bundle: .main) func initializeHomeTabBar(){ let vc = UITabBarController.init() // Instantiate all related coordinators and views also dependecies like VM or API Services. // We want each section has their own navigation controller, rather than app wide view controller. let homeNavigationController = UINavigationController() let homeCoordinator = HomeCoordinator.init(navigationController: homeNavigationController) // we want to home coordinator connected to the App Coordinator, because the HomeTabbar coordinator only serves as a container. homeCoordinator.parentCoordinator = parentCoordinator // Create the tabbar item for tabbar. let homeItem = UITabBarItem() homeItem.title = "HOME" homeItem.image = UIImage.init(systemName: "house.fill") homeNavigationController.tabBarItem = homeItem // Setup for profile tab let profileNavigationController = UINavigationController() let profileCoordinator = ProfileCoordinator.init(navigationController: profileNavigationController) profileCoordinator.parentCoordinator = parentCoordinator let profileItem = UITabBarItem() profileItem.title = "PROFILE" profileItem.image = UIImage.init(systemName: "person.fill") profileNavigationController.tabBarItem = profileItem // Setup for history tab let historyNavigationController = UINavigationController() let historyCoordinator = HistoryCoordinator.init(navigationController: historyNavigationController) historyCoordinator.parentCoordinator = parentCoordinator let historyItem = UITabBarItem() historyItem.title = "HISTORY" historyItem.image = UIImage.init(systemName: "clock.fill") historyNavigationController.tabBarItem = historyItem vc.viewControllers = [homeNavigationController, historyNavigationController, profileNavigationController] navigationController.pushViewController(vc, animated: true) navigationController.setNavigationBarHidden(true, animated: true) // Add the coordinator into parent's child parentCoordinator?.children.append(homeCoordinator) parentCoordinator?.children.append(profileCoordinator) parentCoordinator?.children.append(historyCoordinator) homeCoordinator.start() profileCoordinator.start() historyCoordinator.start() } init(navigationController : UINavigationController) { self.navigationController = navigationController } }
Вы можете видеть что я не назначаю HomeTabbarCoordinator, поскольку дочерний является родительским. Почему? Когда дочерний хочет получить доступ в AppCoordinator, он может это сделать без любого другого кода внутри HomeTabbarCoordinator.
Хорошо, вы можете использовать правую сторону, примеряя основу в вашем проекте.
Для использования координатора необходимо всего лишь вызвать функцию ниже в AppCorrdinator.
func goToHome(){ // Initiate HomeTabBar Coordinator let coordinator = HomeTabBarCoordinator.init(navigationController: navigationController) children.removeAll() coordinator.parentCoordinator = self coordinator.start() }
Дочерние потоки
Давайте предположим что у вас есть приложение без переходов. Флоу переходов огромный и повторяющийся. И вы ходите сделать переиспользуемый координатор. Да вы можете!
предположим что у вас есть очень простой интерфейс, который только переключает экраны. И вы хотите с модулировать флоу переходов как ниже!
Первое, что необходимо сделать это создать TransactionCoordinator
class TransactionCoordinator : Coordinator { var parentCoordinator: Coordinator? var children: [Coordinator] = [] var navigationController: UINavigationController let storyboard = UIStoryboard.init(name: "Main", bundle: .main) lazy var transactionViewModel = TransactionViewModel(nav: self) func start() { print("TransactionCoordinator Start") goToProductList() } init(navigationController : UINavigationController) { self.navigationController = navigationController } deinit { print("TransactionCoordinator deinit") } }
Затем Вы создаете TransactionViewModel
import Foundation protocol TransactionNavigation : AnyObject { func goToPayment() func goToProductList() func goBackToHome() } class TransactionViewModel { weak var navigation : TransactionNavigation! init(nav : TransactionNavigation) { self.navigation = nav } func goToPayment(){ navigation.goToPayment() } func goToProductList(){ navigation.goToProductList() } func goHome(){ navigation.goBackToHome() } }
Помните о реализации NavigationProtocol, и так ваш координатор может реализовать навигацию
import Foundation import UIKit class TransactionCoordinator : Coordinator { var parentCoordinator: Coordinator? var children: [Coordinator] = [] var navigationController: UINavigationController let storyboard = UIStoryboard.init(name: "Main", bundle: .main) lazy var transactionViewModel = TransactionViewModel(nav: self) func start() { print("TransactionCoordinator Start") goToProductList() } init(navigationController : UINavigationController) { self.navigationController = navigationController } deinit { print("TransactionCoordinator deinit") } } extension TransactionCoordinator : TransactionNavigation { func goToPayment() { let vc = storyboard.instantiateViewController(withIdentifier: "PaymentViewController") as! PaymentViewController vc.viewModel = transactionViewModel navigationController.pushViewController(vc, animated: true) } func goToProductList() { let vc = storyboard.instantiateViewController(withIdentifier: "ProductListViewController") as! ProductListViewController vc.viewModel = transactionViewModel navigationController.pushViewController(vc, animated: true) } func goBackToHome() { navigationController.popToRootViewController(animated: true) parentCoordinator?.childDidFinish(self) } }
И так, вы все настроили! Помните, что каждый раз когда вы хотите использовать флоу, необходимо создать TransactionCoordinator и запустить start(). Не забывайте добавить ViewModel зависимость, а также соединить с ViewCntroller’ами ViewModel’и. К примеру я использую единственную ViewModel для всего связанного флоу.
func goToBuyProduct() { // let vc = storyboard.instantiateViewController(withIdentifier: "ProductListViewController") // navigationController.pushViewController(vc, animated: true) // Use this code if you want to separate the transaction as a subflow. let transactionCoordinator = TransactionCoordinator(navigationController: navigationController) // Add the parent with self transactionCoordinator.parentCoordinator = self children.append(transactionCoordinator) transactionCoordinator.start() }
⚠️Позаботьтесь об утечке памяти⚠️
Добавьте это расширение в ваш координатор как здесь
import Foundation import UIKit protocol Coordinator : AnyObject { var parentCoordinator: Coordinator? { get set } var children: [Coordinator] { get set } var navigationController : UINavigationController { get set } func start() } extension Coordinator { /// Removing a coordinator inside a children. This call is important to prevent memory leak. /// - Parameter coordinator: Coordinator that finished. func childDidFinish(_ coordinator : Coordinator){ // Call this if a coordinator is done. for (index, child) in children.enumerated() { if child === coordinator { children.remove(at: index) break } } } }
Если Вы заметили childDidFinish в goBackToHome в координаторе. Что он делает? Это метод очистки. Помните ли вы что добавили дочерний координатор во внутрь родительского? Да, это должно быть отчищено, так ARC сможет освободить память от неиспользуемых координаторов. Если вы не вызовите его в конце координатора, то получите утечку памяти!
Помните о планировании ваших координаторов их области и их функционале, а также об их жизненном цикле. Когда они Завершились, они еще остались живы. Это может спасти вам большое количество утечек памяти.
Результат
Здесь результат использования Дочернего координатора!
Вы можете изучить глубже погружением на мою страничку на Гитхабе!
Заключение
Благословение иметь детей. Особенно когда они могут стать экспертами в чем-то. Но это тяжело уделять должное внимание когда их много. Также с дочерними координаторами. Они могут помочь вам управлять особенностями и модульностью целого флоу. Но когда их слишком много и они сложные, это становится сложно для обслуживания и они склонны создавать утечки памяти.
Но некоторые люди взялись за обработку утечек памяти путем использования шаблона координатора с инверсными ссылками😲😲. Я никогда не использовал их ранее, но уверен интересно посмотреть на них.
Оставляйте обратную связь! Я посмотрю все вопросы и комментарии когда буду свободен!
Желаю всем вам хорошего здоровья и хорошей еды! 🍫🍲