0

I have a CurrentValueSubject to hold data received from Firebase fetch request.

final class CardRepository: ObservableObject {

    private let store = Firestore.firestore()
    var resultSubject = CurrentValueSubject<[Card], Error>([])
    init() {
    }
    
    func get() {
        store.collection(StorageCollection.EnglishCard.getPath)
            .addSnapshotListener { [unowned self] snapshot, err in
                if let err = err {
                    resultSubject.send(completion: .failure(err))
                }
                if let snapshot = snapshot {
                    let cards = snapshot.documents.compactMap {
                        try? $0.data(as: Card.self)
                    }
                    resultSubject.send(cards)
                }
            }
    }
}

In my ViewModel, I want whenever resultSubject sends or emits a value. It will change the state and has that value attached to the succes state.

class CardViewModel: CardViewModelProtocol, ObservableObject {
    
    @Published var repository: CardRepository
    @Published private(set) var state: CardViewModelState = .loading
    private var cancellables: Set<AnyCancellable> = []

    required init (_ repository: CardRepository) {
        self.repository = repository
        bindingCards()
        
    }
    
    private func bindingCards() {
        let _ = repository.resultSubject
            .sink { [unowned self] comp in
                switch comp {
                case .failure(let err):
                    self.state = .failed(err: err)
                case .finished:
                    print("finised")
                }
            } receiveValue: { [unowned self] res in
                self.state = .success(cards: res)
            }

    }
    
    func add(_ card: Card) {
        repository.add(card)
    }
    
    func get() {
        repository.get()
    }

}

On my ContentView, it will display a button that print the result.

struct ContentView: View {
    @StateObject var viewModel = CardViewModel(CardRepository())
    var body: some View {
        Group {
            switch viewModel.state {
            case .loading:
                ProgressView()
                Text("Loading")
            case .success(cards: let cards):
                let data = cards
                Button {
                    print(data)
                } label: {
                    Text("Tap to show cards")
                }
            case .failed(err: let err):
                Button {
                    print(err)
                } label: {
                    Text("Retry")
                }
            }
            Button {
                viewModel.get()
            } label: {
                Text("Retry")
            }
        }.onAppear {viewModel.get() }
    }
}

My problem is the block below only trigger once when I first bind it to the resultSubject.

} receiveValue: { [unowned self] res in
                self.state = .success(cards: res)
            }

I did add a debug and resultSubject.send(cards) works every time.

bao le
  • 927
  • 2
  • 7
  • 12
  • We don't use view model objects in SwiftUI, you have to learn the View struct value type and the property wrappers that makes it behave like an object. If you use objects you'll just get the same consistency bugs it was designed to eliminate. – malhal May 31 '22 at 20:19
  • can you elaborate or link an article if possible please? I've been following SwiftUI tutorials and most of them use MVVM with an `viewModel` object (which will be injected at root or previous view, I just initialized it for faster testing) – bao le Jun 01 '22 at 03:06

1 Answers1

1

You need to store the Cancellable returned from the .sink in the class so it doesn't get deallocated:

Either in a set var cancellables = Set<AnyCancellable>() if you want to use multiple Publishers, or in var cancellable: AnyCancellable?.

Add .store(in &cancellables) like so:

} receiveValue: { [unowned self] res in
    self.state = .success(cards: res)
}.store(in: &cancellables)

Edit:

In ObservableObject classes we don't use sink, we assign to an @Published:

let _ = repository.resultSubject
    .assign(to: &$self.state)
Timmy
  • 4,098
  • 2
  • 14
  • 34
  • we don't use `sink` or cancellables in `ObservableObject`, we `assign` the end of the pipeline to `@Published` vars. – malhal May 31 '22 at 20:17