Привет, Хабр! Меня зовут Кирилл Маканков, я iOS-разработчик в ПСБ.  

Сегодня хочу поговорить про особенности реализации MVVM. Не с практической, а с теоретической стороны. С практической стороны этот архитектурный шаблон давно уже изучен вдоль и поперёк. А вот теоретических исследований, особенно применимых к Swift (и в общем, и к SwiftUI в частности), не так уж и много. Давайте вместе попробуем закрыть этот пробел и обосновать с теоретической точки зрения те или иные вариации реализаций в нашей ежедневной работе. Мобильным разработчикам на ObjC, Java и Kotlin данное исследование и обсуждение тоже будет полезно. Присоединяйтесь!

Если вы уже хорошо знакомы с MVVM, то в конце приведен опрос об используемой лично вами реализации. Давайте вместе определим, какая из реализаций самая популярная.

Почему MVVM?

Но сначала объясню, почему именно MVVM.

Тут всё очень просто! 

Для начала, это самый популярный шаблон проектирования по версии Мобиус от осени 2025 года в российском сообществе мобильных разработчиков. Ниже привожу выдержку результатов их опроса.

Дальше я с помощью ИИ исследовал популярность архитектур по данным из открытых источников. И узнал, что MVVM упоминается в 68% вакансий, связанных с мобильной разработкой. Солидно! 

Этим шаблоном мы и сами активно пользуемся в нашей iOS-разработке, а наши коллеги из Android-разработки тоже начали активно его внедрять.

Решено! – Пристегнитесь! – Поехали исследовать!

Что такое MVVM?

Сначала давайте определимся с терминами. Что же такое MVVM?

Ответ на этот вопрос и прост, и сложен одновременно. Все, наверняка, читали множество различных определений и видели вот такую картинку с Википедии, как на рис. 1?

Рис.1. Самая популярная схема MVVM
Рис.1. Самая популярная схема MVVM

Но что можно понять по этой схеме? Что означают эти квадратики? Это классы? Это объекты? Это целые группы, чем-то связанные? Что означают сплошные стрелочки, а что пунктирная? Что означает цветовая маркировка? По одной лишь схеме разобраться сложно. 
Необходимо читать описание и детально разбираться, что я и предлагаю сделать.

Слоистая архитектура

Из описания шаблона в той же Википедии, мы узнаем, что MVVM – это слоистая или многоуровневая архитектура (ссылка на вики на английском, на русском). Отлично! Значит квадратики на схеме – это не классы и не объекты, это целые слои классов/объектов или даже протоколов (мы увидим это в нескольких примерах далее), логически связанных между собой! Запоминаем, нам это пригодится дальше.

Цветовая индикация, похоже, в этом свете не означает ничего кроме того, что все три слоя не связаны логически друг с другом (Low coupling).

ViewModel

Перейдем сразу к смыслу и, возможно, структуре промежуточного слоя, называемого вью-моделью, ибо на нём будет сфокусировано все наше дальнейшее исследование, т. к. с реализациями уровня View и Model обычно проблем не возникает, там все довольно однозначно. Все нижеперечисленное также взято из описания шаблона проектирования MVVM в Википедии на английском языке, т. к. русский вариант сильно беднее:

Итак, вью-модель – это:

  • В обязательном порядке абстракция! Это объясняется необходимостью развязать слой представления (вью) от слоя бизнес-логики (модели), сделать их независящими друг от друга.

  • Преобразователь данных из формата, в котором они лежат в модели, к формату в котором они отображаются на вьюхе. И возможно, но не обязательно – в обратную сторону.

  • И, наконец, это может быть так называемый binder, связывающий напрямую значения вью-модели со значениями вью и обратно. Ответственность binder’а не обязательна в шаблоне MVVM, но без неё придется писать слишком много одинакового boilerplate-кода. Binder как раз и решает проблему этого одинакового boilerplate-кода. Без binder’а MVVM превращается в MVP (или в MVC с пассивной моделью).

Вот те 3 ответственности, которые должны и могут присутствовать во вью-модели.

При этом, как мы отметили выше, эти 3 ответственности не обязаны сосредотачиваться в одном классе и/или объекте – они могут быть разнесены по разным классам/объектам в пределах одного уровня архитектуры. Далее в примерах мы увидим, что так оно и есть…

Чтобы разобраться правильно ли мы реализуем вью-модель, необходимо разобраться, как можно реализовать каждую из ответственностей.

С биндером и преобразователем данных обычно проблем возникает меньше всего. В рамках этой статьи правильность их реализации мы рассматривать не будем, хоть и косвенно затронем преобразование данных, т. к. показать примеры без него довольно сложно. Вместо этого сконцентрируемся на способах реализации именно абстракции, т. к. это самый сложный вопрос. Если вы хотели бы разобраться в типичных ошибках реализации преобразователя данных и биндера, пишите в комментариях, подготовлю такое же детальное продолжение этой статьи.

Абстракция

Все дальнейшие примеры реализации абстракции я буду показывать на примере SwiftUI-вьюхи с таким телом (само тело дальше приводить не буду, чтобы не нагружать примеры кода):

var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text(viewModel.text)
        }
        .padding()
    }

Установку вью-модели во вью делаю прямо в самой вью для упрощения примеров. В реальном коде так делать не рекомендую, т. к. это создает сильную связанность и невозможность подмены, что противоречит самой сущности шаблона MVVM. В реальном коде связь должна происходить инъекциями за пределами самой вью, иначе абстракции становятся конкретикой! Однако, рассмотрение способов инъекций (фабрики, DI-фреймворки и т. д.) выходит за рамки этой статьи. Если вам они интересны, оставляйте комментарии под статьей, попробую провести и для них такой же детальный анализ.

