В чем разница между Presenter (MVP) и ViewModel (MVVM)?

«В чем разница между Presenter (MVP) и ViewModel (MVVM)?» — вопрос из категории Архитектура, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Presenter (паттерн MVP)

  • Роль: Посредник между Model и View. Содержит логику презентации (как отображать данные).
  • Знание о View: Имеет сильную ссылку (weak) на абстракцию View (протокол).
  • Управление UI: Активно и императивно вызывает методы View для её обновления.
  • Тестирование: Легко тестируется через mock-объект View.
    
    // 1. Протокол View
    protocol LoginViewProtocol: AnyObject {
    func showError(message: String)
    func navigateToHome()
    }

// 2. Presenter class LoginPresenter { weak var view: LoginViewProtocol? let authService: AuthService

func loginButtonTapped(email: String, password: String) {
    authService.login(email: email, password: password) { [weak self] result in
        switch result {
        case .success:
            self?.view?.navigateToHome() // ЯВНЫЙ вызов
        case .failure(let error):
            self?.view?.showError(message: error.localizedDescription) // ЯВНЫЙ вызов
        }
    }
}

}


**ViewModel (паттерн MVVM)**
*   **Роль:** Предоставляет **данные и команды**, специфичные для View. Преобразует сырые данные Model в удобный для отображения формат.
*   **Знание о View:** **Не знает** о существовании View. Не содержит ссылок на UI-компоненты.
*   **Управление UI:** **Реактивно**. View (или ViewController) **подписывается** на изменения свойств ViewModel (через `@Published`, `ObservableObject`, RxSwift).
*   **Тестирование:** Тестируется без моков View, проверяется состояние свойств.
```swift
// 1. ViewModel
class LoginViewModel: ObservableObject {
    @Published var errorMessage: String? = nil // Наблюдаемое состояние
    @Published var isLoading: Bool = false
    let authService: AuthService

    func login(email: String, password: String) {
        isLoading = true
        authService.login(email: email, password: password) { [weak self] result in
            self?.isLoading = false
            switch result {
            case .success:
                // Навигация будет обработана на уровне View (напр., через Coordinator)
                break
            case .failure(let error):
                self?.errorMessage = error.localizedDescription // ИЗМЕНЕНИЕ состояния
            }
        }
    }
}

// 2. View (SwiftUI) автоматически реагирует на изменения
struct LoginView: View {
    @StateObject var viewModel = LoginViewModel()
    var body: some View {
        VStack {
            if let error = viewModel.errorMessage {
                Text(error).foregroundColor(.red) // Автоматическое обновление
            }
            Button("Login") { viewModel.login(...) }
                .disabled(viewModel.isLoading)
        }
    }
}

Итог:

  • Presenter говорит View, что делать (view.showError(...)).
  • ViewModel сообщает об изменении состояния (errorMessage = "..."), а View реагирует на это сама.