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







