Руководство по архитекруре MVVM + Координатор IOS

Создадим простую MVVM-С архитектуру для новичков в Swift.

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

Вводная часть

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

 Я начну мое объяснение с MVVM и Координатора, и небольшой снипет как реализовать это.

Извиняюсь за мою плохую грамматику английского, потому что это моя самая первая статья😅😅. Если зедсь есть чтонибудь что я могу улучшить или есть небольшие исправления, не стесняйтесь исправляйте меня! Быть разработчиком это долгая жизнь обучения, верно? 😁

Фото Bogdan Karlenko на Unsplash

MVVM

Model-View-ViewModel или MVVM архитектура была очень популярна в разработке под IOS. Ее использовали в отрасли уже некоторое время. С появлением SwiftUI (декларативный UI), эта архитектура стала обязательной для IOS разработчиков этой отрасли.

MVVM сделал великую работу по делению бизнес и UI логики. Это решило огромную проблему View controller’ов. Если у вас есть опыт использования MVC. Но, вы можете сократить ViewController еще больше путем разделения кода навигации в другие файлы. Я ссылаюсь на код навигации: 

navigationController?.pushViewController(vc, animated: true)
navigationController?.presentViewController(vc, animated: true)

Но почему? Он содержит всего 1-2 строки кода, конечно же это не важно, верно?

Это не может способствовать делению ViewController’ов. И это нарушает принципы SOLID в частности Принцип единой ответственности. Если вы хотите узнать про принципы SOLID, есть хорошая статья с илюстрациями вот здесь. Если вы знаете эти принципы, это большой плюс и это спасет много ваших товарищей при написании кода😆😆.

Итак, куда мы пишем код связанный и навигацией? Да, в Координатор.

. . .

Photo by Brendan Church on Unsplach

Координаторы

Как часто я использую координаторы, я вижу координаторы как гиды для перемещений. Они единственные знают куда идти и что тебе нужно. Если ты хочешь принять ванну, они знаю что тебе надо пойти в комнату под названием Ванная и они предоставят для тебя Полотенце, Мыло или Шампунь.

В мое случае, работа координатора сводится к созданию всех необходимых зависимостей. На пример, он создает ViewController и ViewModel. Координатор передает ViewModel во ViewController. Также координатор ответственен за инициализацию API сервиса, или других сервисов и внедряет во ViewModel или ViewController в случае необходимости.

Краткая илюстрация: Ты(ViewController) собираешся в школу, твоя мама(Координатор) будит тебя (инициализирует ViewController), подготавливает твой обед (зависимости/сервисы) и кладет твою домашнюю работу(ViewModel) в твой рюгзак. И отправляет тебя в школу.

В Swift, выглядит так:

func goToLogin() {
    let vc = LoginViewController.instantiate(from: authStoryboard)
    let vm = LoginViewModel()
    vm.apiClient = authApi
    vm.authCoordinator = self
    vc.viewModel = vm
    navigationController.setViewControllers([vc], animated: true)
}

Итак я объяснил концепт использования координатора. Но как создать его и реализовать в проекте?

. . .

MVVM+C архитектура, by: Daniel Lozano Valdies. Постори его блог! У него есть глубокий всесторонний урок по этой архитектуре, я так же следовал его инструкциям. Очень полезно!

Реализация

Шаг 1: Заложим фундамент координатора

После создания проекта в XCode, давайте создадим App Coordinator. Это позже будет жизнено-важный блок вашего приложения.

protocol Coordinator {
    var parentCoordinator: Coordinator? { get set }
    var children: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    
    func start()
}

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

Далее создайте AppCoordinator:

class AppCoordinator: Coordinator {
    var parentCoordinator: Coordinator?
    var children: [Coordinator] = []
    var navigationController: UINavigationController
    
    init(navCon : UINavigationController) {
        self.navigationController = navCon
    }
    
    func start() {
        print("App Coordinator start")
    }
}

Что такое Start()? Пуск будет содержать начальную операцию или флоу в приложении. Мы вернемся к этому позже.

