Ответ
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 реагирует на это сама.