5 способов реализации абстракции

Абстракцию на Swift (но при желании вы можете провести аналогичный анализ для своего любимого языка) можно реализовать следующими 5 способами (упорядочу их для удобства от простого к сложному):

Способ 1. Протоколы (они же основное средство абстракции) и расширения этими протоколами. В такой реализации вью-модель (целый слой) – это просто протокол, что не позволяет использовать хранимые во вью-модели свойства. Реализация самая простая из всех рассмотренных:

struct ContentView: View {
    @State var viewModel: ViewModel = Model()
    ...
}
// реализация абстракции вью-модели на протоколе!
protocol ViewModel {
    var text: String { get }
}
// реализация конкретной модели
struct Model {
    let myText = "Hello, protocol world!"
}
// связывание вью-модели и модели, заставляем вью-модель преобразовывать данные
extension Model: ViewModel {
    var text: String { myText }
}

Способ 2. Наследование классов. В Swift нет чистых абстрактных классов, поэтому эмуляцию можно сделать двумя способами:

Вариант 2.1. Снова на протоколах, но дефолтная реализация методов приводит к падению приложения при их вызове. Не рекомендуется так делать, т. к. ошибки компилятора выключаются и будут отловлены только при исполнении программы. К тому же количество кода для выполнения той же задачи увеличивается на пустом месте. Сравните с примером из п.1:

struct AbstractProtocolView: View {
    @State var viewModel: AbstractProtocolViewModel = AbstractProtocolModel()
    ...
}
// Протокол заменяет абстрактный класс
protocol AbstractProtocolViewModel {
    var text: String { get }
}
// Дефолтная реализация протокола эмулирует абстрактный метод
extension AbstractProtocolViewModel {
    var text: String { fatalError("Abstract!") }
}
// реализация конкретной модели
struct AbstractProtocolModel {
    let myText = "Hello, abstract protocol world!"
}
// связывание вью-модели и модели, заставляем вью-модель преобразовывать данные
extension AbstractProtocolModel: AbstractProtocolViewModel {
    var text: String { myText }
}

Вариант 2.2. Напрямую создавать классы с методами, приводящими к падению приложения при их вызове. Не рекомендуется так делать, т. к. это тоже сделает невозможным проверки компилятором, а ошибки будут отловлены только в момент исполнения:

struct ClassView: View {
    @State var viewModel: AbstractViewModel = ClassViewModel(model:ClassModel())
    ...
}
// Эмуляция абстрактного класса
class AbstractViewModel {
    var text: String { fatalError("Abstract!") }
}
// реализация абстракции вью-модели на абстрактном классе!
class ClassViewModel: AbstractViewModel {
    override var text: String { model.myText }
    var model: ClassModel  
    init(model: ClassModel) {
        self.model = model
    }
}
// реализация конкретной модели
struct ClassModel {
    let myText = "Hello, class world!"
}

Способ 3. Дженерики (Generics). Ограничения в них могут быть только протоколами или классами. Поэтому если вам нужно работать с моделями-структурами, то придется дополнительно абстрагировать и их, закрывая протоколами (количество абстракций при этом увеличивается):

struct GenericView: View {
    @State var viewModel: GenericViewModel = GenericViewModel(model: GenericModel())
    ...
}
// реализация абстракции вью-модели на дженериках!
class GenericViewModel<Model: GenericModel> {
    var text: String { model.myText }
    var model: Model
    init(model: Model) {
        self.model = model
    }
}
// реализация конкретной модели, ограничения в дженериках не могут быть структурами, только классами!
class GenericModel {
    let myText = "Hello, generic world!"
}

Способ 4. Ассоциированные типы (associatedtype). Как и дженерики, обязаны задавать ограничения протоколами или классами, поэтому тоже не могут использовать структуры-модели. Код получается еще более монструозным для такой простой задачи:

struct AssociatedTypeView: View {
    @State var viewModel: any AssociatedViewModel = MyAssociatedViewModel(model: AssociatedModel())
    ...
}
// Абстракция на протоколе с ассоциированным типом
protocol AssociatedViewModel {
    associatedtype Model: AssociatedModel
    var model: Model { get }
    var text: String { get }
}
// Связываем вью-модель и модель, заставляя преобразовывать данные
extension AssociatedViewModel {
    var text: String { model.myText }
}
// реализация конкретной вью-модели
class MyAssociatedViewModel: AssociatedViewModel {
    typealias Model = AssociatedModel
    var model: AssociatedModel
    var text: String { model.myText } 
    init(model: AssociatedModel) {
        self.model = model
    }
}
// реализация конкретной модели
class AssociatedModel {
    let myText = "Hello, associated type world!"
}

Способ 5. Обертки, стирающие тип (Стирание типа, Type erasure). Малоизвестный способ создания абстракции. Но это самая часто встречающаяся реализация вью-модели. Просто посмотрите на этот код, сравните со своим и проголосуйте в опросе, похож ли этот код на ваш):

struct TypeErasureView: View {
    @State var viewModel: TypeErasureViewModel = TypeErasureViewModel(model: TypeErasureModel())
    ...
}
// Вью-модель через стирание типа
struct TypeErasureViewModel {
    let model: MyTextContainable  
    init(model: MyTextContainable) {
        self.model = model
    }
}
// связывание вью-модели и модели
extension TypeErasureViewModel {
    var text: String { model.myText }
}
// сокрытие доступа к модели за протоколом
protocol MyTextContainable {
    var myText: String { get }
}
// реализация конкретной модели
struct TypeErasureModel {
    let myText = "Hello, type erasure world!"
}
// прячем модель за абстракцию
extension TypeErasureModel: MyTextContainable { }

