Connect with us

Разработка

Паттерн Координатор: удаляем навигацию из ViewController

Почему опытные iOS-команды переносят логику навигации из ViewControllers в координаторы?

Опубликовано

/

     
     

Прежде чем понять, что такое координаторы, нам сначала нужно увидеть, какую проблему они решают.

Рассмотрим простой процесс авторизации:

LoginScreen -> HomeScreen
LoginScreen -> ForgotPasswordScreen

Типичная реализация для начинающих выглядит следующим образом:

class LoginViewController: UIViewController {
@IBAction func loginTapped() {
        if loginSuccessful {
            let homeVC = HomeViewController()
            navigationController?.pushViewController(homeVC, animated: true)
        }
    }
    @IBAction func forgotPasswordTapped() {
        let forgotVC = ForgotPasswordViewController()
        navigationController?.pushViewController(forgotVC, animated: true)
    }
}

На первый взгляд, это кажется нормальным.

Но есть скрытые проблемы.

Теперь LoginViewController знает:

  • Как создать HomeViewController
  • Как создать ForgotPasswordViewController
  • Как работает навигация
  • Каким должен быть следующий экран

Это означает, что контроллер представления теперь управляет потоком приложения.

Это создает сильную взаимосвязь между экранами.

LoginViewController
     |
     | creates
     v
HomeViewController

Эта взаимосвязь усиливается по мере развития приложения.

Паттерн Координатор: удаляем навигацию из ViewController

Реальный пример

Представьте себе реальное приложение для электронной коммерции:

ProductList
     |
     v
ProductDetail
     |
     +----> Reviews
     |
     +----> AddToCart
     |
     +----> SellerProfile

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

Вскоре ваш код будет выглядеть так:

ProductListVC
   |
   | pushes
   v
ProductDetailVC
   |
   | pushes
   v
ReviewVC

Теперь представьте:

  • Изменения на экране сведений о товаре
  • Изменения в последовательности действий на экране отзывов
  • Изменения в последовательности действий в корзине

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

Принцип единственной ответственности.

Контроллер представления должен управлять пользовательским интерфейсом, а не потоком навигации.

Идея паттерна Координатор

Паттерн Координатор отделяет логику навигации от контроллеров пользовательского интерфейса.

Вместо того чтобы контроллеры представления управляли навигацией, объект Координатор управляет потоком.

Контроллер представления только сообщает о действиях пользователя.

Координатор решает, что произойдет дальше.

Структура становится такой:

Coordinator
    |
    | creates
    v
ViewController

Контроллеры представлений становятся проще.

Они лишь сообщают координатору о том, что произошло.

Пример:

User tapped Login button
User selected product
User requested password reset

Координатор отвечает за навигацию.

Базовая структура координатора

Простой координатор обычно содержит:

  • Контроллер навигации
  • Метод запуска
  • Дочерние координаторы (необязательно)

Базовый протокол:

protocol Coordinator {
    var navigationController: UINavigationController { get set }
    func start()
}

Этот протокол определяет стандартную структуру для координаторов.

Создание AppCoordinator

Первый координатор в приложении обычно управляет начальным потоком.

AppCoordinator
     |
     v
LoginCoordinator
     |
     v
HomeCoordinato

Пример реализации:

class AppCoordinator: Coordinator {

var navigationController: UINavigationController
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    func start() {
        showLogin()
    }
    private func showLogin() {
        let loginVC = LoginViewController()
        loginVC.delegate = self
        navigationController.pushViewController(loginVC, animated: false)
    }
}

Здесь координатор создает контроллер представления.

А не наоборот.

Контроллер представления без навигации

Теперь контроллер представления становится очень простым.

LoginViewController

Оно только отправляет действия пользователя.

Пример:

protocol LoginViewControllerDelegate: AnyObject {
    func loginSuccessful()
    func forgotPasswordTapped()
}

class LoginViewController: UIViewController {
    weak var delegate: LoginViewControllerDelegate?
    @IBAction func loginTapped() {
        delegate?.loginSuccessful()
    }
    @IBAction func forgotPasswordTapped() {
        delegate?.forgotPasswordTapped()
    }
}

Обратите внимание на важную вещь.

В контроллере представления нет кода навигации. Нет push-уведомлений. Нет модальных окон. Нет контроллера навигации.

Контроллер просто сообщает о событиях.

Координатор обрабатывает навигацию

Теперь координатор решает, что произойдет дальше:

