Как стать автором
Обновить

VIPER и SwiftUI: Model layer

Время на прочтение2 мин
Количество просмотров3.8K

Проблема

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

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

Теги:
Хабы:
Всего голосов 3: ↑2 и ↓1+4
Комментарии12

Публикации

Истории

Работа

Swift разработчик
25 вакансий
iOS разработчик
16 вакансий

Ближайшие события