I don't understand the order that code executes when calling onAppear

112 views Asked by At

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?

1

There are 1 answers

2
Peter Friese On

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).