0

I am trying to pass an array of values retrieved from Firebase into a View. However, the array passed into the View is empty when there should be values within the array.

This is the model I've created. eventLocations is the property that I want to pass into my View.

class FoodEventViewModel: ObservableObject {
  @Published var foodEvents = [FoodEvent]()
  @Published var eventLocations = [CLLocationCoordinate2D]()
  private var db = Firestore.firestore()
  var events = [FoodEvent?]()

  init() {
    getLocations()
  }

  func fetchData() {
    let ref = db.collection("Food Submissions").order(by: "dateCreated", descending: true)
    ref.addSnapshotListener { (querySnapshot, error) in
      guard let documents = querySnapshot?.documents else {
        print("No documents")
        return
      }
      
      self.events = documents.map { (queryDocumentSnapshot) -> FoodEvent? in
        let event = try? queryDocumentSnapshot.data(as: FoodEvent.self)
        return event
      }
    }
    foodEvents = events.compactMap { $0 }
  }
  
  func getLocations() {
    fetchData()
    for event in foodEvents {
      let coord = CLLocationCoordinate2D(latitude: event.latitude,
                                         longitude: event.longitude)
      eventLocations.append(coord)
    }
  }
}

I must be misunderstanding how data flows between views and models. Is there a way to make sure that my view receives a populated array? I've tried to use ObservableObject and EnvironmentObject to pass data to the view. I've tried to use .onAppear { viewModel.getLocations } but the view still has an empty array.

struct FoodMapView: View {
  @EnvironmentObject var lm: LocationManager
  @ObservedObject var viewModel = FoodEventViewModel()
  
  var body: some View {
    ZStack(alignment: .top) {
      MapView(locs: viewModel.eventLocations)
        .onAppear {
          viewModel.getLocations()
          print("Map View")
          print(viewModel.eventLocations)
        }
    }.navigationTitle("Map")
  }
}

Any help would be greatly appreciated!

1
  • Fetch data is asynchronous it is likely setting the variable after the loop. You have to trigger the loop after the variable is set. Commented Dec 6, 2021 at 3:48

1 Answer 1

1

When you are dealing with an asynchronous function (like addSnapshotListener), you can't expect to get results right away. Instead, you will get results at some indeterminate time in the future (if at all). So, you can't call an operation immediately after that depends on the results and expect it to work.

Typical ways of dealing with this are by using completion handlers/callback functions, Combine, or the new async/await system in Swift 5.5.

In my example, I've used a completion handler that is called when the snapshot listener returns a result. Note that there is definitely more refactoring that can be done, but this should get you the idea to get started.

class FoodEventViewModel: ObservableObject {
    @Published var foodEvents = [FoodEvent]()
    @Published var eventLocations = [CLLocationCoordinate2D]()
    private var db = Firestore.firestore()
    
    init() {
        getLocations()
    }
    
    func fetchData(completion: @escaping ([FoodEvent]) -> Void) {
        let ref = db.collection("Food Submissions").order(by: "dateCreated", descending: true)
        ref.addSnapshotListener { (querySnapshot, error) in
            guard let documents = querySnapshot?.documents else {
                print("No documents")
                return
            }
            
            // call the completion handler *within* the addSnapshotListener closure
            completion(documents.map { (queryDocumentSnapshot) -> FoodEvent? in
                let event = try? queryDocumentSnapshot.data(as: FoodEvent.self)
                return event
            }.compactMap { $0 })
        }
    }
    
    func getLocations() {
        fetchData(completion: { foodEvents in
            for event in foodEvents {
                let coord = CLLocationCoordinate2D(latitude: event.latitude,
                                                   longitude: event.longitude)
                self.eventLocations.append(coord)
            }
        })
    }
}

struct FoodMapView: View {
    @EnvironmentObject var lm: LocationManager
    @ObservedObject var viewModel = FoodEventViewModel()
    
    var body: some View {
        ZStack(alignment: .top) {
            if viewModel.eventLocations.count { //only display the map if there are results -- this is optional -- you could display it in either scenario
                MapView(locs: viewModel.eventLocations)
            } else {
                Text("No locations...")
            }
        }
        .onAppear {
            viewModel.getLocations()
        }
        .navigationTitle("Map")
    }
}

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.