Search code examples
swiftuicombine

CurrentValueSubject send(value) doesn't trigger receiveValue


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.


Solution

  • 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)