Создание дочерних координаторов

Данная статья является переводом статьи от Bobby Pehtrus

Часть руководства по MVVM-C для IOS

В итоге, мне удалось выпустить еще одну статью на медиуме. Я очень признателен за благодарности и я удивлен что смог получить на этом деньги😁. Это был тяжелый месяц, поэтому задержал некоторые из моих статей. Надеюсь Вам понравится!

. . .

Photo by Piron Guillaume on Unsplash

Так, ранее мы кратко говорили о введение в шаблон 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 сможет освободить память от неиспользуемых координаторов. Если вы не вызовите его в конце координатора, то получите утечку памяти!

Помните о планировании ваших координаторов их области и их функционале, а также об их жизненном цикле. Когда они Завершились, они еще остались живы. Это может спасти вам большое количество утечек памяти.

Результат

Здесь результат использования Дочернего координатора!

Используйте дочерние координаторы

Вы можете изучить глубже погружением на мою страничку на Гитхабе!

Заключение

Благословение иметь детей. Особенно когда они могут стать экспертами в чем-то. Но это тяжело уделять должное внимание когда их много. Также с дочерними координаторами. Они могут помочь вам управлять особенностями и модульностью целого флоу. Но когда их слишком много и они сложные, это становится сложно для обслуживания и они склонны создавать утечки памяти.

Но некоторые люди взялись за обработку утечек памяти путем использования шаблона координатора с инверсными ссылками😲😲. Я никогда не использовал их ранее, но уверен интересно посмотреть на них.

Оставляйте обратную связь! Я посмотрю все вопросы и комментарии когда буду свободен!

Желаю всем вам хорошего здоровья и хорошей еды! 🍫🍲