Знаете ли вы ещё какие-нибудь варианты реализации абстракции на Swift? — Пишите в комментариях.

А сейчас сведем все рассмотренные способы в таблицу, отметив их сильные и слабые стороны:

Таб. 1. Способы реализации абстракции на языке Swift
Таб. 1. Способы реализации абстракции на языке Swift

Красным в таблице отмечены «западающие» свойства. Неудивительно, что при этом самыми распространённым оказывается способ создания абстракции через обертку, стирающую типы. Но самым простым оказывается малоизвестный способ на протоколах с дефолтными реализациями. Пройдите, пожалуйста, опрос в конце статьи, чтобы подтвердить или опровергнуть моё предположение.

Отмечу также, что все из этих способов создания абстракций можно преобразовывать друг в друга довольно легко. Например, эмуляцию абстрактных классов на протоколах (вариант 2.1) можно получить из реализации абстракции на протоколах с дефолтной реализацией (вариант 1, прошу прощения читателя за очень похожие названия, но отличия в реализациях действительно незначительны!), просто добавив к реализации протокола дефолтную реализацию метода, эмулирующего падение приложения при его вызове. Соответственно, чтобы произвести обратное преобразование из варианта 2.1 в вариант 1, необходимо удалить эмуляцию абстракции из дефолтной реализации протокола. 

Далее из эмуляции абстрактных классов на протоколах (вариант 2.1) уже можно получить прямую эмуляцию абстрактных классов (вариант 2.2), просто преобразовав протокол с дефолтной реализацией в класс с абстрактным методом и создав его конкретный потомок. 

Зная эту информацию, вы теперь можете реализовывать вью-модель с помощью любого из указанных способов и при необходимости менять её с одного способа на другой, учитывая преимущества каждого способа!

А теперь следующий вопрос: «Существуют ли еще какие-то способы создания абстракций?»

Абстракция с помощью шаблонов проектирования

Оказывается, существуют. Один из способов создания абстракции с помощью поведенческого шаблона «Посредник» («Mediator») из книги Банды Четырех указан прямо в описании шаблона MVVM в Википедии: "The viewmodel may implement a mediator pattern".

Mediator + Strategy + Observer

Внимательно ознакомимся с шаблоном и убедимся, что главная его цель – это как раз сделать коммуникацию между несколькими объектами, которые не должны знать друг о друге, что и нужно вью-модели в шаблоне MVVM. При этом связь должна быть инкапсулирована и иметь возможность быть легко замененной на любую другую независимо от самих коммуницирующих объектов! Теоретически с использованием этого шаблона мы можем менять реализацию медиатора на любую другую, ни вью, ни модель при этом не должны измениться ни на символ кода.

Однако в чистом виде, описываемом в шаблоне, инкапсулируется связь о взаимодействии однотипных объектов. В более сложном случае, когда как в MVVM нам нужно инкапсулировать взаимодействие между разнотипной вью и разнотипной моделью, к шаблону «Посредник» приходится добавлять шаблон «Стратегия» («Strategy») (и даже комбинацию шаблонов «Команда»(«Command») и «Цепочка обязанностей»(«Chain of Responsibility»)) и «Observer» («Наблюдатель») из той же книги Банды Четырех, которые как раз и абстрагируют взаимодействие со вью («Стратегия», «Команда» и «Цепочка обязанностей») и с моделью («Наблюдатель»). 

Помните табличные делегаты? Вот это как раз реализация шаблона «Стратегия», позволяющая одну и ту же табличку (UITableView) использовать с различными данными. А помните механизм target-action? Это пример реализации «Команды» и «Цепочка обязанностей» — в простейшем случае у разработчиков не вызывает проблем с переиспользованием этих механизмов на простейших экранах. 

Примером шаблона «Наблюдатель» является стек CoreData, уведомляющий и обновляющий свои NSManagedObject’ы. При этом NSFetchedResultsController — это пример инкапсуляции конкретного взаимодействия любой таблички с CoreData — нам не нужно каждый раз писать новые взаимодействия, если мы хотим отображать данные из базы в табличном виде, но при желании мы легко можем реализовать свой новый контроллер. Но вот такие сложные взаимодействия у разработчиков уже вызывают проблемы, хоть сам NSFetchedResultsController и довольно прост.

Скомбинировав указанные 3 шаблона вместе, мы получим схему MVC с пассивной моделью с рис. 7.2 из документации Apple (см. рис. 2).

Рис. 2. MVC с пассивной моделью по версии Apple
Рис. 2. MVC с пассивной моделью по версии Apple

Т. е. MVVM без binder’а может вырождаться не только в MVP, но и в MVC в зависимости от направления зависимости от вью к промежуточному слою, либо обратно (это, кажется, единственно отличие MVP от MVC с пассивной моделью или вы можете указать какие-то еще?).

Если теперь добавить в эту схему binder и перенаправить уведомления от модели с вью-модели (контроллера) напрямую во вью, то мы получим схему MVC с активной моделью, как на рис. 7.1 из той же документации Apple (см. рис. 3).

Рис. 3. MVC с активной моделью (Она же MVVM с binder’ом)
Рис. 3. MVC с активной моделью (Она же MVVM с binder’ом)