Часть 2: Удаление SceneDelegate и настройка AppDelegate.

Идем в AppDelegate и создаем ваш собственный Window. Зачем? AppCoordinator необходим как глобальный родительский координатор для вашего приложения, таким образом должны начать приложение с использованием AppCoordinator’а в целом. Сделайте это, мы имеем две кастомные инициализации самого приложения удерживая его собственное Window. Как в IOS 13, существует Scene Delegate у которого есть наша переменная Window.

Удаление SceneDelegate
SceneDelegate.swift, Это было добавлено Apple начиная с IOS 13 и поддерживает Swift UI.

Давайте удалим SceneDelegate и создадим переменную Window для себя.  Так же не забудьте удалить ApplicationManifest в Info.plist. Я так же установить Target приложения на 12.0 итак поддерживаем ранние IOS.

изменение AppDelegate
AppDelegate.swift, если ты собирается разрабатывать версий ниже чем IOS 13, я рекомендую тебе удалить все функции связанные со сценами, потому что это вызовет проблемы на ранних устройствах.(Ты можешь оставить их если хочешь)

Далее внутри didFinishLaunchingWithOptions давате создадим нашу переменную Window и AppCoordinator

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    var appCoordinator : AppCoordinator?
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
         window = UIWindow(frame: UIScreen.main.bounds)
         let navigationCon = UINavigationController.init()
         appCoordinator = AppCoordinator(navigationController: navigationCon)
         appCoordinator!.start()
         window!.rootViewController = navigationCon
         window!.makeKeyAndVisible()
         return true
     }
}

И потом запустите проект, запуститься вот так.

Пустой начальный экран

Пустой UINavigationController и вы увидите запуск приложения! Распечатанный в консоле текст. Что означает, ваше приложение запускается с помощью AppCoordinator!

Шаг 3: Инструменты для Координатора

Итак, как вы видите. Оно пустое. У вас также есть настроенные  Main.storyboard с initialViewController. Но почему не работает? Потому что установили в window значение. Вы можете прочитать здесь чтобы узнать как работает AppDelegate.

. . .

О Сториборде

Если вы используете координаторы и сториборды, тогда сигвеи и инициализацию ViewController’ов больше не используем, потому что координаторы единственные, кто ответственный за навигацию и передачу данных между VC’ми. По моему, когда я использую координаторы, я вижу сториборды как контейнеры множества VC’ов. Это помогает только сгруппировать ViewController’ры и лэйауты. В данной архитектуре не используем сигвеи, потому что их роль берет на себя координаторы.

Я рекомендую работать со множеством сторибордов в больших проектах. Это позволит вам сгруппировать ViewController’ы и позволит сделать ваш проект более аккуратным и структурированным. Если вы хотите поделится этим со мной, комментируйте не стесняйтесь! 😁😁

Сейчас, пока у вас есть Main Interface (General -> Deployment info) настраиваемых в вашем Main.storyboards все будет работать хорошо.

. . .

Добавим методы в наш AppCoordinator

class AppCoordinator : Coordinator {
    var parentCoordinator: Coordinator?
    var children: [Coordinator] = []
    var navigationController: UINavigationController
    init(navigationController : UINavigationController) {
        self.navigationController = navigationController
    }
    func start() {
         // The first time this coordinator started, is to launch login page.
    goToLoginPage()
    }
    
    let storyboard = UIStoryboard.init(name: "Main", bundle: .main)
    func goToLoginPage(){
         // Instantiate LoginViewController
         let loginViewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
         // Instantiate LoginViewModel
         let loginViewModel = LoginViewModel.init()
         // Set the Coordinator to the ViewModel
         loginViewModel.appCoordinator = 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()
        registerViewModel.appCoordinator = self
        registerViewController.viewModel = registerViewModel
         navigationController.pushViewController(registerViewController, animated: true)
    }
}

