
Проблема
Один из проектов нашей компании использует архитектуру 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.
Надеюсь, что я ничего не нарушил в процессе написания статьи. Прошу не кидать в меня палками, это только первая моя статья) Буду рад конструктивной критике!