Реализуете ли вы MVVM по схеме MVC с активной/пассивной моделью? Если нет, то почему? 

Есть ли еще какие-то способы реализации абстракции, чтобы на ее основе реализовать вью-модель?

«Adapter» («Адаптер»)

Конечно! Аналогом «Посредника» для реализации абстракции является шаблон «Адаптер»(«Adapter»). В книге Банды Четырех он описан в двух реализациях: как адаптер класса (с множественным наследованием) и адаптер объекта. Если мы попробуем глубже разобраться с обеими реализациями, мы с удивлением обнаружим, что адаптер классов с множественным наследованием – наши уже упомянутые реализации абстракции на Swift под вариантами 1 и 2.1, а адаптер объектов – это реализация варианта 5.

Внесем варианты на основе шаблонов проектирования в нашу таблицу. Расширенная версия указана в таблице 2.

Таб. 2. Способы реализации абстракции на языке Swift (расширенная версия)
Таб. 2. Способы реализации абстракции на языке Swift (расширенная версия)

Удобство тестирования

Что касается тестирования всех указанных вариантов реализации MVVM, это не должно составлять труда ни в одном из перечисленных вариантов, т. к.в основе всего шаблона MVVM лежит принцип абстракции и снижения связности между вью и моделью. Если в каком-то из вариантов у вас встречаются сложности, пишите в комментариях, попробуем разобраться вместе! В частности, трудности обычно встречаются с вариантом обертки, стирающей типы (он же «адаптер объекта»). Более подробно это будет рассмотренно ниже в разделе ошибочных реализаций.

Ошибочные реализации

Выше мы детально рассмотрели и попытались обосновать целых 6 различных способов реализации шаблона MVVM на языке Swift. Самое время переходить к рассмотрению ошибочных способов.

И начнем мы с самого очевидного!

Отсутствие абстракции во вью-модели

Ну, тут совсем все понятно. Код самый простой из всех рассмотренных. Однако, если отсутствует абстракция, разрушается весь смысл шаблона MVVM, то это уже не MVVM. В таком случае невозможно разрабатывать представление и бизнес-логику независимо друг от друга. Любое изменение, предложенное дизайнерами, непременно будет требовать изменений бизнес-логики. Любое изменение, предложенное владельцем продукта или пользователями, непременно будет требовать изменений в дизайне. Сущий ад для разработчика!

Если вам по каким-то причинам все же потребовалось убрать абстракцию из вью-модели, то лучше пересмотрите всю архитектуру конкретного экрана - очень вероятно, что MVVM к нему просто не применим.

Пример кода:

struct SpecificView: View {
    @State var viewModel: SpecificViewModel = SpecificViewModel(model: SpecificModel())
    ...
}
// конкретная вью-модель с конкретной моделью,
// ошибка: отсутствие абстракции!
struct SpecificViewModel {
    var model: SpecificModel
    var text: String { model.myText }
}
// реализация модели
struct SpecificModel {
    let myText = "Hello, specific world!"
}

Абстракция на абстракции и абстракцией погоняет

Противоположный пример. Для правильной работы шаблона MVVM (чтобы отделить представление от логики) достаточно одной абстракции на уровне вью-модели. Но частенько у разработчиков, не пытавшихся разобраться в теории, возникают сложности с тестированием. Они пытаются тестировать вью-модель отдельно от модели на уровне модульных (юнит-) тестов. Но не понимая, как работает уменьшение связности конкретного выбранного способа реализации абстрактности, добавляют дополнительные способы, которые и используют для подмены реальных моделей на моки.

В результате такого ошибочного решения появляется примерно такой код:

struct MultiAbstractionView: View {
    @State var viewModel: MultiAbstractionViewModelProtocol = MultiAbstractionViewModel(model: MultiAbstractionModel()) 
    ...
}
// протокол вью-модели
protocol MultiAbstractionViewModelProtocol {
    var text: String { get }
}
// АБСТРАКЦИЯ вью-модели по схеме обертки, стирающей типы
struct MultiAbstractionViewModel {
    var model: MultiAbstractionModelProtocol
    init(model: MultiAbstractionModelProtocol) {
        self.model = model
    }
}
// добавление еще одной АБСТРАКЦИИ через протокол к вью-модели!
// и связывание вью-модели с моделью
extension MultiAbstractionViewModel: MultiAbstractionViewModelProtocol {
    var text: String { model.myText }
}
// протокол модели
protocol MultiAbstractionModelProtocol {
    var myText: String { get }
}
// сама модель
struct MultiAbstractionModel {
    var myText = "Hello, crazy multi abstraction world!"
}
// добавление АБСТРАКЦИИ уже к модели, по сути просто дублирование интерфейса модели в интерфейсе протокола, чтобы отделить вью-модель от модели.
extension MultiAbstractionModel: MultiAbstractionModelProtocol { }

Просто безумное количество кода для такого простого примера. Его даже больше, чем в крайне сложных реализациях на дженериках или с ассоциативным типом. Возможно, что больше только в реализации MVVM с медиатором и то только из-за того, что в неё добавлены шаблоны «Стратегия» и «Наблюдатель».

Если мы внимательно изучим этот код, то заметим, что он содержит сразу 3!!! абстракции: 

  • И вью-модель скрывается за протоколом;

  • И модель скрывается за протоколом;

  • Еще и вью-модель реализуется по схеме обертки, стирающей типы.

Встречаются также реализации с двумя любыми реализациями абстракции из указанных трех.

