Если вы когда-нибудь открывали проект, где ViewModel превратилась в свалку логики, навигации и форматирования дат - поздравляю, вы видели MVVM-курильщика. Рассказываю, почему «чистый» MVVM из учебников рассыпается в бою, как превратить ViewModel в машину состояний вместо мусорного ведра, и почему import UIKit в вашей VM - это диагноз. Без воды, с примерами кода и болью. Эта статья - не очередной пересказ документации. Вы научитесь разделять данные и их представление так, чтобы тесты писались сами собой, а ваши коллеги не проклинали вас на код-ревью.
Я придерживаюсь мнения, что архитектура - это не про то, как разложить файлы по папкам, а про то, как вы управляете сложностью и состоянием. Давайте разберем, как заставить MVVM работать на вас, а не против вас.
Почему MVVM часто проваливается на практике
Большинство проблем с MVVM проистекают из неверного понимания ответственности. Часто ViewModel воспринимается как «место, куда я кладу всё, что не влезло в View».
Основные причины провала:
Нарушение инкапсуляции: View знает слишком много о внутренностях ViewModel, или, что еще хуже, ViewModel хранит ссылки на UI-компоненты. Если в вашей ViewModel есть
import UIKit(или любая другая UI-библиотека), у вас проблемы.Отсутствие четкого State: Переменные
@Published var name,@Published var isLoading,@Published var errorживут своей жизнью. В итоге можно получить состояние, когда и спиннер крутится, и ошибка показывается одновременно. Это «невалидное состояние», и это грех.Логика навигации внутри VM: ViewModel не должна решать, куда идти дальше. Ее дело - сказать: «Я закончила работу, данные сохранены». А вот кто и куда после этого поведет пользователя - задача координатора или роутера.
Основные принципы: разделение, тестируемость, unidirectional data flow
Забудьте про двустороннее связывание (Two-Way Binding) как про стандарт. Оно уместно в простых формах ввода, но в сложных экранах оно превращает поток данных в хаос. Будущее (и настоящее) - за Unidirectional Data Flow (UDF).
View отправляет Action (нажатие кнопки,
viewDidLoad).ViewModel обрабатывает Action, дергает сервис и обновляет State.
View подписывается на State и перерисовывается.
Это делает систему предсказуемой. Вы всегда знаете, какое событие привело к изменению состояния. К тому же, это в разы упрощает тестирование: вы просто подаете на вход экшен и проверяете, соответствует ли итоговый стейт ожидаемому.
ViewModel как машина состояний (а не просто мешок свойств)
Вместо россыпи разрозненных свойств я предпочитаю использовать единый State. Идеальный инструмент для этого - enum.
Swift
final class ProductListViewModel: ObservableObject {
enum State {
case idle
case loading
case loaded([Product])
case error(String)
}
@Published private(set) var state: State = .idle
private let repository: ProductRepositoryProtocol
init(repository: ProductRepositoryProtocol) {
self.repository = repository
}
func loadProducts() {
state = .loading
repository.fetchProducts { [weak self] result in
switch result {
case .success(let products):
self?.state = .loaded(products)
case .failure(let error):
self?.state = .error(error.localizedDescription)
}
}
}
}
Почему это круто? Потому что View теперь максимально тупая. Она просто «рендерит» стейт. В SwiftUI это превращается в элегантный switch внутри body. Я категорически против логики в View, даже если это простой if-else. Чем меньше View «думает», тем меньше шансов поймать странные баги при рендеринге.
Обработка побочных эффектов: работа с сетью, аналитика, навигация
ViewModel - это диспетчер. Она не должна сама лезть в сеть или писать в базу. Она вызывает абстракцию (протокол).
Навигация
Я сторонник паттерна Coordinator. ViewModel должна сообщать о необходимости навигации через замыкание или делегат.
Swift
final class LoginViewModel: ObservableObject {
var onLoginSuccess: (() -> Void)?
func handleLogin() {
// ... логика авторизации
onLoginSuccess?()
}
}
Аналитика
Не засоряйте методы бизнес-логики вызовами Analytics.log(...). Это «побочный эффект». Лучше всего выносить это в отдельные декораторы или использовать обсерверы, которые следят за изменением состояния. Но если проект небольшой, я допускаю инъекцию сервиса аналитики во ViewModel, лишь бы это не превращалось в спагетти.
Стратегии биндинга: Combine vs замыкания vs @Published
Выбор инструмента зависит от вашего стека и религии.
@Published (SwiftUI): Самый простой и лаконичный вариант. Но будьте осторожны: обновление происходит в
objectWillChange, что иногда приводит к нюансам в жизненном цикле.Combine: Дает мощь операторов (
debounce,filter,combineLatest). Если у вас сложный ввод с валидацией «на лету», Combine незаменим. Но отладка длинных цепочек - это отдельный вид мазохизма.Closures: Олдскульный и самый быстрый вариант. Никаких внешних зависимостей, никакой магии. Если вы пишете библиотеку, это лучший выбор, чтобы не навязывать пользователю Combine или RxSwift.
Я лично предпочитаю Combine для iOS 13+, но стараюсь держать цепочки короткими. Если цепочка больше 5-6 операторов - пора разбивать ее на части или выносить логику в отдельный метод.
Тестирование ViewModel без UIKit/SwiftUI
Если вы не можете протестировать ViewModel без создания экземпляра UIViewController или View - ваша архитектура провалена. Тест должен выглядеть примерно так:
Swift
func test_onLoad_setsLoadingState() {
let mockRepository = MockProductRepository()
let sut = ProductListViewModel(repository: mockRepository)
sut.loadProducts()
if case .loading = sut.state {
// success
} else {
XCTFail("Expected .loading state")
}
}
Я всегда использую Dependency Injection через инициализатор. Это позволяет легко подсовывать моки. Тестировать асинхронщину в Combine чуть сложнее (нужны Expectations), но это всё равно в разы быстрее, чем гонять UI-тесты.
Антипаттерны, которых стоит избегать
Massive ViewModel: Если ваша VM перевалила за 500 строк - режьте её. Выносите логику форматирования в
Formatter, работу с данными - вService, а сложные трансформации - вUse Case(привет, Clean Architecture).Leaky Views: Передача
UIобъектов во ViewModel. Никогда не передавайтеUIImageилиNSAttributedString. ПередавайтеDataили простоString. ViewModel должна жить в мире чистой логики.Shared ViewModels: Использование одной и той же инстанс-модели для разных экранов через Singleton. Это прямой путь к состоянию «кто-то поменял данные на третьем экране, и у меня всё упало». Каждому экрану - своя ViewModel. Если нужно делиться данными - используйте общий Service или Storage.
MVVM - это не догма, а инструмент. Он прекрасно масштабируется, если не пытаться сделать из него «архитектуру всего приложения». В реальности MVVM отлично дружит с координаторами для навигации и сервисами для бизнес-логики.
Главное - помнить: ViewModel отвечает за что показывать, а View - за как это выглядит. Если вы разделите эти понятия в голове, ваш код станет чище, а сон - спокойнее.
Комментарии (5)

