0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>В одной из предыдущих статей мы<a>начали разговор о том</a>, как адаптировать уже существующее бизнес-приложение под SwiftUI, а также<a>рассмотрели работу</a>с готовыми библиотеками под UIKit. Продолжим разбирать тонкости SwiftUI и поговорим об особенностях<strong>архитектуры</strong>и о том, как перенести и встроить в приложение на SwiftUI существующую бизнес-логику..</p>
1
<p>В одной из предыдущих статей мы<a>начали разговор о том</a>, как адаптировать уже существующее бизнес-приложение под SwiftUI, а также<a>рассмотрели работу</a>с готовыми библиотеками под UIKit. Продолжим разбирать тонкости SwiftUI и поговорим об особенностях<strong>архитектуры</strong>и о том, как перенести и встроить в приложение на SwiftUI существующую бизнес-логику..</p>
2
<p>Стандартный поток данных в SwiftUI построен на взаимодействии View и некой модели, содержащей свойства и переменные состояния, или самой являющейся такой переменной состояния. Поэтому логично, что рекомендуемым архитектурным партерном для приложений на SwiftUI является<strong>MVVM</strong>. Apple предлагает использовать его совместно с фреймворком<strong>Combine</strong>, который представляет декларативный Api SwiftUI для обработки значений во времени. ViewModel реализует протокол ObservableObject и подключается как ObservedObject к конкретному View.</p>
2
<p>Стандартный поток данных в SwiftUI построен на взаимодействии View и некой модели, содержащей свойства и переменные состояния, или самой являющейся такой переменной состояния. Поэтому логично, что рекомендуемым архитектурным партерном для приложений на SwiftUI является<strong>MVVM</strong>. Apple предлагает использовать его совместно с фреймворком<strong>Combine</strong>, который представляет декларативный Api SwiftUI для обработки значений во времени. ViewModel реализует протокол ObservableObject и подключается как ObservedObject к конкретному View.</p>
3
<p>Изменяемые свойства модели декларируются как @Published.</p>
3
<p>Изменяемые свойства модели декларируются как @Published.</p>
4
class NewsItemModel: ObservableObject { @Published var title: String = "" @Published var description: String = "" @Published var image: String = "" @Published var dateFormatted: String = "" }<p>Как и в классическом MVVM, ViewModel общается с моделью данных (т.е бизнес-логикой) и передает данные в том или ином виде View.</p>
4
class NewsItemModel: ObservableObject { @Published var title: String = "" @Published var description: String = "" @Published var image: String = "" @Published var dateFormatted: String = "" }<p>Как и в классическом MVVM, ViewModel общается с моделью данных (т.е бизнес-логикой) и передает данные в том или ином виде View.</p>
5
struct NewsItemContentView: View { @ObservedObject var moder: NewsItemModel init(model: NewsItemModel) { self.model = model } //... какой-то код }<p>MVVM, как и практически любой другой паттерн, имеет тенденцию к перегруженности и избыточности. Загруженность ViewModel всегда зависит от того, насколько хорошо выделена и абстрагирована бизнес-логика. Загруженность View определяется сложностью зависимостью элементов от переменных состояния и переходов на другие View.</p>
5
struct NewsItemContentView: View { @ObservedObject var moder: NewsItemModel init(model: NewsItemModel) { self.model = model } //... какой-то код }<p>MVVM, как и практически любой другой паттерн, имеет тенденцию к перегруженности и избыточности. Загруженность ViewModel всегда зависит от того, насколько хорошо выделена и абстрагирована бизнес-логика. Загруженность View определяется сложностью зависимостью элементов от переменных состояния и переходов на другие View.</p>
6
<p>В SwiftUI к этому добавляется то, что<strong>View является структурой, а не классом</strong>, и, следовательно,<strong>не поддерживает наследование, вынуждая дублировать код</strong>. Если в небольших приложениях это не критично, то с ростом функционала и усложнения логики перегруз становится критическим, а большое количество копипаста угнетает.</p>
6
<p>В SwiftUI к этому добавляется то, что<strong>View является структурой, а не классом</strong>, и, следовательно,<strong>не поддерживает наследование, вынуждая дублировать код</strong>. Если в небольших приложениях это не критично, то с ростом функционала и усложнения логики перегруз становится критическим, а большое количество копипаста угнетает.</p>
7
<p>Попробуем воспользоваться подходом чистого кода и чистой архитектуры в данном случае. Совсем отказаться от MVVM мы не можем, все-таки на нем построен DataFlow SwiftUI, но немного перестроить вполне.</p>
7
<p>Попробуем воспользоваться подходом чистого кода и чистой архитектуры в данном случае. Совсем отказаться от MVVM мы не можем, все-таки на нем построен DataFlow SwiftUI, но немного перестроить вполне.</p>
8
<p><em>Предупреждение!</em></p>
8
<p><em>Предупреждение!</em></p>
9
<p><em>Если у вас аллергия на статьи про архитектуру, а от словосочетания Clean code выворачивает наизнанку, пролистните пару абзацев вниз</em>.<em>Это не совсем Clean code от дядюшки Боба!</em></p>
9
<p><em>Если у вас аллергия на статьи про архитектуру, а от словосочетания Clean code выворачивает наизнанку, пролистните пару абзацев вниз</em>.<em>Это не совсем Clean code от дядюшки Боба!</em></p>
10
<p>Да, мы не будем брать Clean Code дядюшки Боба в чистом виде. Как по мне, в нем присутствует оверинженеринг. Мы возьмем только идею.</p>
10
<p>Да, мы не будем брать Clean Code дядюшки Боба в чистом виде. Как по мне, в нем присутствует оверинженеринг. Мы возьмем только идею.</p>
11
<p>Основная идея чистого кода - это создание максимально читабельного кода, который можно потом безболезненно расширять и модифицировать.</p>
11
<p>Основная идея чистого кода - это создание максимально читабельного кода, который можно потом безболезненно расширять и модифицировать.</p>
12
<p>Существует довольно много принципов разработки ПО, которых рекомендуется придерживаться.</p>
12
<p>Существует довольно много принципов разработки ПО, которых рекомендуется придерживаться.</p>
13
<p>Многие их знают, но не все любят и не все используют. Это отдельная тема для холивара.</p>
13
<p>Многие их знают, но не все любят и не все используют. Это отдельная тема для холивара.</p>
14
<p>Для обеспечения чистоты кода как минимум следует разделить код на функциональные слои и модули, использовать решение задач в общем виде и реализовать абстракцию взаимодействия между компонентами.<strong>И, по крайней мере, нужно отделить код UI от так называемой бизнес-логики.</strong></p>
14
<p>Для обеспечения чистоты кода как минимум следует разделить код на функциональные слои и модули, использовать решение задач в общем виде и реализовать абстракцию взаимодействия между компонентами.<strong>И, по крайней мере, нужно отделить код UI от так называемой бизнес-логики.</strong></p>
15
<p>Независимо от выбранного архитектурного паттерна логика работы с БД и сетью, обработки и хранения данных отделяется от UI и модулей самого приложения. При этом модули работают с реализациями сервисов или хранилищ, которые в свою очередь обращаются к общему сервису сетевых запросов или общему хранилищу данных. Инициализация переменных, по которым можно обратиться к тому или иному сервису, производится в неком общем контейнере, к которому в итоге модуль приложения (бизнес- логика модуля) и обращается.</p>
15
<p>Независимо от выбранного архитектурного паттерна логика работы с БД и сетью, обработки и хранения данных отделяется от UI и модулей самого приложения. При этом модули работают с реализациями сервисов или хранилищ, которые в свою очередь обращаются к общему сервису сетевых запросов или общему хранилищу данных. Инициализация переменных, по которым можно обратиться к тому или иному сервису, производится в неком общем контейнере, к которому в итоге модуль приложения (бизнес- логика модуля) и обращается.</p>
16
<p>Если у нас выделена и абстрагирована бизнес-логика, то мы можем устраивать взаимодействие между компонентами модулей так, как нам нравится.</p>
16
<p>Если у нас выделена и абстрагирована бизнес-логика, то мы можем устраивать взаимодействие между компонентами модулей так, как нам нравится.</p>
17
<p>В принципе все существующие паттерны IOS-приложений функционируют по одному и тому же принципу.</p>
17
<p>В принципе все существующие паттерны IOS-приложений функционируют по одному и тому же принципу.</p>
18
<p>Всегда есть бизнес-логика, есть данные. Также есть некий диспетчер вызовов, то, что отвечает за представление и преобразование данных для вывода и то, куда выводятся преобразованные данные.<strong>Разница лишь в том, как распределяются роли между компонентами.</strong></p>
18
<p>Всегда есть бизнес-логика, есть данные. Также есть некий диспетчер вызовов, то, что отвечает за представление и преобразование данных для вывода и то, куда выводятся преобразованные данные.<strong>Разница лишь в том, как распределяются роли между компонентами.</strong></p>
19
<p>Т.к. мы стремимся сделать приложение читабельным, упростить текущие и будущие изменения, то логично все эти роли разделить. Бизнес-логика у нас уже выделена, данные всегда отделены. Остаются диспетчер, презентер и view. В итоге мы получаем архитектуру, состоящую из View-Interactor-Presenter, в которой интерактор взаимодействует с сервисами бизнес-логики, презентер преобразует данные и отдает их в виде некой ViewModel нашему View. По-хорошему навигация и конфигурация также выносятся из View в отдельные компоненты.</p>
19
<p>Т.к. мы стремимся сделать приложение читабельным, упростить текущие и будущие изменения, то логично все эти роли разделить. Бизнес-логика у нас уже выделена, данные всегда отделены. Остаются диспетчер, презентер и view. В итоге мы получаем архитектуру, состоящую из View-Interactor-Presenter, в которой интерактор взаимодействует с сервисами бизнес-логики, презентер преобразует данные и отдает их в виде некой ViewModel нашему View. По-хорошему навигация и конфигурация также выносятся из View в отдельные компоненты.</p>
20
<p>Получаем архитектуру VIP + R с разделением спорных ролей по разным компонентам.</p>
20
<p>Получаем архитектуру VIP + R с разделением спорных ролей по разным компонентам.</p>
21
<p>Попробуем посмотреть на примере. У нас есть небольшое приложение агрегатор новостей, написанное на SwiftUI и MVVM.</p>
21
<p>Попробуем посмотреть на примере. У нас есть небольшое приложение агрегатор новостей, написанное на SwiftUI и MVVM.</p>
22
<p>В приложении 3 отдельных экрана со своей логикой, т.е 3 модуля:
</p>
22
<p>В приложении 3 отдельных экрана со своей логикой, т.е 3 модуля:
</p>
23
<ul><li>модуль списка новостей;</li>
23
<ul><li>модуль списка новостей;</li>
24
<li>модуль экрана новости;</li>
24
<li>модуль экрана новости;</li>
25
<li>модуль поиска по новостям.</li>
25
<li>модуль поиска по новостям.</li>
26
</ul><p>Каждый из модулей состоит из ViewModel, которая взаимодействует с выделенной бизнес- логикой, и View, который отображает то, что ему транслирует ViewModel.</p>
26
</ul><p>Каждый из модулей состоит из ViewModel, которая взаимодействует с выделенной бизнес- логикой, и View, который отображает то, что ему транслирует ViewModel.</p>
27
<p>Мы стремимся к тому, чтобы ViewModel занимался только хранением готовых для отображения данных. Сейчас же он занимается как обращением к сервисам, так и обработкой полученных результатов.</p>
27
<p>Мы стремимся к тому, чтобы ViewModel занимался только хранением готовых для отображения данных. Сейчас же он занимается как обращением к сервисам, так и обработкой полученных результатов.</p>
28
<p>Эти роли мы переносим на презентер и интерактор, которые заводим для каждого модуля.</p>
28
<p>Эти роли мы переносим на презентер и интерактор, которые заводим для каждого модуля.</p>
29
<p>Полученные от сервиса данные интерактор передает презентеру, который наполняет подготовленными данными существующую ViewModel, привязанную к View. В принципе в том, что касается разделения бизнес-логики модуля, все несложно.
</p>
29
<p>Полученные от сервиса данные интерактор передает презентеру, который наполняет подготовленными данными существующую ViewModel, привязанную к View. В принципе в том, что касается разделения бизнес-логики модуля, все несложно.
</p>
30
<p>Теперь переходим к View. Попробуем разобраться с вынужденным дублированием кода. Если мы имеем дело с каким-нибудь контролом, то это могут быть его стили или настройки. Если же речь идет об экранном View, то это:</p>
30
<p>Теперь переходим к View. Попробуем разобраться с вынужденным дублированием кода. Если мы имеем дело с каким-нибудь контролом, то это могут быть его стили или настройки. Если же речь идет об экранном View, то это:</p>
31
<ul><li>стили экрана;</li>
31
<ul><li>стили экрана;</li>
32
<li>общие UI-элементы (LoadingView);</li>
32
<li>общие UI-элементы (LoadingView);</li>
33
<li>информационные алерты;</li>
33
<li>информационные алерты;</li>
34
<li>некие общие методы.</li>
34
<li>некие общие методы.</li>
35
</ul><p>Наследование мы использовать не можем, но вполне можем<strong>использовать композицию</strong>.<em>Именно по этому принципу создаются все кастомные View в SwiftUI</em>.</p>
35
</ul><p>Наследование мы использовать не можем, но вполне можем<strong>использовать композицию</strong>.<em>Именно по этому принципу создаются все кастомные View в SwiftUI</em>.</p>
36
<p>Итак, мы создаем View-контейнер, в который перенесем всю одинаковую логику, а наш экранный View передадим в инициализатор контейнера и затем используем как контентный View внутри body.</p>
36
<p>Итак, мы создаем View-контейнер, в который перенесем всю одинаковую логику, а наш экранный View передадим в инициализатор контейнера и затем используем как контентный View внутри body.</p>
37
struct ContainerView<Content>: IContainer, View where Content: View { @ObservedObject var containerModel = ContainerModel() private var content: Content public init(content: Content) { self.content = content } var body : some View { ZStack { content if (self.containerModel.isLoading) { LoaderView() } }.alert(isPresented: $containerModel.hasError){ Alert(title: Text(""), message: Text(containerModel.errorText), dismissButton: .default(Text("OK")){ self.containerModel.errorShown() }) } }<p>Экранный View встраивается в ZStack внутри body ContainerView, куда также вынесен код по отображению LoadingView и код для отображения информационного алерта.</p>
37
struct ContainerView<Content>: IContainer, View where Content: View { @ObservedObject var containerModel = ContainerModel() private var content: Content public init(content: Content) { self.content = content } var body : some View { ZStack { content if (self.containerModel.isLoading) { LoaderView() } }.alert(isPresented: $containerModel.hasError){ Alert(title: Text(""), message: Text(containerModel.errorText), dismissButton: .default(Text("OK")){ self.containerModel.errorShown() }) } }<p>Экранный View встраивается в ZStack внутри body ContainerView, куда также вынесен код по отображению LoadingView и код для отображения информационного алерта.</p>
38
<p>Также нам нужно, чтобы наш ContainerView получал сигнал от ViewModel внутреннего View и обновлял свое состояние. Мы не можем подписаться через @Observed на ту же модель, что и внутренний View, потому что перетянем ее сигналы.</p>
38
<p>Также нам нужно, чтобы наш ContainerView получал сигнал от ViewModel внутреннего View и обновлял свое состояние. Мы не можем подписаться через @Observed на ту же модель, что и внутренний View, потому что перетянем ее сигналы.</p>
39
<p>Поэтому мы налаживаем связь с ней через паттерн делегат, а для актуального состояния контейнера используем его собственную ContainerModel.</p>
39
<p>Поэтому мы налаживаем связь с ней через паттерн делегат, а для актуального состояния контейнера используем его собственную ContainerModel.</p>
40
class ContainerModel:ObservableObject { @Published var hasError: Bool = false @Published var errorText: String = "" @Published var isLoading: Bool = false func setupError(error: String){ //.... } func errorShown() { //... } func showLoading() { self.isLoading = true } func hideLoading() { self.isLoading = false } }<p>ContainerView реализует протокол IContainer, ссылка на экземпляр присваивается модели встраиваемого View.</p>
40
class ContainerModel:ObservableObject { @Published var hasError: Bool = false @Published var errorText: String = "" @Published var isLoading: Bool = false func setupError(error: String){ //.... } func errorShown() { //... } func showLoading() { self.isLoading = true } func hideLoading() { self.isLoading = false } }<p>ContainerView реализует протокол IContainer, ссылка на экземпляр присваивается модели встраиваемого View.</p>
41
protocol IContainer { func showError(error: String) func showLoading() func hideLoading() } struct ContainerView<Content>: IContainer, View where Content: View&IModelView { @ObservedObject var containerModel = ContainerModel() private var content: Content public init(content: Content) { self.content = content self.content.viewModel?.listener = self } //какой-то код }<p>View реализует протокол IModelView для инкапсуляции доступа к модели и унификации некоторой логики. Модели для тех же целей реализуют протокол IModel:</p>
41
protocol IContainer { func showError(error: String) func showLoading() func hideLoading() } struct ContainerView<Content>: IContainer, View where Content: View&IModelView { @ObservedObject var containerModel = ContainerModel() private var content: Content public init(content: Content) { self.content = content self.content.viewModel?.listener = self } //какой-то код }<p>View реализует протокол IModelView для инкапсуляции доступа к модели и унификации некоторой логики. Модели для тех же целей реализуют протокол IModel:</p>
42
protocol IModelView { var viewModel: IModel? {get} } protocol IModel:class { //.... var listener:IContainer? {get set} }<p>Затем уже в этой модели при необходимости вызывается метод делегата, например, для отображения алерта с ошибкой, в котором происходит изменение переменной состояния модели контейнера.</p>
42
protocol IModelView { var viewModel: IModel? {get} } protocol IModel:class { //.... var listener:IContainer? {get set} }<p>Затем уже в этой модели при необходимости вызывается метод делегата, например, для отображения алерта с ошибкой, в котором происходит изменение переменной состояния модели контейнера.</p>
43
struct ContainerView<Content>: IContainer, View where Content: View&IModelView { @ObservedObject var containerModel = ContainerModel() private var content: Content //какой-то код func showError(error: String) { self.containerModel.setupError(error: error) } func showLoading() { self.containerModel.showLoading() } func hideLoading() { self.containerModel.hideLoading() } }<p>Теперь мы можем унифицировать работу View, переключившись на работу через ContainerView. Это очень облегчит нам жизнь при работе с конфигурацией следующих модулей и навигацией. Как настроить навигацию в SwiftUI и сделать чистую конфигурацию, мы поговорим в следующей статье.</p>
43
struct ContainerView<Content>: IContainer, View where Content: View&IModelView { @ObservedObject var containerModel = ContainerModel() private var content: Content //какой-то код func showError(error: String) { self.containerModel.setupError(error: error) } func showLoading() { self.containerModel.showLoading() } func hideLoading() { self.containerModel.hideLoading() } }<p>Теперь мы можем унифицировать работу View, переключившись на работу через ContainerView. Это очень облегчит нам жизнь при работе с конфигурацией следующих модулей и навигацией. Как настроить навигацию в SwiftUI и сделать чистую конфигурацию, мы поговорим в следующей статье.</p>
44
<p>Исходники примера вы можете найти по<a>ссылке</a>.</p>
44
<p>Исходники примера вы можете найти по<a>ссылке</a>.</p>
45
45