
Проблема
Один из проектов нашей компании использует архитектуру VIPER. Во времена UIKit проблем с ней не было, но настала новая »темная» эра SwiftUI. В условиях SwiftUI «чистый» VIPER невозможен. Пришлось что-то придумывать, поскольку аналогичное решение в сети не подходило.

Теория
Моя идея состояла в следующем: если VIPER использует делегаты и реактивность в нем смотрится как минимум странно, а SwiftUI как раз таки реактивен - нужно сделать отдельный слой, который будет поддерживать реактивность, и при этом будет работать как делегат Presenter слоя. Почему не использовать реактивные переменные в Presenter? Ответом послужит, что мне не хотелось загромождать этот слой реактивностью. Как по мне, эти слои должны быть разделены и Presenter должен выполнять свои изначальные функции. В таком случае возникает меньше путаницы и идет разграничение: что происходит и на каком именно слое. Название у этого слоя выбрано Model, что следует из тайтла статьи. Он должен работать, как работало бы View со стороны взаимодействия с Presenter. То есть принимать ивенты и отправлять свои на SwiftUIView. А с самим SwiftUI View слой будет связан реактивными переменными.

Реализация
Для начала разберем реализацию самого слоя Model
import Combine
class SceneModel: ObservableObject {
var output: SceneModelOutput! // Реализуем презентер как делегат модели
@Published var articlesState: State = .rest {
didSet {
if articlesState == .loaded {
setSpinnerState(isSpin: false)
}
}
}
@Published var articles: [Articles] = []
@Published var spinnerState: Bool = false
func loadArticles() {
output.loadArticles()
}
func setSpinnerState(isSpin: Bool) {
spinnerState = isSpin
}
}
// Реализуем модель как делегат презентера
extension SceneModel: SceneModelInput {
func setArticlesState(state: State) {
articlesState = state
}
func setArticles(articles: [Articles]) {
self.articles = articles
}
}
В SceneModel мы храним различные состояния и данные, которые нам подготавливает Presenter, в нашем случае это реактивные поля.
// Реализуем презентер как делегат модели
extension ScenePresenter: SceneModelOutput {
func loadArticles() {
interactor.loadArticles()
}
}
// Вызываем метод делегата презентера
extension ScenePresenter: SceneModelOutput {
func articlesLoaded(articles: [Article]) {
model.setArticles(articles: articles)
model.setArticlesState(state: .loaded)
}
}
В Presenter мы реализуем функционал делегата модели и, в нашем случае, получая событие из Interactor выполняем функции на модели.
import SwiftUI
struct SceneView: View {
@ObservedObject var output: SceneModel
var body: some View {
Group {
switch output.articlesState {
case .loaded:
List {
ForEach(output.articles) {
Text($0.text)
}
}
default:
EmptyView()
}
}
.onAppear(perform: {
output.loadArticles()
})
}
}
В SwiftUIView выстраиваем взаимодействие с моделью.
Во всех остальных же модулях все остается работать так же, как и работало раньше.
Таким образом получаем работающий в реактивном пространстве VIPER со SwiftUI. При этом не нарушается суть VIPER в делегировании полномочий между модулями и сохраняется "реактивность" SwiftUI.
Надеюсь, что я ничего не нарушил в процессе написания статьи. Прошу не кидать в меня палками, это только первая моя статья) Буду рад конструктивной критике!