VIPER и SwiftUI: Model layer

—

от автора

Проблема

Один из проектов нашей компании использует архитектуру 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.

Надеюсь, что я ничего не нарушил в процессе написания статьи. Прошу не кидать в меня палками, это только первая моя статья) Буду рад конструктивной критике!


ссылка на оригинал статьи https://habr.com/ru/articles/575150/