Search code examples
swiftgoogle-cloud-firestorecombinecombinelatest

Swiftui + Firestore + CombineLatest keep publishing data


I'm trying to merges two published object from query of two different collections to make a single published array using combineLatest.

import Combine
import Firebase

class FirestoreViewModel: ObservableObject {
    let userId: String
    
    @Published var aList: [DocumentA]? = nil
    @Published var bList: [DocumentB]? = nil
    
    init(userId: String, listenForChanges: Bool = false)    {
        
        // Get the user
        if (listenForChanges)   {
            self.loadA()
            self.loadB()
        }
    }
    
    var cList: AnyPublisher<[DocumentC]?, Never> {
        return Publishers.CombineLatest(
            $aList.map { (aList) -> [DocumentC]?  in
                guard let aList = aList else { return nil }
                return aList.map {a in
                    DocumentC(from: a)
                }
            },
            $bList.map { (bList) -> [DocumentC]?  in
                guard let bList = bList else { return nil }
                return bList.map { n in
                    DocumentC(from: b)
                }
            })
            .map { (aList, bList) -> [DocumentC]?  in
                
                if (aList == nil && bList == nil) { return nil }
                
                var cList: [DocumentC] = []
                
                if let aList = aList {
                    cList.append(contentsOf: aList)
                }
                
                if let bList = bList {
                    cList.append(contentsOf: bList)
                }
                return cList
            }
            .eraseToAnyPublisher()
    }
    
    private func loadA() {
        
        // Start listening
        Firestore.firestore().collection("colA").addSnapshotListener { (snapshot, error) in
            if let error = error {
                print("DEBUG: Unable to get user data: \(error.localizedDescription)")
                return
            }
            
            // Invalid data return
            guard let snapshot = snapshot else {
                print("DEBUG: null data returned")
                self.aList = nil
                return
            }
            
            // Update the info
            var aList: [DocumentA] = []
            snapshot.documents.forEach { document in
                let aData = document.data()
                guard !aData.isEmpty else {
                    return
                }
                aList.append(DocumentA(from: aData)
                
            }
            self.aList = aList
        }
    }
    
    private func loadB() {
        
        // Start listening
        Firestore.firestore().collection("colB").addSnapshotListener { (snapshot, error) in
            if let error = error {
                print("DEBUG: Unable to get user data: \(error.localizedDescription)")
                return
            }
            
            // Invalid data return
            guard let snapshot = snapshot else {
                print("DEBUG: null data returned")
                self.bList = nil
                return
            }
            
            // Update the info
            var bList: [DocumentB] = []
            snapshot.documents.forEach { document in
                let bData = document.data()
                guard !bData.isEmpty else {
                    return
                }
                bList.append(DocumentB(from: bData))
                
            }
            self.userUnits = userUnits
        }
    }
}

When I debug, I can see that aList and bList are published once, however, the cList keeps been published eventually eating all the memory...

The cList is consumed via a onReceive statement in the view, which output each item.

Would anybody be able to tell me why the combineLatest keeps on publishing cList, though there are no fresh aList or bList published ?

Many thanks in advance.


Solution

  • In all likelihood, you have a situation where you update the view with values just received from cList, which causes the body to be recomputed, which causes another onReceive(vm.cList) {...}, which causes a new publisher to be returned by the computed property cList, which emits the values again and repeats the cycle.

    Here's an simplified example of what I mean:

    class ViewModel: ObservableObject {
       @Published var aList: [Int] = [1,2]
    
       var cList: AnyPublisher<Int, Never> {
          $aList.map { $0 + $0 }.eraseToAnyPublisher()
       }
    }
    
    struct ContentView: View {
       @StateObject var vm = ViewModel()
       @State var list: [Int] = []
    
       var body: some View {
          VStack {
             ForEach(list, id: \.self) { v in
                Text("\(v)")
             }
          }
          .onReceive(vm.cList) { self.list = $0 }
       }
    }
    

    To avoid that, cList shouldn't be a computed property. It could just be a lazy-ly assigned constant:

    lazy var cList: AnyPublisher<Int, Never> = 
       $alist.map { $0 + $0 }
             .eraseToAnyPublisher()