Какой компонент в архитектуре MVVM+Router обычно владеет роутером и управляет навигацией?

Ответ

В чистой реализации MVVM+Router (или Coordinator) ответственность за навигацию делегируется ViewModel. Это позволяет соблюдать принцип единой ответственности.

Почему ViewModel, а не ViewController?

  • ViewModel содержит бизнес-логику и знает, когда должен произойти переход (например, после успешной загрузки данных или нажатия кнопки).
  • ViewController (View) остаётся «глупым»: он только отображает состояние из ViewModel и передаёт ей пользовательские события. Он не должен принимать решения о навигации.
  • Router/Coordinator инкапсулирует как выполняется переход (например, present или push).

Пример взаимодействия:

// ViewModel решает о переходе
class LoginViewModel {
    private let router: LoginRouter

    func loginButtonTapped() {
        // Проверка данных...
        router.showHomeScreen() // Команда на навигацию
    }
}

// Router выполняет переход
class LoginRouter {
    weak var viewController: UIViewController?

    func showHomeScreen() {
        let homeVC = HomeViewController()
        viewController?.navigationController?.pushViewController(homeVC, animated: true)
    }
}

// ViewController только передаёт событие
class LoginViewController: UIViewController {
    var viewModel: LoginViewModel!

    @IBAction func loginTapped(_ sender: Any) {
        viewModel.loginButtonTapped()
    }
}

Такой подход делает код тестируемым (можно замокать Router) и отделяет логику от UIKit.

Ответ 18+ 🔞

Да ты посмотри, какая красота получается, когда за навигацию отвечает ViewModel, а не этот вечно зажравшийся ViewController! Это ж как в хорошем ресторане: шеф-повар (ViewModel) решает, что и когда готовить, а официант (ViewController) просто таскает тарелки и улыбается. А координатор — это вообще метрдотель, который знает, куда какую тарелку поставить.

А че собственно ViewModel, а не ViewController?

  • ViewModel — это мозги операции. Она знает, когда пора переходить на другой экран: данные загрузились, кнопку нажали, звёзды сошлись. Вся логика тут.
  • ViewController — это просто говорящая голова, марионетка. Его задача — показать, что ему скажут, и доложить наверх, если юзер тыкнул куда не надо. Решения о навигации? Да он, блядь, даже дорогу до туалета сам найти не должен!
  • Router/Coordinator — это наш личный штурман. ViewModel кричит «НАДО ТУДА!», а этот чувак уже знает, как именно туда пролезть: present или push, может ещё с пиротехникой и фанфарами.

Смотри, как это в коде выглядит, ёпта:

// ViewModel — тут решают
class LoginViewModel {
    private let router: LoginRouter // Привязали штурмана

    func loginButtonTapped() {
        // Тут проверяем логин-пароль, хуле...
        router.showHomeScreen() // И командуем: "Вперёд, нахуй!"
    }
}

// Router — тут исполняют
class LoginRouter {
    weak var viewController: UIViewController? // Знает, откуда плывём

    func showHomeScreen() {
        let homeVC = HomeViewController() // Цель назначения
        viewController?.navigationController?.pushViewController(homeVC, animated: true) // И пошёл пуш!
    }
}

// ViewController — тут только тыкают и слушаются
class LoginViewController: UIViewController {
    var viewModel: LoginViewModel! // Привязали мозги

    @IBAction func loginTapped(_ sender: Any) {
        viewModel.loginButtonTapped() // Просто стучимся к мозгу: "Чувак, нажали!"
    }
}

Вот такой расклад, блядь. Код становится тестируемым — можно подсунуть фейкового роутера и смотреть, вызывает его вьюмодель или нет. И главное — вся эта UIKit-овская мутотень с переходами заперта в одном месте, а не размазана по всем контроллерам, как дерьмо по стенке. Красота, в рот меня чих-пых!