» » VIPER and SwiftUI: Model layer

VIPER and SwiftUI: Model layer

Problem

One of our company's projects uses the VIPER architecture. At the time of UIKit, there were no problems with it, but a new "dark" era of SwiftUI has come. Under SwiftUI conditions, "pure" VIPER is not possible. I had to come up with something, because a similar solution on the network did not fit.

Theory

My idea was as follows: if VIPER uses delegates and reactivity looks at least strange in it, and SwiftUI is just so reactive, you need to make a separate layer that will support reactivity, and at the same time will work as a delegate of the Presenter layer. Why not use reactive variables in Presenter? The answer is that I didn't want to clutter this layer with reactivity. As for me, these layers should be separated and the Presenter should perform its original functions. In this case, there is less confusion and there is a distinction: what happens and on which layer. The name of this layer is Model, which follows from the title of the article. It should work the way the View would work from the side of interaction with the Presenter. That is, accept events and send your own to SwiftUIView.

 

Implementation

First, let's analyze the implementation of the Model layer itself.

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
    }
}

In the SceneModel we store various states and data that the Presenter prepares for us, in our case these are reactive fields.

// Реализуем презентер как делегат модели
extension ScenePresenter: SceneModelOutput {
    func loadArticles() {
        interactor.loadArticles()
    }
}

// Вызываем метод делегата презентера
extension ScenePresenter: SceneModelOutput {
    func articlesLoaded(articles: [Article]) {
        model.setArticles(articles: articles)
        model.setArticlesState(state: .loaded)
    }
}

In the Presenter, we implement the functionality of the model delegate and, in our case, by receiving an event from the Interactor, we execute functions on the model.

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()
        })
    }
    
}

In SwiftUIView we build interaction with the model.

In all other modules, everything remains to work in the same way as it worked before.

Thus, we get VIPER working in the reactive space with SwiftUI. At the same time, the essence of VIPER in delegation of authority between modules is not violated and the "reactivity" of SwiftUI is preserved.

Related Articles

Add Your Comment

reload, if the code cannot be seen

All comments will be moderated before being published.