SwiftUI MVC? Я так не думаю.

Apple не зря создавала свои основные UI-фреймворки. И UIKit, и SwiftUI в своих концепциях ориентируются на какую-то конкретную архитектуру для фронтенд-проектов и даже на некоторые шаблоны проектирования.

Основное различие между ними заключается в том, что UIKit нацелен на архитектуру MVC, а SwiftUI был разработан для MVVM.

Несмотря на то, что 99% разработчиков считают, что MVC бесполезен в проектах 2022 года, а MVVM гораздо лучше разработан, я должен сказать, что у нас все еще есть некоторые преимущества с MVC, но поскольку цель этой статьи другая, я оставлю это как упражнение для читателя.

UIKit и MVC

Как я уже говорил, Apple создала фреймворк UIKit, полностью думая о модели-представлении-контроллере.

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

  1. Model: Управляет состоянием вашей сцены, сохраняя все логические данные для ее изменения.
  2. Controller: Управляет жизненным циклом экрана (или, возможно, подкомпонента), прослушивая некоторые из его основных событий (загрузка, появление и т. д.) и обновляя представление в соответствии с выходными данными модели. Все контроллеры наследуются от класса UIViewController.
  3. View: Это сырой холст, внутри которого размещены некоторые другие компоненты. Здесь нет никакой логики, и каждое изменение делегируется его ViewController.

Это наша архитектура: у нас есть Model со всей логикой для сцены, которая должна отражаться в нашем View внешнем виде. У нас есть пунктирная линия между нашими View и Controller, потому что, несмотря на то, что наш контроллер имеет свой собственный способ прослушивания представления, представление не имеет (и не может) ссылки на свой ViewController, поскольку это нарушает наши принципы MVC.

Итак, как Controller может слушать наш View, если он не может иметь ссылку на ViewController? Ключевой концепцией этого являются ссылочные типы, которые представляют наши компоненты. Вы должны знать, что наш Controller полностью связан с нашим View. UIViewController имеет ссылку на тип UIView, который загружается из xib (или ViewCode) внутри метода loadView. Представление просто работает как корневой компонент для хранения всех частей пользовательского интерфейса.

Наш View имеет ссылки на свои подкомпоненты, но Controller имеет те же ссылки через свойства IBOutlet (или, может быть, ленивые переменные?) и прослушивает свои события через IBAction события (или методы обработки). Поскольку эти UIViews разделены между View и Controller, контроллер может знать о событиях, не нуждаясь в View, чтобы сообщить об этом.

Просто наблюдение: если вы используете ViewCode и устанавливаете какую-то связь между ViewController и View через делегаты или завершения, прекратите это прямо сейчас, View не может иметь никакого представления о контроллере, отличном от того, что происходит в MVVM.

Классы представлений, являющиеся ссылочными типами, позволяют использовать общую память между двумя слоями, поэтому Controller управляет View, ничего о нем не зная.

SwiftUI и MVVM

Я бы сказал, что одно из основных различий между MVC и MVVM заключается в том, как наша логика и пользовательский интерфейс взаимодействуют друг с другом.

Поскольку наш Контроллер в MVC управляет View так, как если бы производитель заботился о продукте, ViewModel в MVVM действует как схема со всеми поведениями и данными, на которые должно опираться наше представление (теперь UIView + UIViewController).

Представление знает все о нашей ViewModel как эталоне, несмотря на то, что наша ViewModel ничего не знает о представлении.

Проекты UIKit позволяют использовать MVVM с расхождением с первоначальной идеей: наш ViewModel может обновлять View через протокол доступа, но вы можете изменить это, используя общую память, а также NotificationCenters, RxSwift и Combine. Дело в том, что наш View выполняет все свои действия в зависимости от нашего состояния ViewModel, но слой ViewModel об этом не знает.

В SwiftUI на самом деле происходит то, что наша структура View опирается на ObservedObject/StateObject, которые соответствуют нашей модели ViewModel, реализующей протокол ObservableObject, поэтому наше представление повторно отображается при каждом обновлении данных. Таким образом, наше представление может параметризовать все свои данные в свойствах ViewModel, и ему не нужно знать о представлении, поскольку оно обновляется само по себе при повторном рендеринге и просмотре ViewModel.

SwiftUI MVC? я так не думаю

Что действительно сделало MVC возможным в UIKit, как мы обсуждали ранее, так это общая память между UIView и UIViewController , которая зависела от некоторых свойств пользовательского интерфейса, таких как кнопки, табличные представления, подпредставления и т. д. Таким образом, наш контроллер мог реагировать на любые события и обновлять отдельные компоненты с помощью себя, и ни один из них не знает о контроллере, тем самым устанавливая логическую изоляцию. Давайте попробуем то же самое в сцене SwiftUI:

Здесь у нас есть ContentView, соответствующий нашему слою View. Как видите, у нас есть два встроенных свойства, соответствующие TextFields, и два состояния, которые будут заполнять наши метки ниже. Текстовые поля должны храниться нашим контроллером так же, как мы это делали в UIKit.

Теперь у нас есть слой Controller, который имеет свойство типа ContentView в качестве необязательного и создает два текстовых поля, которые нам нужно прослушивать, чтобы обновить наш пользовательский интерфейс. Поскольку типы SwiftUI не зависят от делегатов, мы создаем ContentModel и привязываем его свойства к текстовым полям.

М в MVC ничего не значит. Давайте создадим нашу Модель:

Обратите внимание, что мы используем замыкание наблюдателя didSet каждый раз, когда наша модель изменяется через текстовые поля, и оно должно обновлять наше представление. Спойлер: не будет!

Теперь создайте фабричное перечисление, чтобы создать экземпляр нашего ContentView и обернуть все остальные слои:

Теперь мы создаем экземпляр модели и внедряем его в наш новый класс Controller. После этого мы создаем новый ContentView с теми же значениями textField и присваиваем его значение нашему контроллеру, после чего возвращаем наше представление.

Запускаем наш образец

Давайте попробуем запустить наши результаты:

Как видите, две метки не обновляются, и этому есть простая причина: представления, на которые мы «ссылаемся» внутри нашего контроллера, не являются ссылочными типами, поэтому, когда мы внедряли наше представление, мы фактически передавали копию в Controller.

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

Заключение

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