Search code examples
swiftuiobservedobject

Published/Observed var not updating in view swiftui w/ called function


Struggling to get a simple example up and running in swiftui:

  1. Load default list view (working)
  2. click button that launches picker/filtering options (working)
  3. select options, then click button to dismiss and call function with selected options (call is working)
  4. display new list of objects returned from call (not working)

I'm stuck on #4 where the returned query isn't making it to the view. I suspect I'm creating a different instance when making the call in step #3 but it's not making sense to me where/how/why that matters.

I tried to simplify the code some, but it's still a bit, sorry for that.

Appreciate any help!

Main View with HStack and button to filter with:

import SwiftUI
import FirebaseFirestore

struct TestView: View {
    @ObservedObject var query = Query()
    @State var showMonPicker = false
    @State var monFilter = "filter"

    
    var body: some View {
        VStack {
            HStack(alignment: .center) {
                Text("Monday")
                Spacer()
                Button(action: {
                    self.showMonPicker.toggle()
                }, label: {
                    Text("\(monFilter)")
                })
            }
            .padding()
            
            ScrollView(.horizontal) {
                LazyHStack(spacing: 35) {
                    ForEach(query.queriedList) { menuItems in
                        MenuItemView(menuItem: menuItems)
                    }
                }
            }
        }
        .sheet(isPresented: $showMonPicker, onDismiss: {
            //optional function when picker dismissed
        }, content: {
            CuisineTypePicker(selectedCuisineType: $monFilter)
        })
    }
}

The Query() file that calls a base query with all results, and optional function to return specific results:

import Foundation
import FirebaseFirestore

class Query: ObservableObject {
    
    @Published var queriedList: [MenuItem] = []
    
    init() {
        baseQuery()
    }
    
    func baseQuery() {
      let queryRef = Firestore.firestore().collection("menuItems").limit(to: 50)
        
        queryRef
            .getDocuments() { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                self.queriedList = querySnapshot?.documents.compactMap { document in
                    try? document.data(as: MenuItem.self)
                    
                } ?? []
            }
        }
    }
    
    func filteredQuery(category: String?, glutenFree: Bool?) {
        var filtered = Firestore.firestore().collection("menuItems").limit(to: 50)

      // Sorting and Filtering Data
        if let category = category, !category.isEmpty {
          filtered = filtered.whereField("cuisineType", isEqualTo: category)
        }

        if let glutenFree = glutenFree, !glutenFree {
          filtered = filtered.whereField("glutenFree", isEqualTo: true)
        }

      filtered
            .getDocuments() { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                self.queriedList = querySnapshot?.documents.compactMap { document in
                    try? document.data(as: MenuItem.self);
                } ?? []
                print(self.queriedList.count)
            }
        }
    }
}

Picker view where I'm calling the filtered query:

import SwiftUI

struct CuisineTypePicker: View {
    
    @State private var cuisineTypes = ["filter", "American", "Chinese", "French"]
    @Environment(\.presentationMode) var presentationMode

    @Binding var selectedCuisineType: String
    @State var gfSelected = false
    
    let query = Query()
       
    var body: some View {
        VStack(alignment: .center) {
            //Buttons and formatting code removed to simplify.. 
            }
            .padding(.top)
            
            Picker("", selection: $selectedCuisineType) {
                ForEach(cuisineTypes, id: \.self) {
                    Text($0)
                }
            }
            Spacer()
            Button(action: {
                self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
                
                self.presentationMode.wrappedValue.dismiss()
                
            }, label: {
                Text( "apply filters")
            })               
        }
        .padding()
    }
}

Solution

  • I suspect that the issue stems from the fact that you aren't sharing the same instance of Query between your TestView and your CuisineTypePicker. So, when you start a new Firebase query on the instance contained in CuisineTypePicker, the results are never reflected in the main view.

    Here's an example of how to solve that (with the Firebase code replaced with some non-asynchronous sample code for now):

    struct MenuItem : Identifiable {
        var id = UUID()
        var cuisineType : String
        var title : String
        var glutenFree : Bool
    }
    
    struct ContentView: View {
        @ObservedObject var query = Query()
        @State var showMonPicker = false
        @State var monFilter = "filter"
        
        var body: some View {
            VStack {
                HStack(alignment: .center) {
                    Text("Monday")
                    Spacer()
                    Button(action: {
                        self.showMonPicker.toggle()
                    }, label: {
                        Text("\(monFilter)")
                    })
                }
                .padding()
                
                ScrollView(.horizontal) {
                    LazyHStack(spacing: 35) {
                        ForEach(query.queriedList) { menuItem in
                            Text("\(menuItem.title) - \(menuItem.cuisineType)")
                        }
                    }
                }
            }
            .sheet(isPresented: $showMonPicker, onDismiss: {
                //optional function when picker dismissed
            }, content: {
                CuisineTypePicker(query: query, selectedCuisineType: $monFilter)
            })
        }
    }
    
    class Query: ObservableObject {
        
        @Published var queriedList: [MenuItem] = []
        
        private let allItems: [MenuItem] = [.init(cuisineType: "American", title: "Hamburger", glutenFree: false),.init(cuisineType: "Chinese", title: "Fried Rice", glutenFree: true)]
        
        init() {
            baseQuery()
        }
        
        func baseQuery() {
            self.queriedList = allItems
        }
        
        func filteredQuery(category: String?, glutenFree: Bool?) {
            queriedList = allItems.filter({ item in
                if let category = category {
                    return item.cuisineType == category
                } else {
                    return true
                }
            }).filter({item in
                if let glutenFree = glutenFree {
                    return item.glutenFree == glutenFree
                } else {
                    return true
                }
            })
        }
    }
    
    struct CuisineTypePicker: View {
        @ObservedObject var query : Query
        @Binding var selectedCuisineType: String
        @State private var gfSelected = false
        
        private let cuisineTypes = ["filter", "American", "Chinese", "French"]
        @Environment(\.presentationMode) private var presentationMode
        
        var body: some View {
            VStack(alignment: .center) {
                //Buttons and formatting code removed to simplify..
            }
            .padding(.top)
            
            Picker("", selection: $selectedCuisineType) {
                ForEach(cuisineTypes, id: \.self) {
                    Text($0)
                }
            }
            Spacer()
            Button(action: {
                self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
                self.presentationMode.wrappedValue.dismiss()
            }, label: {
                Text( "apply filters")
            })
        }
    }