В любом случае, это избыточно там, где хватит всего одной абстракции по правильным образцам, представленным выше. Пожалуйста, не реализовывайте MVVM так. Очевидно, что из трёх абстракций достаточно оставить лишь одну в нужном месте. 

Приглашаю обсудить в комментариях, в каких случаях такая реализация имеет право на жизнь.

Вывод

В рамках детального анализа архитектуры MVVM удалось определить как минимум 6 различных способов реализации.

Самым популярным является способ с оберткой, стирающей типы модели. Он же «адаптер объекта».

Самым простым, но непопулярным является реализация на протоколах. Она же «адаптер класса». Видимо, непопулярность обусловлена тем, что вью-модель в такой реализации не может иметь хранимых свойств, что вызывает определенные сложности, хоть и ошибочно с точки зрения архитектуры, т. к. данные все же должны храниться в свойствах на уровне модели. Тем не менее мы настоятельно рекомендуем этот вариант к использованию.

Самым презираемым в сообществе, но при этом самым гибким, является способ реализации на «Посреднике» («Mediator»). Это, видимо, вызвано тем, что этот способ содержит в себе сразу как минимум 2 связанных шаблона («Стратегию» и «Наблюдателя») и очень сложен в правильной реализации. Также сложности может вызывать то, что в зависимости от деталей реализации этот шаблон может превращаться в различные архитектурные схемы (MVP, MVC с активной или пассивной моделью).

Остальные способы реализации совсем редкие и должны использоваться исключительно в случаях, когда раскрываются их сильные стороны.

Мы так же рассмотрели ошибочные реализации, чтобы каждый мог скорректировать свой подход. 

Спасибо всем, кому было интересно, кто дочитал эту простыню до конца. Теперь предлагаю проголосовать, чтобы определить, а как же реализует MVVM сообщество и обсудить детали в комментариях.