Bardakan
31.01.2026 09:03enum State { case idle case loading case loaded([Product]) case error(String) }Вместо россыпи разрозненных свойств я предпочитаю использовать единый
State. Идеальный инструмент для этого -enum.вы видимо не сталкивались с более сложными view model, где нужно данные из одного State case перегонять в другой.
Leaky Views: Передача
UIобъектов во ViewModel. Никогда не передавайтеUIImageилиNSAttributedString. ПередавайтеDataили простоString. ViewModel должна жить в мире чистой логики.не совсем понял. Вы предлагаете запретить передавать UI в модель или чтобы модель не использовала swiftui и uikit в целом?
А еще что вы предлагаете делать, когда у вас NSAttributedString состоит из нескольких частей с разным форматированием, которые нужно еще собрать в одну строку перед тем, как показать?
SHK83 Автор
31.01.2026 09:03Навскидку не придумал для чего мне могло бы понадобиться передавать данные из одного State case в другой. Но поразмыслю над этим, спасибо.
А про запрет UI во ViewModel - да, если у вас есть необходимость импортить UIKit во viewModel, то это порочная практика. Именно об этом и пишу. Этого всегда можно избежать. В случае с атрибутированной строкой можно написать отдельный helper, который помогал бы решить эту проблему и дергать его из ViewModel. Кмк это бы решило проблему чистого ViewModel.
bighorik
Спасибо за статью, как раз вынужден оперативно разбираться с авалонией и мввм, очень доступно разложили. Было бы здорово, если пример о навигации был бы более развернутый
Поставил бы лайк, да карма не позволяет :(
SHK83 Автор
Будет серия статей про mvvm. И про навигацию тоже будет подробная статья. Постараюсь за следующую неделю все опубликовать. Следите. Спасибо за отзыв.
SHK83 Автор
Карму немного исправил :)