0

I have been getting this problem now a few times when I'm coding and I think I just don't understand the way SwiftUI execute the order of the code.

I have a method in my context model that gets data from Firebase that I call in .onAppear. But the method doesn't execute the last line in the method after running the whole for loop.

And when I set breakpoints on different places it seems that the code first is just run through without making the for loop and then it returns to the method again and then does one run of the for loop and then it jumps to some other strange place and then back to the method again...

I guess I just don't get it?

Has it something to do with main/background thread? Can you help me?

Here is my code.

Part of my UI-view that calls the method getTeachersAndCoursesInSchool

VStack {
  //Title
  Text("Settings")
    .font(.title)

  Spacer()

  NavigationView {
    VStack {
      NavigationLink {
        ManageCourses()
          .onAppear {
            model.getTeachersAndCoursesInSchool()
          }
      } label: {
        ZStack {
          // ...
        }
      }
    }
  }
}

Here is the for-loop of my method:

//Get a reference to the teacher list of the school
let teachersInSchool = schoolColl.document("TeacherList")

//Get teacherlist document data
teachersInSchool.getDocument { docSnapshot, docError in
  if docError == nil && docSnapshot != nil {

    //Create temporary modelArr to append teachermodel to
    var tempTeacherAndCoursesInSchoolArr = [TeacherModel]()

    //Loop through all FB teachers collections in local array and get their teacherData
    for name in teachersInSchoolArr {

      //Get reference to each teachers data document and get the document data
      schoolColl.document("Teachers").collection(name).document("Teacher data").getDocument {
        teacherDataSnapshot, teacherDataError in

        //check for error in getting snapshot
        if teacherDataError == nil {

          //Load teacher data from FB
          //check for snapshot is not nil
          if let teacherDataSnapshot = teacherDataSnapshot {

            do {
              //Set local variable to teacher data
              let teacherData: TeacherModel = try teacherDataSnapshot.data(as: TeacherModel.self)

              //Append teacher to total contentmodel array of teacherdata
              tempTeacherAndCoursesInSchoolArr.append(teacherData)
            } catch {
              //Handle error
            }
          }

        } else {
          //TODO: Error in loading data, handle error
        }
      }
    }
    //Assign all teacher and their courses to contentmodel data
    self.teacherAndCoursesInSchool = tempTeacherAndCoursesInSchoolArr

  } else {
    //TODO: handle error in fetching teacher Data
  }
}

The method assigns data correctly to the tempTeacherAndCoursesInSchoolArr but the method doesn't assign the tempTeacherAndCoursesInSchoolArr to self.teacherAndCoursesInSchool in the last line. Why doesn't it do that?

Peter Friese
  • 6,709
  • 31
  • 43
FreddanF1
  • 11
  • 3
  • It seems hard to tell from the provided code, I'd suggest trying to separate concerns, for example, refactor that code and create a few smaller functions, each with one specific task to do, and then it will be easier to debug – cedricbahirwe Dec 11 '22 at 15:16
  • If you can provide the full function or class, It'll allow people to better help. – cedricbahirwe Dec 11 '22 at 15:17

1 Answers1

1

Most of Firebase's API calls are asynchronous: when you ask Firestore to fetch a document for you, it needs to communicate with the backend, and - even on a fast connection - that will take some time.

To deal with this, you can use two approaches: callbacks and async/await. Both work fine, but you might find that async/await is easier to read. If you're interested in the details, check out my blog post Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await | Peter Friese.

In your code snippet, you use a completion handler for handling the documents that getDocuments returns once the asynchronous call returns:

schoolColl.document("Teachers").collection(name).document("Teacher data").getDocument { teacherDataSnapshot, teacherDataError in
  // ...
}

However, the code for assigning tempTeacherAndCoursesInSchoolArr to self.teacherAndCoursesInSchool is outside of the completion handler, so it will be called before the completion handler is even called.

You can fix this in a couple of ways:

  1. Use Swift's async/await for fetching the data, and then use a Task group (see Paul's excellent article about how they work) to fetch all the teachers' data in parallel, and aggregate them once all the data has been received.

  2. You might also want to consider using a collection group query - it seems like your data is structure in a way that should make this possible.

Generally, iterating over the elements of a collection and performing Firestore queries for each of the elements is considered a bad practice as is drags down the performance of your app, since it will perform N+1 network requests when instead it could just send one single network request (using a collection group query).

Peter Friese
  • 6,709
  • 31
  • 43
  • that sounds like a great answer and exactly what I experience. I made a more "accurate" "step through" the code with breakpoints and thats exactly what I experience. That the code in the method first calls the lines that is outside the completion handler and then, after that, returned and made the code inside the completion handler. I solved it in an "ugly" way by moving my main array inside the completion and made an if-statement that assigned the temp-array to the main array on the last for-loop. – FreddanF1 Dec 12 '22 at 18:32
  • 1
    But I want to do it correct so I will read the articles you linked to. I will also dive down into the collection group-query and see if I can make my query in just one request. – FreddanF1 Dec 12 '22 at 18:32