Комментарии (24)


  1. laptev_am
    17.03.2026 07:54

    Вообще не понимаю, зачем такое сложности? Зачем делать слой абстракции, если View наразрывно связана со своей/своими ViewModel? Основной концепт подхода в том, что при изменении данных/стейта внутри ViewModel автоматически обновляется View. Делать абстракцию не нужно, потому что View + ViewModel - это и есть слой presentation (на вашей же картинке в статье это указано), который отвязан от model.


    1. house2008
      17.03.2026 07:54

      Я тоже не понял зачем делать абстракцию для вью модели, учитывая что в примерах vm вообще создается внутри вью, вот если бы она передавалась из вне то тогда абстракция была бы полезна чтобы через моки закрыть весь компонент. То есть по самому паттерну верно делать интерфейсы для vm, но на практике это очень избыточно и даже вредно. Абстракцию нужно делать для инжектируемых компонентов во вью модель чтобы мокать в тестах.

      простая вм
      @Observable
      final class ViewModel {
      
          let apiService: ApiServiceInterface
      
          init(apiService: ApiServiceInterface) {
              self.apiService = apiService
          }
      
          var list: [Element] = []
          var selected: Element?
      
          func fetchList() async -> [Element] {
              list = await apiService.list()
          }
      }
      
      struct View {
      
          @State private var viewModel = ViewModel(apiService: .shared)
          
          var body: some View {
              List(viewModel.list, selection: $viewModel.selected) {
                  Text("\($0)")
              }
              .task {
                  await viewModel.fetchList()
              }
          }
      }
      
      @Test func test() async {
          class Service: ApiServiceInterface {
              func fetchList() async -> [Element] {
                  [1, 2, 3]
              }
          }
          let mock = Service()
          let viewModel = ViewModel(apiService: mock)
          #expect(viewModel.list.isEmpty)
          await viewModel.fetchList()
          #expect(viewModel.list == [1, 2, 3])
      }


      1. ws233 Автор
        17.03.2026 07:54

        В вашей реплике сразу несколько вопросов и неточностей. По очереди и отвечу.

        1. Именно для того, чтоб не перегружать примеры, в начале статьи и делается сноска, что инжектирование специально остается за скобками. Иначе она спрячет от Вас суть. Но Вы можете самостоятельно убедиться, что ни один из способов не изменится, если добавить в него инжектирование. Они (в смысле инжектирование и способы) независимы друг от друга. Освоив любой из способов (или даже несколько, или даже все) Вы легко сможете использовать их с инжектированием. Отдельно в статье отмечено (ближе к концу), что ни с одним из перечисленных способов не должно быть проблем с тестированием именно потому, что сама реализация уже содержит абстракцию, а значит, может легко позволить подменить вью для модели или модель для вью.

        2. Не во всех перечисленных примерах вьмодели скрыты протоколами. Абстракция – это не только протоколы. Статья пытается это показать. В частности, Ваш пример – это вариант 5, обертка, стирающая тип. Не самый простой вариант (есть проще), но самый популярный, как и отмечено в статье. И при этом мало известный :) Не удивительно, что он первый же в комментариях и проявился. Вам осталось проголосовать в опросе, не стесняйтесь, пожалуйста! Давайте создадим статистику!

        3. Что касается "по самому паттерну верно делать интерфейсы для vm, но на практике это очень избыточно и даже вредно." Так статья именно это и пытается показать! Еще раз: далеко не во всех перечисленных примерах вьюмодель спрятана за протоколом. Более того, я тоже считаю, что иногда (если это не варианты 1, 2.1, где вьюмодель – это и есть протокол и ничего более, или, может быть, еще какие-то) это вредно! К тому же, в самом шаблоне нет ни слова про протокол! Там лишь написано, что вьюмодель должна быть абстракцией! А уж какой способ реализации абстракции Вы выберете - неважно, это все равно будет MVVM. Проблема будет только если а) Вы вообще забудете абстракцию; б) нагородите их слишком много.

        Я смог ответить на Ваши вопросы?


        1. house2008
          17.03.2026 07:54

          Ваш пример – это вариант 5, обертка, стирающая тип.

          У вас 5 это просто проксирование, в google можно посмотреть что такое swift type erasure.

          Абстракция – это не только протоколы.

          Спасибо за уточнение, у меня автоматически в голове включились абстрактные классы и интерфейсы (писал много на Java)

          В целом большое спасибо за развернутый ответ. Возможно я где-то не так понял.


          1. ws233 Автор
            17.03.2026 07:54

            Просто для уточнения. А у Вас чем от "простого проксирования" отличается? Тем, что есть преобразование данных? Ну, я опять же в начале статьи указал, что оставим это за скобками, чтобы не скрыть суть. Но в некоторых примерах в комментах подписано, что собственно вот там и можно осуществлять преобразование, а где-то оно даже есть (из свойства с одним названием, в свойство с другим, но того же типа).

            Я, в принципе, много про стирание типов разбирался прежде, чем это опубликовать. Кажется, это в чистом виде оно. Но поправьте меня, почему Вы считает, что это не оно.

            Вам спасибо, за хороший вопрос и детальный пример. Это мне и нужно было, чтобы люди делились тем, как делают они. Хочется разобраться, какой же вариант самый популярный, кто пользуется непопулярными, в каким случаях, есть ли еще какие-то способы...


            1. house2008
              17.03.2026 07:54

              Просто для уточнения. А у Вас чем от "простого проксирования" отличается?

              А я ничего не утверждал) Просто 5 это не очищение типов метод.

              что это не оно

              AnyHashable например что первое в голову приходит.


              1. ws233 Автор
                17.03.2026 07:54

                Спасибо, понятно.

                Оба случая — обертка, стирающая типы.

                В вашем случае обертка — это протокол. В моем — класс. Но в обоих случаях их цель, скрыть детали реализации и интерфейс от потребителя.

                Спросил Гугл, он именно так и сказал:

                «Manual Wrapping: Traditionally, a wrapper struct or class (e.g., AnyMyProtocol) is created to wrap the concrete type and forward methods.»

                Как бы Вы этот способ назвали, если им постоянно пользуетесь и не считаете это оберткой, стирающей типы?

                Ну, кроме «адаптера объекта, т.к.это тоже оно самое в чистом виде.


                1. house2008
                  17.03.2026 07:54

                  Очищение типов значит очищающий тип невозможно или сложно вытащить в рантайме. В вашем примере через рефлекию тип `model` можно вытащить:

                  struct MultiAbstractionViewModel {
                      var model: MultiAbstractionModelProtocol
                      init(model: MultiAbstractionModelProtocol) {
                          self.model = model
                      }
                  }

                  Взял первый попавшийся пример из гугла с настоящим очищением

                  Скрытый текст
                  /// Cell `Interface`
                  protocol CellModel {
                      /// PAT Placeholder for unknown Concrete Type `Model`
                      associatedtype Cell: UITableViewCell
                      /// Recieves a parameter of Concrete Type `Model`
                      func tableViewCell(_ tableView: UITableView) -> Cell
                  }
                  
                  /// Wrapper `AnyCell` erased the Type requirement
                  struct AnyCell {
                      private let _tableViewCell: (_ tableView: UITableView) -> UITableViewCell
                  
                      init<Model: CellModel>(_ model: Model)  {
                          self._tableViewCell = model.tableViewCell
                      }
                  
                      /// Conforming to `AnyCell` protocol
                      func tableViewCell(_ tableView: UITableView) -> UITableViewCell {
                          return _tableViewCell(tableView)
                      }
                  }
                  
                  /// `Concrete Type` of `CellModel`
                  struct ImageCellModel: CellModel {
                      internal let name: String
                  
                      init(_ name: String) {
                          self.name = name
                      }
                  
                      func tableViewCell(_ tableView: UITableView) -> ImageTableViewCell {
                          guard let cell = tableView.dequeueReusableCell(withIdentifier: Cell.id)
                          as? Cell else { fatalError() }
                          cell.titleLabel.text = self.name.capitalized
                          cell.thumbImageView.image = UIImage(named: self.name)
                          return cell
                      }
                  }
                  
                  /// `Concrete Type` of `CellModel`
                  struct TextCellModel: CellModel {
                      internal let quote: String
                      private let author: String
                  
                      init(_ quote: String, author: String) {
                          self.quote = quote
                          self.author = author
                      }
                  
                      func tableViewCell(_ tableView: UITableView) -> TextTableViewCell {
                          guard let cell = tableView.dequeueReusableCell(withIdentifier: Cell.id)
                          as? Cell else { fatalError() }
                          cell.titleLabel?.text = self.quote
                          cell.subtitleLabel?.text = self.author
                          return cell
                      }
                  }

                  И использовать как

                  let imageCell = AnyCell(ImageCellModel("cyclamen"))  
                  let quoteCell = AnyCell(TextCellModel("Hello World.", author: "-"))  

                  После компиляции underlying тип для `imageCell` и `quoteCell` полностью потерялся (очистился) и в райтайме нет возможности узнать кто лежит внутри `AnyCell`.

                  Я детали могу упускать, потому что давно не сталвикался с этим, но главная идея в том, что невозможно восстановить типы потому что они очистились компилятором.


                  1. ws233 Автор
                    17.03.2026 07:54

                    Пусть будет так, спорить не собираюсь. Пусть для Вас и для остальных, кто не согласен, что это стирание типов, это будет способ, называемый адаптер объектов.

                    На мой взгляд Ваш последний демонстрируемый пример ближе к ассоциированным типам из статьи.

                    Отмечу также, что в этом примере не хранится модель, а сохраняются данные модели (замыкание, а через него имя, автора, цитату). Конечно, в этом случае невозможно определить тип модели, просто потому, что Вы её не сохранили. Это же можно сделать и в моих примерах. В обоих – и в стирающей типы обертке и в ассоциированных типах.

                    Вообще последний пример выглядит сильно крипово.

                    Использоваться это будет так, поправьте меня:

                    imageCell.tableViewCell(tableView)

                    Т.е.у вьюмодели (которая почему-то называется ячейкой) мы запрашиваем ячейку (реальную вьюху у вьюмодели) и передаем ей параметром таблицу. Все перевернуто с ног на голову.

                    При этом вьюмодель содержит замыкания.

                    Уважаемому @laptev_amниже я отмечал, что как раз замыкания использовать для автоматического обновления я не рекомендую. Как раз вот в том числе и из-за вот таких вот "перевертышей".

                    Закончу простым вопросом. Вам, правда, Ваш последний пример кода кажется проще любого из приведенных в статье?


                    1. house2008
                      17.03.2026 07:54

                      Вам, правда, Ваш последний пример кода кажется проще любого из приведенных в статье?

                      Да причем тут код) Я говорю что вы не верно используете терминологию, у вас нет ни в одном примере type erasure.

                      Вообще последний пример выглядит сильно крипово.

                      Всё верно, но это был единственный способ до Swift 5.7 чтобы дженеризированные протоколы использовать как типы переменных

                      protocol A {
                          associatedtype B
                      }
                      
                      class C: A {
                          typealias B = Int
                      }
                      
                      // Так нельля делать было до Swift 5.7
                      // Поэтому применяли технику отчистки типов 
                      // через конвертацию в Any..
                      let a: A = C()
                      
                      // Начиная Swift 5.7+ это упросили, 
                      // потому что все устали писать эти перевертыши
                      let a: any A = C()

                      Сейчас мне даже Claude Code пример написал пример очистки типов как тот самый из криповых.

                      называемый адаптер объектов

                      Да, супер, адаптер, проксер, полностью согласен.

                      Спасибо большое за обсуждение, давно так глубоко не копал.


                      1. ws233 Автор
                        17.03.2026 07:54

                        И Вам спасибо. Еще поизучаю type erasure, попробую понять Вашу мысль.


    1. ws233 Автор
      17.03.2026 07:54

      Вам тоже отвечу по частям, постараюсь максимально кратко.

      1. О сложностях какого способа Вы говорите? Способ 1 очень даже простой. Там кроме протокола вообще больше ничего нет. На мой взгляд даже проще, чем тот, что описали Вы. Сложно судить по тексту без кода, что Вы описываете, но кажется, что Вы говорите как раз о том, что привел уважаемый @house2008. Если так, то это в чистом виде способ 5 – обертка, стирающая типы. В этом способе появляется 2 новых сущности (а не одна) – протокол и класс. А для класса еще и объекты надо создавать и потом управлять их жизненным циклом. В первом случае это не нужно совсем.

      2. "Основной концепт подхода в том, что при изменении данных/стейта внутри ViewModel автоматически обновляется View." Не стану с этим спорить, но не зря я ссылку дал на Википедию же. Она утверждает, что это не главное и вообще опциональное. Это лишь третья ответственность из трех возможных, называемая биндинг. Автоматического обновления можно и без биндинга добиться, например, шаблоном "Наблюдатель"/"Observer". Или даже простым делегированием или замыканиями (в терминологии андроида, кажется, это колбеки), но я крайне не рекомендую делать ни первое, ни второе – других проблем огребете.
        А вот важна во вьюмодели как раз абстракция, и Вы её добиваетесь оберткой, стирающей типы. Сравните внимательно.

      Я смог ответить на Ваши вопросы?

      ПС: не стесняйтесь, отмечайте тогда вариант 5 в опросе. Это он!


      1. laptev_am
        17.03.2026 07:54

        Нет, на мои вопросы вы не ответили. Мой основной вопрос: зачем скрывать ViewModel за абстракциями?

        1. И я, и @house2008 пишем примерно о том, что на ViewModel абстракции не надо делать, протоколы тут излишни. Нигде в коде @house2008 я не вижу стирания типов. Фреймворк хочет конкретный класс ViewModel? - окей, дайте ему конктретный класс ViewModel. В чем проблема то? Нужно тестировать? - окей, подменяйте в тестах источники данных, это будет чище и прозрачнее.

        2. Вот я цитирую Википедию в разделе Rationale:

        MVVM was designed to remove virtually all GUI code ("code-behind") from the view layer, by using data binding functions in WPF (Windows Presentation Foundation) to better facilitate the separation of view layer development from the rest of the pattern.

        Data binding - это и есть основной механизм. Это не опциональное, это и есть фундамент MVVM. Data binding - это и есть реализация паттерна "Наблюдатель", просто зашитая внутри фреймворка. Не надо предлагать вручную дублировать функционал.

        Кстати, в Android разработке я могу на коллбеках построить механизм, и мне за это ничего не будет, потому что есть Garbage Collector. А вот в iOS такую схему уже так просто не реализовать, потому как циклические ссылки и всё такое. Возможно, что ради избегания циклических ссылок MVVM и становится все более популярным для iOS.


        1. ws233 Автор
          17.03.2026 07:54

          Так а где Вы в моей статье увидели "скрытие ViewModel за абстракциями"?

          Способ 1 и 2.1 - это тупо протоколы и ничего более. Там вьюмодель сама является протоколом, а не скрывается им.

          Остальные способы не содержат "скрытие ViewModel за абстракциями". Или Вы о каких-то непонятных мне абстракциях?

          Обертка, стирающая тип, сама тоже абстракцией не закрывается. Смотрите пример. Она и есть абстракция. Более того, я и Вам повторю, как уже написал выше это @house2008: статья как раз и показывает неправильный пример, где вьюмодель содержит сразу 3 абстракции! Отдельно отмечается, что так делать не нужно.

          :)


  1. AlexViolin
    17.03.2026 07:54

    Немного конкретизирую своё виденье логической структуры шаблона MVVM.

    View - это визуальный интерфейс приложения, который является фасадом слоя presentation layer и через который приложение взаимодействует с внешним пользователем.

    View Model - реализует модель данных (presentation model) слоя presentation layer и внутреннюю логику слоя presentation layer.

    Binder реализует двустороннее связывание данных между View и presentation model.

    Model - это уже модель данных нижележащего слоя logic layer.

    View Model содержит функционал (обычно он называется маппером), который наполняет Model используя данные из presentation model или наполняет presentation model используя данные из Model.

    Прошу автора статьи и авторов комментариев поделиться, как они видят логическую структуру шаблона MVVM.


    1. ws233 Автор
      17.03.2026 07:54

      Добрый день. Спасибо за вопрос.

      Автор видит как-то так. Уж простите. :)

      View не является "фасадом" в значении шаблона Банды Четырех. Это вполне себе цельная ответственность (и соответственно логика, бывает ответственность без логики?) – поговорить с пользователем: выслушать его, отобразить ему ответ. Неважно в каком виде его выслушать и в каком вернуть ответ: консоль разработчика – это тоже View. Шлем VR – это тоже View. и т.д. Соответственно, у View тоже есть своя логика (но она ограничивается пониманием пользователя и показом ему ответов). Пример, нажатие на кнопку её подсвечивает, отпускание нажатия подсветку убирает. У Apple, как минимум, у кнопки для этой логики нет отдельной вьюмодели. Вся эта логика размещена прямо во View и это кажется логичным, нет?

      ViewModel -- вот вики и картинка из неё (оригинальная) говорят, что "реализует модель данных (presentation model) слоя presentation layer и внутреннюю логику слоя presentation layer" находится где-то между View и ViewModel. Я не могу это однозначно трактовать... Оно размазано между ними двумя? Оно относится к стрелочке биндинга? Оно размазано между двумя слоями и еще и стрелочкой биндинга? Мне сложно сказать по одной лишь картине... Поэтому я учитываю коммент про View выше, который является самостоятельной ответственностью со своей логикой презентации, а также описание шаблона в Вики, которая говорит, что ответственностей у вью-модели всего 3, и среди них нет логики презентации. Еще могу сказать, что логика презентации должна быть переиспользуемой в пределах устройства, но между разными приложениями. Т.е.я могу взять кнопку, о которой говорилось выше, вставить в другое приложение и она точно так же при нажатии начнет подсвечиваться, а при отпускании подсветка будет сниматься... Если предположить, что эта логика будет не в кнопке, а во вью-модели, то получается, что из приложения в приложение мы должны переносить не только класс самой кнопки, но и класс её вьюмодели. Но!..

      Тут мы переходим к тому, что Вы сказали (и Вики, и автор с вами согласны), что вьюмодель может содержать мэппер (преобразователь данных в дословном переводе). Так вот он жестко связан с тем, что он переводит (и с моделью, и с вью). Значит, что при таком переносе, я не смогу какую-то другую модель подключить к этой комбинации вью+вьюмодель. Только ту, которая ждет данные в нужном формате. Значит, вся концепция переиспользования вью убивается на корню. А MVVM должна, вроде как, её сохранять...

      Отсюда следует, что логика презентации должна быть во вью, иначе вся концепция разваливается.

      А картинка из Вики... Ну, я не зря в начале статьи написал, что картинка и очень проста, и одновременно безумно сложна. Так оно и есть при более глубоком рассмотрении.

      Что скажете по этому поводу?


      1. AlexViolin
        17.03.2026 07:54

        Насчёт логики. Конечно View содержит определённую логику. Например перевести чекбокс из одного состояния в другое или поменять надпись на форме в зависимости от выбранного в комбобоксе значения. Для этого ViewModel не нужен. Но есть логика используемая глобально во всём слое presentation layer. Самый типовой пример такой логики - валидаторы данных, вводимых на визуальной форме. Например при вводе в текстовое поле должен быть формат данных типа email. Или валидатор сразу отслеживающий данные в двух полях дат - чтобы дата в одном поле всегда была больше даты в другом поле на форме. Валидатор может использоваться в любой визуальной форме приложения. Такой функционал будет во ViewModel.

        Что касается термина "фасад", то это не в значении шаблона Банды Четырех. Наверное зря я его использовал. Под фасадом здесь понимается тот функционал, который обращён непосредственно к внешнему пользователю. В данном случае это View.

        Presentation model - это модель данных всего слоя. Используется и во View и во ViewModel.