0

I am working with Swift Combine in a SwiftUI App. I've had success using it until recently. I am attempting to use a ViewModel to set value when using combine but it does not appear to be getting updated. What's odd is I have another ViewModel that is working. I am using Firebase as a database.

ListingRepository

This file starts a listener from Firestore and sets an @Published property with a array of listings.

class ListingRepository: ObservableObject {
    let db = Firestore.firestore()
    
    @Published var listings = [Listing]()
    
    init() {
        startSnapshotListener()
    }
    
    func startSnapshotListener() {
        db.collection(FirestoreCollection.listings).addSnapshotListener { (querySnapshot, error) in
            if let error = error {
                print("Error getting documents: \(error)")
            } else {
                guard let documents = querySnapshot?.documents else {
                    print("No Listings.")
                    return
                }
                
                self.listings = documents.compactMap { listing in
                    do {
                        return try listing.data(as: Listing.self)
                    } catch {
                        print(error)
                    }
                    return nil
                }
            }
        }
    }
}

Working ViewModel

This is the ViewModel that is working. I am using Combine here to map over the listings and create ListRowViewModels. The code works and I can populate the rows successfully.

class MarketplaceViewModel: ObservableObject {
    @Published var listingRepository = ListingRepository()
    @Published var listingRowViewModels = [ListingRowViewModel]()
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        listingRepository
            .$listings
            .receive(on: RunLoop.main)
            .map { listings in
                listings.map { listing in
                    ListingRowViewModel(listing: listing)
                }
            }
            .assign(to: \.listingRowViewModels, on: self)
            .store(in: &cancellables)
    }
}

ViewModel Not Working

This ViewModel is for an account view. I started simply by using the count() operator to try and capture the number of listings and then display this on the AccountView. When I build and run the view is not updated.

class AccountViewModel: ObservableObject {
    @Published var listingRepository = ListingRepository()
    
    @Published var numberOfListings: Int = 0
    @
        
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        listingRepository
            .$listings
            .receive(on: RunLoop.main)
            .count()
            .assign(to: \.numberOfListings, on: self)
            .store(in: &cancellables)
    }
}

Upon further inspection it appears the publisher is not being called. I used a sink to try and print the result of the count and nothing gets printed.

class AccountViewModel: ObservableObject {
    @Published var listingRepository = ListingRepository()
    
    @Published var numberOfListings: Int = 0
    @
        
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        listingRepository
            .$listings
            .receive(on: RunLoop.main)
            .count()
            .sink {
                print("Number Of Listings: \($0)")
            }
            .store(in: &cancellables)
    }
}

MarketplaceViewModel and AccountViewModel are used in MarketplaceView and AccountView`.

MarketplaceView

struct MarketplaceView: View {
    
    @ObservedObject var marketplaceViewModel = MarketplaceViewModel()
    
    
    var body: some View {
        
        Text(marketPlaceViewModel.listingRowViewModels[1].listing.name)
    }
}

AccountView

struct AccountView: View {
    
    @ObservedObject var accountViewModel = AccountViewModel()
        
    var body: some View {
        Text(String(accountViewModel.numberOfListings)                     }
    }
}

These two views are presented in ContentView

struct ContentView: View {
    
    var body: some View {
        Group{
           
                TabView {
                    MarketplaceView()
                        .tabItem {
                            Image(systemName: "shippingbox")
                            Text("Marketplace")
                        }.tag(0) // MarketplaceView
                    AccountView(user: testUser1)
                        .tabItem {
                            Image(systemName: "person")
                            Text("Account")
                        }.tag(1) // AccountView
                } // TabView
                .accentColor(.white)
        }// Group
    }
}

I'm very puzzled why this is not working. Any help would be greatly appreciated.

Alexander
  • 59,041
  • 12
  • 98
  • 151
jonthornham
  • 2,881
  • 4
  • 19
  • 35
  • Where is `MarketplaceViewModel` and `AccountViewModel` used? – Alexander Jul 10 '21 at 00:17
  • They are used in MarketplaceView and AccountView which is on ContentView. – jonthornham Jul 10 '21 at 00:51
  • Your question doesn't mention `ContentView`. – Alexander Jul 10 '21 at 01:04
  • @Alexander You are correct I edited the post. – jonthornham Jul 10 '21 at 01:15
  • This is a lot of code, which is usually a good thing (better to have too much rather than too little), but could you narrow it down to take out the things that aren't relevant to your issue? – Alexander Jul 10 '21 at 01:28
  • 1
    It seems to me that when you say `@Published var listingRepository = ListingRepository()` you are making a totally new ListingRepository, so that when your ObservableObject ListingRepository changes, it isn't this one. – matt Jul 10 '21 at 01:38
  • @matt how would you only make one? – jonthornham Jul 10 '21 at 01:40
  • Look at https://stackoverflow.com/a/63955859/341994. What you are doing seems to me to be exactly what that answer says not to do. – matt Jul 10 '21 at 01:51
  • @Alexander I've simplified the text. – jonthornham Jul 10 '21 at 01:51
  • @matt I tested this by commenting out the ListingRepository in MarketplaceViewModel so there was only one. This did not work. – jonthornham Jul 10 '21 at 02:07
  • It looks to me like you're creating a new `AccountViewModel` in every `AccountView`. This doesn't make sense. SwiftUI View structs are ephemeral. You would benefit from watching https://developer.apple.com/videos/play/wwdc2021/10022/ and https://developer.apple.com/videos/play/wwdc2020/10040/ – Alexander Jul 10 '21 at 02:13
  • Try replacing every use of `@ObservedObject` with `@StateObject`. – Peter Schorn Jul 10 '21 at 02:25
  • @PeterSchorn I tried this but it did not work. Thanks for the suggestion. – jonthornham Jul 10 '21 at 04:04
  • Have you confirmed that `addSnapshotListener` is being called and `ListingRepository.listings` is in fact being changed in this closure (as opposed to an error being thrown)? – Peter Schorn Jul 11 '21 at 04:12
  • @PeterSchorn I have. It’s loading the data for MarketplaceView – jonthornham Jul 11 '21 at 04:33
  • 1
    The `count` operator in your problem. I should've noticed it earlier. It only emits after the upstream publisher finishes, and a `Published.Publisher` never finishes. Read the [docs](https://developer.apple.com/documentation/combine/fail/count()). – Peter Schorn Jul 11 '21 at 21:41
  • 1
    What you probably meant is to retrieve the count of the `listings` array. For that, use `.map { $0.count }`. – Peter Schorn Jul 11 '21 at 21:48
  • @PeterSchorn. Brilliant! Thank you very much! This solved the problem. I will be sure to read the docs more thoroughly. – jonthornham Jul 11 '21 at 22:00

1 Answers1

0

Because it's ObservedObject instead of of a StateObject