Igor Ševtšenko
Igor Ševtšenko

Reputation: 142

SwiftUI View don't see property of ObservableObject marked with @Published

I'm writing my app using SwiftUI and VIPER. And to save the idea of viper(testability, protocols and etc) and SwiftUI reactivity I want to add 1 more layer - ViewModel. My presenter will ask data from interactor and will put in ViewModel, then view will just read this value.I checked does method that put data into view model works - and yes it does. But my view just don't see the property of view model (shows empty list) even if it conforms to ObservableObject and property is marked with Published. What is more interesting that if I store data in presenter and also mark it with published and observable object it will work. Thank in advance!

class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
    var interactor: BeersListInteractorProtocol
    @ObservedObject var viewModel = BeersListViewModel()
    
    init(interactor: BeersListInteractorProtocol){
        self.interactor = interactor
        
    }
    func loadList(at page: Int){
        interactor.loadList(at: page) { beers in
            DispatchQueue.main.async {
                self.viewModel.beers.append(contentsOf: beers)
                print(self.viewModel.beers)
            }
        }
    }


class BeersListViewModel:ObservableObject{
    @Published var beers = [Beer]()
}


struct BeersListView: View{
    var presenter : BeersListPresenterProtocol
    @StateObject var viewModel : BeersListViewModel
    var body: some View {
        NavigationView{
            List{
                ForEach(viewModel.beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(presenter.formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }

Upvotes: 0

Views: 1110

Answers (1)

lorem ipsum
lorem ipsum

Reputation: 29242

Some things to note.

You can't chain ObservableObjects so @ObservedObject var viewModel = BeersListViewModel() inside the class won't work.

The second you have 2 ViewModels one in the View and one in the Presenter you have to pick one. One will not know what the other is doing.

Below is how to get your code working

import SwiftUI
struct Beer: Identifiable{
    var id: UUID = UUID()
    var name: String = "Hops"
    var abv: String = "H"
}
protocol BeersListInteractorProtocol{
    func loadList(at: Int, completion: ([Beer])->Void)
}

struct BeersListInteractor: BeersListInteractorProtocol{
    func loadList(at: Int, completion: ([Beer]) -> Void) {
        completion([Beer(), Beer(), Beer()])
    }
}
protocol BeersListPresenterProtocol: ObservableObject{
    var interactor: BeersListInteractorProtocol { get set }
    var viewModel : BeersListViewModel { get set }
    
    func formattedABV(_ abv: String) -> String
    func loadList(at page: Int)
}
class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
    var interactor: BeersListInteractorProtocol
    //You can't chain `ObservedObject`s
    @Published var viewModel = BeersListViewModel()
    
    init(interactor: BeersListInteractorProtocol){
        self.interactor = interactor
        
    }
    func loadList(at page: Int){
        interactor.loadList(at: page) { beers in
            DispatchQueue.main.async {
                self.viewModel.beers.append(contentsOf: beers)
                print(self.viewModel.beers)
            }
        }
    }
    func formattedABV(_ abv: String) -> String{
        "**\(abv)**"
    }
}

//Change to struct
struct BeersListViewModel{
    var beers = [Beer]()
}


struct BeerListView<T: BeersListPresenterProtocol>: View{
    //This is what will trigger view updates
    @StateObject var presenter : T
    //The viewModel is in the Presenter
    //@StateObject var viewModel : BeersListViewModel
    var body: some View {
        NavigationView{
            List{
                Button("load list", action: {
                    presenter.loadList(at: 1)
                })
                ForEach(presenter.viewModel.beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(presenter.formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }
                    }
                }
            }
        }
    }
}
struct BeerListView_Previews: PreviewProvider {
    static var previews: some View {
        BeerListView(presenter: BeersListPresenter(interactor: BeersListInteractor()))
    }
}

Now I am not a VIPER expert by any means but I think you are mixing concepts. Mixing MVVM and VIPER.Because in VIPER the presenter Exists below the View/ViewModel, NOT at an equal level.

I found this tutorial a while ago. It is for UIKit but if we use an ObservableObject as a replacement for the UIViewController and the SwiftUI View serves as the storyboard.

It makes both the ViewModel that is an ObservableObject and the View that is a SwiftUI struct a single View layer in terms of VIPER.

You would get code that looks like this

protocol BeersListPresenterProtocol{
    var interactor: BeersListInteractorProtocol { get set }
    
    func formattedABV(_ abv: String) -> String
    func loadList(at: Int, completion: ([Beer]) -> Void)
    
}
struct BeersListPresenter: BeersListPresenterProtocol{
    var interactor: BeersListInteractorProtocol
    
    init(interactor: BeersListInteractorProtocol){
        self.interactor = interactor
        
    }
    func loadList(at: Int, completion: ([Beer]) -> Void) {
        
        interactor.loadList(at: at) { beers in
            completion(beers)
        }
    }
    func formattedABV(_ abv: String) -> String{
        "**\(abv)**"
    }
}
protocol BeersListViewProtocol: ObservableObject{
    var presenter: BeersListPresenterProtocol { get set }
    var beers: [Beer] { get set }
    func loadList(at: Int)
    func formattedABV(_ abv: String) -> String
    
}
class BeersListViewModel: BeersListViewProtocol{
    @Published var presenter: BeersListPresenterProtocol
    @Published var beers: [Beer] = []
    
    init(presenter: BeersListPresenterProtocol){
        self.presenter = presenter
    }
    func loadList(at: Int) {
        DispatchQueue.main.async {
            self.presenter.loadList(at: at, completion: {beers in
                self.beers = beers
            })
        }
    }
    
    func formattedABV(_ abv: String) -> String {
        presenter.formattedABV(abv)
    }
}
struct BeerListView<T: BeersListViewProtocol>: View{
    @StateObject var viewModel : T
    
    var body: some View {
        NavigationView{
            List{
                Button("load list", action: {
                    viewModel.loadList(at: 1)
                })
                ForEach(viewModel.beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(viewModel.formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }
                    }
                }
            }
        }
    }
}
struct BeerListView_Previews: PreviewProvider {
    static var previews: some View {
        BeerListView(viewModel: BeersListViewModel(presenter: BeersListPresenter(interactor: BeersListInteractor())))
    }
}

If you don't want to separate the VIPER View Layer into the ViewModel and the SwiftUI View you can opt to do something like the code below but it makes it harder to replace the UI and is generally not a good practice. Because you WON'T be able to call methods from the presenter when there are updates from the interactor.

struct BeerListView<T: BeersListPresenterProtocol>: View, BeersListViewProtocol{
    var presenter: BeersListPresenterProtocol
    @State var beers: [Beer] = []
    
    var body: some View {
        NavigationView{
            List{
                Button("load list", action: {
                    loadList(at: 1)
                })
                ForEach(beers, id: \.id){ beer in
                    HStack{
                        VStack(alignment: .leading){
                            Text(beer.name)
                                .font(.headline)
                            Text("Vol: \(formattedABV(beer.abv))")
                                .font(.subheadline)
                            
                        }
                    }
                }
            }
        }
    }
    func loadList(at: Int) {
        DispatchQueue.main.async {
            self.presenter.loadList(at: at, completion: {beers in
                self.beers = beers
            })
        }
    }
    
    func formattedABV(_ abv: String) -> String {
        presenter.formattedABV(abv)
    }
}
struct BeerListView_Previews: PreviewProvider {
    static var previews: some View {
        BeerListView<BeersListPresenter>(presenter: BeersListPresenter(interactor: BeersListInteractor()))
    }
}

Upvotes: 1

Related Questions