The view is not updated with data from Firestore in SwiftUI

Advertisements

I have a little issue, I’m fetching data from Firebase, from Firestore database, but the view is only updated when I’m selecting one of the category. The view has 4 categories which can contains items. I’ll post a short video at the end of this questions to make it clear what’s my problem.

So, this is my ViewModel :

class HomeViewModell : ObservableObject {
    
    
    @Published var productType : ProductType = .Wearable
    @Published var products = [Product]()
    @Published var filteredProducts : [Product] = []
    var searchCancellable: AnyCancellable?
    init() {
        getData()
        filteredProductByType()
    }
    func getData() {
        FirebaseManager.shared.firestore.collection("products").addSnapshotListener { querySnapshot, error in
            
            guard let documents = querySnapshot?.documents else {
                print("No documents")
                return
            }
            DispatchQueue.main.async {
                self.products = documents.map { d in
                    return Product(id: d.documentID, type: ProductType(rawValue: d["type"] as? String ?? "") ?? .Laptops
                                   , title: d["title"] as? String ?? "",
                                   subtitle: d["subtitle"] as? String ?? "",
                                   price: d["price"] as? String ?? "", productImage: d["productImage"] as? String ?? ""
                    )
                }
            }
        }
    }
    
    func filteredProductByType() {
        DispatchQueue.global(qos: .userInteractive).async {
            let results = self.products
            //Since it will require more memory so were using lazy to perform more
                .lazy
                .filter { product in
                    return product.type == self.productType
                }
            // Limiting results..
                .prefix(4)
            
            DispatchQueue.main.async {
                self.filteredProducts = results.compactMap({ product in
                    return product
                })
            }
        }
    }
}

These are my structs :

struct Product : Identifiable, Hashable {
    var id = UUID().uuidString
    var type : ProductType
    var title : String
    var subtitle : String
    var description : String = ""
    var price : String
    var productImage : String = ""
    var quantity : Int = 1
}

enum ProductType : String, CaseIterable {
    case Wearable = "Wearable"
    case Laptops = "Laptops"
    case Phones = "Phones"
    case Tablets = "Tablets"
}

And this is my view


struct Homee: View {
    @Namespace var animation : Namespace.ID
    @EnvironmentObject var sharedData : SharedDataViewModel
    @ObservedObject var homeData : HomeViewModel = HomeViewModel()
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            VStack(spacing : 15) {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing : 18) {
                        ForEach(ProductType.allCases, id: \.self) { type in
                            productTypeView(type: type)
                        }
                    }
                    .padding(.horizontal, 25)
                }
                .padding(.top, 28 )
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing : 25) {
                        ForEach(homeData.filteredProducts) { product in
                            ProductCardView(product: product)
                        }
                    }
                    .padding(.horizontal, 25)
                    .padding(.bottom)
                    .padding(.top, 80)
                }
                .padding(.top, 30)
            }
            .padding(.vertical)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color("HomeBG"))
        .onChange(of: homeData.productType) { newValue in
            homeData.filteredProductByType()
    }
    }

    @ViewBuilder
    func productTypeView(type : ProductType) -> some View {
        Button {
            withAnimation {
                homeData.productType = type
            }
        } label: {
            Text(type.rawValue)
                .font(.custom(customFont, size: 15))
                .fontWeight(.semibold)
                .padding(.bottom, 10)
                .overlay(
                    ZStack {
                        if homeData.productType == type {
                            Capsule()
                                .fill(Color("Purple"))
                                .matchedGeometryEffect(id: "PRODUCTTAB", in: animation)
                                .frame(height : 2)
                        } else {
                            Capsule()
                                .fill(Color.clear)
                                .frame(height : 2)
                        }
                    }
                        .padding(.horizontal, -5)
                    , alignment: .bottom
                )
        }
    }
    @ViewBuilder
    func ProductCardView(product : Product) -> some View {
        VStack(spacing: 10) {
           //Addid matched geometry effect
            ZStack {
                if sharedData.showDetailProduct {
                    Image(product.productImage)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                }
                else {
                    Image(product.productImage)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .matchedGeometryEffect(id: "\(product.id)IMAGE", in: animation)
                }
            }
                .frame(width: getRect().width / 2.5, height: getRect().width / 2.5)
                .offset(y: -80)
                .padding(.bottom, -80
            Text(product.title)
                .font(.custom(customFont, size: 18))
                .fontWeight(.semibold)
                .padding(.top)
            Text(product.subtitle)
                .font(.custom(customFont, size: 14))
                .fontWeight(.semibold)
                .foregroundColor(.gray)
            Text(product.price)
                .font(.custom(customFont, size: 16))
                .fontWeight(.bold)
                .foregroundColor(Color("Purple"))
                .padding(.top, 5)
        }
        .padding(.horizontal,20)
        .padding(.bottom, 22)
        .background(
            Color.white.cornerRadius(25)
            )
    }
    
}

Anyone know what can be the issue ?

So, only when a category is selected, my items appears.

Watch here the issue

>Solution :

The actions inside getData() are asynchronous. That means that what happens inside that function will not necessarily have executed by the time you call filteredProductByType() in your init method. So, on first run, it pretty much guarantees that filteredProducts will be empty.

To fix this, you could, for example, call filteredProductByType after your self.products = documents.map { closure inside getData.

That being said, a more efficient way of dealing with the problems would likely be to turn filteredProducts into a computed property that generates its results when called. Something like:

var filteredProducts: [Product] {
  products.filter { $0.type == self.productType }
}

Then, you’d never have to call filteredProductByType and worry about the order that it happens.

Leave a ReplyCancel reply