Налаштування архітектури 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 дні. Ми гарантуємо, що ваш проєкт отримає структуру, зручну для тестування, а команда — робочий процес без зайвого оверхеду. Зв'яжіться з нами для консультації.