extension AppCoordinator: LoginViewControllerDelegate {
func loginSuccessful() {
        showHome()
    }
    func forgotPasswordTapped() {
        showForgotPassword()
    }
    private func showHome() {
        let homeVC = HomeViewController()
        navigationController.pushViewController(homeVC, animated: true)
    }
    private func showForgotPassword() {
        let forgotVC = ForgotPasswordViewController()
        navigationController.pushViewController(forgotVC, animated: true)
    }
}

Теперь вся логика навигации сосредоточена в одном месте.

Координатор управляет потоком выполнения. ViewController управляет пользовательским интерфейсом.

Визуализация архитектуры

Без координаторов:

LoginVC ---> HomeVC
   |
   ---> ForgotPasswordVC

С Координатором:

        Coordinator
          /      \
         v        v
     LoginVC   ForgotVC

Контроллеры представлений не знают друг о друге. Они взаимодействуют только с координатором.

Дочерние координаторы

В больших приложениях часто содержится несколько потоков.

Примеры:

  • Поток аутентификации
  • Поток оформления заказа
  • Поток настроек

Каждый поток может иметь свой собственный координатор. Пример структуры:

AppCoordinator
    |
    +---- AuthCoordinator
    |
    +---- MainCoordinator
    |
    +---- CheckoutCoordinator

Пример:

class AuthCoordinator: Coordinator {
var navigationController: UINavigationController
    func start() {
        showLogin()
    }
    private func showLogin() {
        let vc = LoginViewController()
        vc.delegate = self
        navigationController.pushViewController(vc, animated: true)
    }
}

Это обеспечивает изоляцию потоков.

Передача данных между экранами

Еще одно преимущество координаторов — более чистый поток данных.

Пример:

ProductList -> ProductDetail

С Координатором:

let detailVC = ProductDetailViewController()
detailVC.product = selectedProduct
navigationController?.pushViewController(detailVC)

Без него:

func productSelected(_ product: Product) {
    coordinator?.showProductDetail(product)
}

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

func showProductDetail(_ product: Product) {
    let vc = ProductDetailViewController(product: product)
    navigationController.pushViewController(vc, animated: true)
}

Теперь поток данных централизован.

Управление памятью координаторов

Одна из проблем, с которой сталкиваются разработчики — освобождение координаторов. Если координаторы не удаляются должным образом, они остаются в памяти.

Пример:

var childCoordinators: [Coordinator] = []

Когда поток заканчивается:

childCoordinators.removeAll { $0 === coordinator }

Это гарантирует освобождение координаторов.

Когда следует использовать шаблон проектирования Координатор

Координаторы полезны, когда в вашем приложении сложные потоки навигации.

Пример:

  • регистрация пользователя
  • оформление заказа
  • аутентификация
  • глубокие ссылки

Они также полезны, когда:

  • над приложением работают несколько команд
  • потоки часто меняются
  • тестирование логики навигации

Однако небольшим приложениям координаторы могут не понадобиться.

Распространенные ошибки разработчиков

1. Слишком большие координаторы

Некоторые разработчики помещают бизнес-логику внутрь координаторов. Это неправильно. Координаторы должны управлять только потоком навигации.

2. ViewControllers продолжают выполнять навигацию

Иногда разработчики оставляют push логику внутри контроллеров представлений.

Это противоречит цели использования координаторов.

3. Неправильный подход к управлению жизненным циклом координаторов

Если вы забудете удалить дочерние координаторы, то это может привести к утечкам памяти.

Реальные вопросы на собеседованиях по шаблону Координатор

На собеседованиях по iOS часто задают вопросы по архитектуре.

Вот некоторые из них.

Вопрос 1: Почему навигация внутри контроллеров представлений считается плохой практикой?

Хороший ответ: Контроллеры представлений должны фокусироваться на логике пользовательского интерфейса. Когда в них включается логика навигации, контроллеры становятся тесно связанными и сложными в поддержке.

Вопрос 2: Какую проблему решает шаблон Координатор?

Ответ: Он отделяет логику навигации от контроллеров представлений и централизует управление потоками.

Вопрос 3: Как контроллеры представлений взаимодействуют с координаторами?

Распространенные подходы:

Вопрос 4: Что такое дочерние координаторы?

Дочерние координаторы управляют конкретными потоками, такими как аутентификация или оформление заказа.

Они контролируются родительским координатором.

Вопрос 5: Заменяет ли шаблон «Координатор» MVC или MVVM?

Нет. Координатор работает совместно с архитектурными шаблонами.

Пример:

  • Coordinator + MVVM
  • Coordinator + MVC
  • Coordinator + VIPER

Он только управляет навигацией.

Почему многие команды используют координаторы

Крупные компании предпочитают координаторы, потому что они хорошо масштабируются.

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

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: