With SwiftUI / Combine, How to avoid to put cancellables in ViewModel

Viewed 1106

I have always placed the DisposeBag in ViewController in MVVM with RxSwift like it said in this topic:

On iOS, for the DisposeBag in MVVM, can it be placed in ViewModel?

But with combine, since the View is a struct and cancelable can't be placed in this, I am stuck with solution.

How to manage subscription between View and VM in Combine without add cancelable in ViewModel

Or maybe, in SwiftUI / Combine, there is no choice to place cancelables in VM.

There is an example of implementation in SiwftUI / Combine :

The ViewModel


class EquityViewModel: ObservableObject {
    
    @Injected private var api: AlphaVantageAPI
    
    private var cancellables = Set<AnyCancellable>()
    private let code: String
    @Published private var result: Quote?
    @Published var price: String = ""
    
    init(code: String) {
        self.code = code
        self.$result
            .map {
                return "\($0?.price ?? 0) €"
            }.assign(to: &$price)
    }
    
    
    func addToPortfolio(){
        
    }
    
    func onAppear() {
        self.api.quote(symbol: self.code).share()
            .sink { completion in }
                receiveValue: { quote in
                    self.result = quote.quote
                }
            .store(in: &cancellables)
    }
    
}

The View

struct EquityView: View {
    
    @ObservedObject  var viewModel: EquityViewModel
    
    init(viewModel: EquityViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        ZStack {
            Color("primary").edgesIgnoringSafeArea(.all)
            VStack {
                Text("Stock Price")
                    .foregroundColor(.white)
                    .frame(minWidth: 0,
                           maxWidth: .infinity,
                           alignment: .topLeading)
                    .padding()
                HStack {
                    Text(self.viewModel.price)
                        .foregroundColor(.white)
                    Text("+4.75 %")
                        .foregroundColor(.white)
                        .padding(.leading, 20)
                }.frame(minWidth: 0,
                        maxWidth: .infinity,
                        alignment: .topLeading)
                .padding()
                Button(action: self.viewModel.addToPortfolio, label: {
                    Text("Add to portfolio")
                        .foregroundColor(.white)
                        .frame(minWidth: 0,
                                maxWidth: .infinity,
                                maxHeight: 30,
                                alignment: .center)
                        .background(Color.blue)
                        .cornerRadius(5)
                }).padding()
                Spacer()
            }
        }.frame(alignment: .leading)
        .onAppear(perform: self.viewModel.onAppear)
    }
}
2 Answers

It that what you search? I can't test it i don't know what a library you are use

class EquityViewMode: ObservableObject {
//    @Injected private var api: AlphaVantageAPI
    
    var pricePublisher: AnyPublisher<String, Never>
    @Published var price: String = ""

    init(){
        
//         init your publisher like
//        pricePublisher = self.api.quote(symbol: self.code)
//            .share()
//            .map { "\($0?.price ?? 0) €" }
//            .eraseToAnyPublisher()

}


struct EquityView: View {

    @ObservedObject var viewModel: EquityViewModel
    var handle: AnyCancellable? = nil

    init(m:EquityViewMode) {
        viewModel = m
        handle = m.pricePublisher.assign(to: \.price, on: self.viewModel)
    }

    var body: some View{
        Text(viewModel.price)
    }
}

You have to store them in some place (generally in the viewModel) to have the reference and delete them when you don't need them anymore. To do it you can use the deinit method:

deinit {
    cancellables.removeAll()
}
Related