Настройка архитектуры MVP для iOS-приложения
Представьте: кодовая база UIKit-приложения разрослась до 20+ экранов, каждый ViewController тащит 500+ строк, а тесты отсутствуют или требуют запуска симулятора. Вы пробовали MVVM, но связка Combine с @Published не вписалась — проект слишком стар, или команда пришла из Android с её MVP-культурой. Наша команда имеет опыт настройки MVP на 15+ проектах и гарантирует работоспособность шаблона с первого коммита. В этой статье разберём ключевые протоколы, изоляцию UIKit и тестирование без симулятора.
Wikipedia: Model-View-Presenter — это архитектурный паттерн, в котором Presenter выступает посредником между View и Model. На iOS это особенно полезно для крупных проектов, где MVVM с Combine требует переписывания кода под реактивный стек.
Почему MVP подходит для UIKit-проектов?
Если ваш проект — не простой список с деталями, а сложный многоэкранный UIKit, MVP даёт чёткое разделение: Presenter не видит UIKit, ViewController тоньше, тесты пишутся без симулятора. В отличие от MVVM, где ViewModel подписывается на Combine, MVP использует простые протоколы и слабые ссылки. Это снижает порог входа и не требует переписывания кода под реактивный стек. Такой подход является элементом чистой архитектуры iOS, поскольку зависимости направлены от внешних слоёв к ядру.
Что такое MVP на iOS и в чём отличие от MVVM?
По определению Model-View-Presenter — это архитектурный паттерн, в котором Presenter выступает посредником между View и Model. В MVVM ViewController подписывается на @Published-свойства ViewModel через Combine. В MVP Presenter не знает о UIKit: он общается с протоколом View, который реализует ViewController. Нет биндингов, нет реактивности — полная контролируемость потока данных.
// View protocol — интерфейс для Presenter protocol ProfileView: AnyObject { func showUser(_ user: User) func showLoading(_ isLoading: Bool) func showError(_ message: String) } // Presenter — чистый Swift, ноль UIKit final class ProfilePresenter { weak var view: ProfileView? private let userRepository: UserRepository init(userRepository: UserRepository) { self.userRepository = userRepository } func viewDidLoad() { view?.showLoading(true) Task { do { let user = try await userRepository.fetchCurrentUser() await MainActor.run { view?.showLoading(false) view?.showUser(user) } } catch { await MainActor.run { view?.showLoading(false) view?.showError(error.localizedDescription) } } } } } // ViewController — тонкий, только UI final class ProfileViewController: UIViewController, ProfileView { private var presenter: ProfilePresenter! override func viewDidLoad() { super.viewDidLoad() presenter.viewDidLoad() } func showUser(_ user: User) { nameLabel.text = user.name avatarImageView.load(url: user.avatarURL) } func showLoading(_ isLoading: Bool) { isLoading ? activityIndicator.startAnimating() : activityIndicator.stopAnimating() } func showError(_ message: String) { // Toast или Alert } } Ключевой момент: weak var view: ProfileView? — слабая ссылка обязательна, иначе retain cycle. Presenter держит View, View держит Presenter — один из них должен быть weak.
Как тестировать Presenter без симулятора?
Главное преимущество MVP — Presenter тестируется без симулятора. Вот пример с моком:
final class MockProfileView: ProfileView { var didShowUser = false var isLoading = false func showUser(_ user: User) { didShowUser = true } func showLoading(_ isOn: Bool) { isLoading = isOn } func showError(_ message: String) { /* ignore */ } } func testViewDidLoad_success() async { let mockView = MockProfileView() let mockRepository = MockUserRepository(result: .success(User.fixture)) let sut = ProfilePresenter(userRepository: mockRepository) sut.view = mockView sut.viewDidLoad() // Небольшая пауза для async Task try await Task.sleep(nanoseconds: 100_000_000) XCTAssertTrue(mockView.didShowUser) XCTAssertFalse(mockView.isLoading) } Тест занимает миллисекунды, не требует XCUITest. Покрытие кода такими тестами достигает 80%, а экономия средств на регрессе — до 40% бюджета. Сравните: типичная MVVM-ViewModel с Combine требует настройки XCTestExpectation или Sink — MVP здесь значительно проще и быстрее.
Что входит в настройку MVP?
При заказе настройки MVP вы получаете:
- Полные Swift-файлы модуля: Presenter, ViewController, Router.
- Unit-тесты Presenter (5–10 кейсов на модуль, покрытие 80%).
- Интеграционные инструкции для добавления новых экранов.
- Обучение команды (1–2 созвона с демонстрацией).
Работа занимает 2–3 дня для нового проекта. Свяжитесь с нами для оценки объёма миграции существующих экранов. Получите консультацию по внедрению MVP в ваш проект.
Навигация в MVP: что такое Router / Wireframe?
Presenter не должен управлять навигацией напрямую — это нарушает Single Responsibility. Классическое решение: Router (или Wireframe в оригинальном MVP терминах):
protocol ProfileRouter: AnyObject { func navigateToEditProfile(user: User) func navigateToSettings() } Конкретный ProfileRouterImpl работает с UINavigationController — UIKit-зависимость изолирована. Presenter получает Router через DI и вызывает router.navigateToEditProfile(user:) — без знания о том, что происходит под капотом.
Как мигрировать MVC на MVP?
Миграция существующего MVC-экрана на MVP — задача на 4–8 часов на экран. Создайте протокол View, выделите логику в Presenter, добавьте Router. Перенесите зависимость от Model в Presenter. Реализуйте View в ViewController. Тестируйте Presenter. Такой пошаговый план применим для любого экрана, а итоговая архитектура становится модульной и тестируемой.
Пошаговая настройка MVP для нового проекта
- Определите модули. Каждый экран — отдельный модуль с протоколами View, Presenter, Router.
- Напишите протоколы. View — только методы отображения; Presenter — методы жизненного цикла и обработки событий; Router — навигационные методы.
- Реализуйте Presenter. Вся логика, работа с сервером/БД. Никакого UIKit.
- Реализуйте View (ViewController). Пробрасывайте вызовы к Presenter.
- Реализуйте Router. Создайте класс, который использует UINavigationController для переходов.
- Настройте DI. Используйте фабрику модулей или Swinject для компоновки зависимостей.
- Напишите тесты. Покройте каждый метод Presenter (5–10 кейсов на модуль).
| Шаг | Длительность |
|---|---|
| Определение модулей | 1–2 ч |
| Протоколы и Presenter | 3–4 ч |
| View и Router | 2–3 ч |
| DI и фабрика | 1–2 ч |
| Тесты (10 кейсов) | 2–3 ч |
| Интеграция | 1 ч |
Сравнение MVP и MVVM
| Характеристика | MVP | MVVM |
|---|---|---|
| Зависимость от UIKit | Нет (Presenter) | Нет (ViewModel) |
| Биндинг | Отсутствует | Combine (или Rx) |
| Тестирование без симулятора | Да | Да (с оговорками) |
| Сложность на SwiftUI | Неприменим | Естественен |
| Стек в проекте | UIKit | UIKit / SwiftUI |
Какие типичные ошибки допускают при внедрении MVP?
- Забыть weak на view — retain cycle, утечка памяти. Всегда используйте
weak var view: ViewProtocol?. - Смешивать логику навигации в Presenter — выделите Router.
- Не тестировать Presenter — теряете главное преимущество MVP. Покрывайте моками.
- Переусложнять протоколы — один метод на экран вместо дробления. Держите количество методов View в разумных пределах (5–7).
Закажите настройку MVP для вашего проекта — получите готовый шаблон за 2–3 дня. Мы гарантируем, что ваш проект получит структуру, удобную для тестирования, а команда — рабочий процесс без лишнего оверхеда. Свяжитесь с нами для консультации.