Как вы видите, цель этих методов исключительно для внедрения и навигации путем использования UINavigationController. Это включает APiServices, или даже ViewModels, а это больше чем два ViewController’a. Для передачи данных, вы только добавляете параметры в методы и внедряете в следующий VC или VM.

Шаг 4: Показ UIViewControllers с ViewModel’ю

Я добавил 2 очень простых ViewController’а и соединил их с аутлетами в сторибордах.

LoginViewController и RegisterViewController

Давайте создадим ViewModel для каждого нашего ViewController’а. LoginViewController содержит только функцию запроса координатора для перехода на экран регистрации. Это тоже самое что RegisterViewController.

import Foundation
class LoginViewModel {
    weak var coordinator : AppCoordinator!
    
    func goToRegister(){
        coordinator.goToRegisterPage()
    }
}
class RegisterViewModel {
    weak var appCoordinator : AppCoordinator!
    func goToLogin(){
        appCoordinator.goToLoginPage()
    }
}

Здесь оба ViewController’а

class LoginViewController : UIViewController {
    var viewModel : LoginViewModel!
    @IBOutlet weak var registerButton: UIButton!
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func registerButtonTapped(_ sender: Any) {
        viewModel.goToRegister()
     }
}
class RegisterViewController: UIViewController {
    var viewModel : RegisterViewModel!
    @IBOutlet weak var backToLoginButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func backToLoginTapped(_ sender: Any) {
        viewModel.goToLogin()
    }
}

Вы можете видеть, что сейчас ViewController’ы могут быть сфокусированы на операциях связанных с View. Потому что, процесс навигации перемещен в координатор.

Результат

пример работы координатора
результат

и сейчас масштабирование приложения готово и в вашем распоряжении.

Другие темы

Итог… Если весь код навигации был перемещен в AppCoordinator, не станет ли AppCoordinator ОгромнымAppCoordinator? Итак это зависит от того как вы его будете использовать. Но нет.

Координатор может иметь много дочерних координаторов.

Дочерний координатор помогает группировать код навигации. Как пример, если у вас есть экраны связанные с авторизацией и вы хотите их связать, то вы можете создать AuthCoordinator для обработки Логина, Регистрации, ПИН кода или одноразового пароля для смены ПИН кода. HomeCoordinator может содержать главный экран, экран профиля или истории. Это зависит от требований вашего проекта и вашего образа управлять объектами.

Я буду рассказывать о дочернем координаторе в другой теме. Но если вы не хотите ждать можете посмотреть сам на список ссылок ниже.

Кнопка назад и Управление памятью тоже интересные темы. Но это сказка для обсуждения в другой раз

Рекомендованные ссылки

Принципы SOLID с публикациями

Руководство MVVM+C

Я надеюсь эта статья будет полезной, извините за плохую грамматику английского. Если есть какие либо исправления или некоторые недостатки, я буду рад взглянуть на них.

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

Улучшения и обновления

Привет! прошло много времени… Я был немного занят и прошел через какой-то тяжелый месяц. В любом случае, я разместил проект на Гитхабе! Здесь можете посмотреть! Есть небольшие изменения, но в целом концепт остался тем же.

Так что я прочитал очень полезный ответ от Russ Warwick, поместить всю кучу функций во внутрь протокола и реализовать их в координаторе лучше чем передавать экземпляр координатора во ViewModel.

Так что я добавил протокол LoginNavigator. Имея это, VIewModel ничего не знает о координаторе. Это лучший путь реализации и координатор может быть легко изменен

import Foundation

protocol LoginNavigation : AnyObject{
    func goToRegisterPage()
    func goToHome()
}

class LoginViewModel {
    
    weak var navigation : LoginNavigation!
    
    init(nav : LoginNavigation) {
        self.navigation = nav
    }
    
    func goToRegister(){
        navigation.goToRegisterPage()
    }
    
    func goToHome(){
        navigation.goToHome()
    }
    
    deinit {
        print("Deinit login")
    }
}
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)
    }
}

Не стесняйтесь исправлять мой код, каждое изменение может привести к более лучшему кода в будущем!