Создадим простую MVVM-С архитектуру для новичков в Swift.
Данная статья является переводом статьи от Bobby Pehtrus
Вводная часть
В этой статье, я хочу рассказать об архитектуре которую использую в моих проектах и на работе. Эта статья будет так же служить моей собственной документацией для меня же, так что я знаю как далеко я зашел изучая эту архитектуру.
Я начну мое объяснение с MVVM и Координатора, и небольшой снипет как реализовать это.
Извиняюсь за мою плохую грамматику английского, потому что это моя самая первая статья😅😅. Если зедсь есть чтонибудь что я могу улучшить или есть небольшие исправления, не стесняйтесь исправляйте меня! Быть разработчиком это долгая жизнь обучения, верно? 😁
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, есть хорошая статья с илюстрациями вот здесь. Если вы знаете эти принципы, это большой плюс и это спасет много ваших товарищей при написании кода😆😆.
Итак, куда мы пишем код связанный и навигацией? Да, в Координатор.
. . .
Координаторы
Как часто я использую координаторы, я вижу координаторы как гиды для перемещений. Они единственные знают куда идти и что тебе нужно. Если ты хочешь принять ванну, они знаю что тебе надо пойти в комнату под названием Ванная и они предоставят для тебя Полотенце, Мыло или Шампунь.
В мое случае, работа координатора сводится к созданию всех необходимых зависимостей. На пример, он создает 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) }
Итак я объяснил концепт использования координатора. Но как создать его и реализовать в проекте?
. . .
Реализация
Шаг 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 и создадим переменную Window для себя. Так же не забудьте удалить ApplicationManifest в Info.plist. Я так же установить Target приложения на 12.0 итак поддерживаем ранние IOS.
Далее внутри 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’а и соединил их с аутлетами в сторибордах.
Давайте создадим 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 может содержать главный экран, экран профиля или истории. Это зависит от требований вашего проекта и вашего образа управлять объектами.
Я буду рассказывать о дочернем координаторе в другой теме. Но если вы не хотите ждать можете посмотреть сам на список ссылок ниже.
Кнопка назад и Управление памятью тоже интересные темы. Но это сказка для обсуждения в другой раз
Рекомендованные ссылки
Я надеюсь эта статья будет полезной, извините за плохую грамматику английского. Если есть какие либо исправления или некоторые недостатки, я буду рад взглянуть на них.
Желаю вам хорошего здоровья и хорошей еды!🍫🍲
Улучшения и обновления
Привет! прошло много времени… Я был немного занят и прошел через какой-то тяжелый месяц. В любом случае, я разместил проект на Гитхабе! Здесь можете посмотреть! Есть небольшие изменения, но в целом концепт остался тем же.
Так что я прочитал очень полезный ответ от 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) } }
Не стесняйтесь исправлять мой код, каждое изменение может привести к более лучшему кода в будущем!