Как организовать связь между Coordinator и ViewController в iOS?

Ответ

Связь между Coordinator и ViewController строится по принципу инверсии зависимостей: ViewController сообщает о событиях, а Coordinator принимает решения о навигации. Вот основные паттерны:

1. Через замыкания (Closures) — популярный и гибкий способ Coordinator внедряет замыкания во ViewModel или напрямую во ViewController.

// В Coordinator
func showLoginScreen() -> UIViewController {
    let viewModel = LoginViewModel()

    // ViewModel получает замыкание для навигации
    viewModel.onLoginSuccess = { [weak self] user in
        self?.showHomeScreen(for: user)
    }

    viewModel.onSignUpTapped = { [weak self] in
        self?.showSignUpScreen()
    }

    return LoginViewController(viewModel: viewModel)
}

2. Через протоколы/делегаты (Protocols/Delegates) ViewController объявляет делегата, который реализует Coordinator.

// Протокол для событий ViewController
protocol ProductListDelegate: AnyObject {
    func didSelectProduct(_ product: Product)
    func didTapFilters()
}

// ViewController хранит слабую ссылку на делегата
class ProductListViewController: UIViewController {
    weak var delegate: ProductListDelegate?

    private func productSelected(_ product: Product) {
        delegate?.didSelectProduct(product) // Сообщаем координатору
    }
}

// Coordinator реализует протокол
class AppCoordinator: ProductListDelegate {
    func start() -> UIViewController {
        let vc = ProductListViewController()
        vc.delegate = self // Устанавливаем связь
        return vc
    }

    // MARK: - ProductListDelegate
    func didSelectProduct(_ product: Product) {
        let detailVC = makeDetailVC(for: product)
        navigationController.pushViewController(detailVC, animated: true)
    }
}

3. Через Router (отдельный объект) Навигационная логика выносится в отдельный объект Router, которым управляет Coordinator.

Критически важные правила:

  • Всегда используйте [weak self] в замыканиях или weak var delegate, чтобы избежать циклов сильных ссылок (retain cycles).
  • ViewController не должен знать о других ViewController'ах. Он только сообщает о событии («пользователь нажал кнопку»).
  • Весь поток навигации (последовательность экранов) управляется Coordinator. Это делает его центральным узлом для тестирования и изменения навигации.

Ответ 18+ 🔞

Смотри, тут вся философия в том, что один другого за жопу не держит, а просто кричит в темноту «Эй, я тут кнопку нажал!». А уже координатор, этот главный по цирку, решает, куда прыгнуть дальше. Вот как это на практике выглядит, без всей этой академической пурги.

1. Замыкания (Closures) — просто, как три копейки Координатор впендюривает во ViewModel или прямо в контроллер функции-прыгалки. Контроллер их дергает — и полетели.

// Координатор, он же режиссёр этой басни
func makeLoginScreen() -> UIViewController {
    let viewModel = LoginViewModel()

    // Скармливаем вьюмодели замыкания на все случаи жизни
    viewModel.onLoginSuccess = { [weak self] user in
        // Пользователь залогинился — гоним его на главную
        self?.showHomeScreen(for: user)
    }

    viewModel.onSignUpTapped = { [weak self] in
        // Захотел регистрироваться — получай форму
        self?.showSignUpScreen()
    }

    return LoginViewController(viewModel: viewModel)
}

Контроллер при этом нихуя не знает про showHomeScreen или showSignUpScreen. Он просто, как обезьянка, тыкнул кнопку — и дернул onSignUpTapped(). А куда это приведёт — его, мартышку, не ебёт.

2. Протоколы/делегаты (Protocols/Delegates) — классика, для любителей формальностей Тут контроллер объявляет: «Слушай, я буду орать вот в эту трубу, а ты подставь ухо». Координатор подставляет.

// Это та самая труба, интерфейс для ора
protocol ProductListDelegate: AnyObject {
    func didSelectProduct(_ product: Product)
    func didTapFilters()
}

// Контроллер списка товаров
class ProductListViewController: UIViewController {
    // Слабая ссылка, блядь! Чтобы не получить цикл и не накрыться медным тазом.
    weak var delegate: ProductListDelegate?

    private func productSelected(_ product: Product) {
        // Произошло событие — орем в трубу
        delegate?.didSelectProduct(product)
    }
}

// Координатор реализует протокол и слушает
class AppCoordinator: ProductListDelegate {
    func start() -> UIViewController {
        let vc = ProductListViewController()
        vc.delegate = self // Вот тут и подключаем трубу
        return vc
    }

    // MARK: - ProductListDelegate
    // А вот тут уже реагируем на ор
    func didSelectProduct(_ product: Product) {
        let detailVC = makeDetailVC(for: product)
        navigationController.pushViewController(detailVC, animated: true)
    }
}

3. Через Router — для параноиков, которые хотят ещё один слой абстракции Выносим всю механику пушей-попов в отдельную сущность, а координатор ей рулит. Ну, типа, чтобы ещё чище было. Иногда это овердохуища кода, но для больших проектов бывает оправдано.

Главные заповеди, которые нарушать — себя не уважать:

  • [weak self] и weak var — это святое. Забудешь — получишь retain cycle, память потечёт, и приложение накроется тихо и беспощадно. Проверено на собственных костылях.
  • ViewController — тупая мартышка. Он не должен знать, какой экран следующий. Его удел — показать кнопки и заорать делегату или дернуть замыкание: «Хозяин, тут нажали!».
  • Весь цирк с шапито — в координаторе. Последовательность экранов, переходы, передача данных — это всё его вотчина. Хочешь изменить поток — ковыряешь в одном месте, а не бегаешь по всем контроллерам, как угорелый.

Вот и вся магия. Никакой телепатии, просто один кричит, другой слушает и решает, куда идти. А то, бывает, напишут такое, что сам Тургенев с Муму в гробу перевернулся бы.