CeleryMan
CeleryMan

Reputation: 11

SwiftUI Lists + Firebase Firestore, fetching data and then unfetching? (Bug)

I'm not sure what the problem is with my project. Basically, I have a pretty typical data structure: I'm using Firestore for a real-time doc based database, and I have a bunch of different collections and docs and fields. Just very simple, nothing crazy.

I have some model classes, and then some ViewModels to fetch data, filter, add documents to Firestore, and so on.

The app itself is almost 100% SwiftUI, and I'd like to keep it that way, just a challenge for my own development. I've hit a bit of a wall though.

In the app, I have a series of Views with NavigationView Lists that I pass small pieces of data to as you navigate. An example (this is for educational institutions) might be List of Schools > List of Classes > List of Students in Class > List of Grades for Student. Basically the perfect use for a navigation view and a bunch of lists.

The bug: When I move from one list to the next in the stack, I fetch the firestore data, which loads for a second (enough that the list populates), and then "unloads" back to nothing. Here is some code where this happens (I've cleaned it up to make it as simple as possible):

struct ClassListView: View {
    let schoolCode2: String
    let schoolName2: String
    @ObservedObject private var viewModelThree = ClassViewModel()
    
    var body: some View {
        VStack{
            List{
                if viewModelThree.classes.count > 0{
                    ForEach(self.viewModelThree.classes) { ownedClass in
                        NavigationLink(destination: StudentListView()){
                            Text(ownedClass.className)
                        }
                        
                    }
                } else {
                    Text("No Classes Found for \(schoolName2)")
                }
            }
        }
            .navigationBarTitle(schoolName2)
            .onAppear(){
                print("appeared")
                self.viewModelThree.fetchData(self.schoolCode2)
        } 
    }
}

So that's the ClassListView that I keep having issues with. For debugging, I added the else Text("No Classes Found") line and it does in fact show. So basically, view loads (this is all in a Nav view from a parent), it fetches the data, which is shown for a second (list populates) and then unloads that data for some reason, leaving me with just the "No classes found".

For more context, here is the code for the ClassViewModel (maybe that's where I'm going wrong?):

struct Classes: Identifiable, Codable {
    @DocumentID var id: String? = UUID().uuidString
    var schoolCode: String
    var className: String
}

enum ClassesCodingKeys: String, CodingKey {
    case id
    case schoolCode
    case className
}

class ClassViewModel: ObservableObject {
    @Published var classes = [Classes]()
    private var db = Firestore.firestore()
    
    func fetchData(_ schoolCode: String) {

        db.collection("testClasses")
            .order(by: "className")
            .whereField("schoolCode", isEqualTo: schoolCode)
            .addSnapshotListener{ (querySnapshot, error) in
                guard let documents = querySnapshot?.documents else {
                    print("no docs found")
                    return
                }
                self.classes = documents.compactMap{ queryDocumentSnapshot -> Classes? in
                    return try? queryDocumentSnapshot.data(as: Classes.self)
                }
        }
    }
    
    func addClass(currentClass: Classes){
        do {
            let _ = try db.collection("testClasses").addDocument(from: currentClass)
        }
        catch {
            print(error)
        }
    }
    
}

Most relevant bit is the fetchData() function above.

Maybe the problem is in the view BEFORE this (the parent view?). Here it is:

struct SchoolUserListView: View {
    @State private var userId: String?
    @EnvironmentObject var session: SessionStore
    @ObservedObject private var viewModel = UserTeacherViewModel()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State private var counter = 0
    @State private var showingAddSchool: Bool = false
    @Environment(\.presentationMode) var presentationMode

    func getUser() {
        session.listen()
    }
    var body: some View {
        VStack{
            List{
                if viewModel.teachers.count > 0{
                    ForEach(viewModel.teachers) { ownedClass in
                        NavigationLink(destination: ClassListView(schoolName2: ownedClass.schoolName, schoolCode2: ownedClass.schoolCode)){
                            Text(ownedClass.schoolName)
                        }
                    }
                } else {
                    Button(action:{
                        self.presentationMode.wrappedValue.dismiss()
                    }){
                        Text("You have no schools, why not add one?")
                    }
                }
            }
            .navigationBarTitle("Your Schools")
             
        }
        .onAppear(){
            self.getUser()
            self.userId = self.session.session?.uid
        }

        .onReceive(timer){ time in
            if self.counter == 1 {
                self.timer.upstream.connect().cancel()
            } else {
                print("fetching")
                self.viewModel.fetchData(self.userId!)
            }
            self.counter += 1
        }
        
    }
}

And the FURTHER Parent to that View (and in fact the starting view of the app):

struct StartingView: View {

    @EnvironmentObject var session: SessionStore
    
    func getUser() {
        session.listen()
    }
    var body: some View {
        NavigationView{
            VStack{
                Text("Welcome!")
                Text("Select an option below")
                Group{
                NavigationLink(destination:
                    SchoolUserListView()
                ){
                    Text("Your Schools")
                }
                NavigationLink(destination:
                    SchoolCodeAddView()
                ){
                    Text("Add a School")
                }
                Spacer()
                Button(action:{
                    self.session.signOut()
                }){
                    Text("Sign Out")
                }
                

            }
            }
        }
        .onAppear(){
            self.getUser()
        }
    }
    
}

I know that is a lot, but just so everyone has the order of parents:

StartingView > SchoolUserListView > ClassListView(BUG occurs here)

By the way, SchoolUserListView uses a fetchData method just like ClassListView(where the bug is) and outputs it to a foreach list, same thing, but NO problems? I'm just not sure what the issue is, and any help is greatly appreciated!

Here is a video of the issue: https://i.sstatic.net/IoGdL.jpg

Upvotes: 0

Views: 645

Answers (1)

CeleryMan
CeleryMan

Reputation: 11

Fixed it! The issue was the EnvironmentObject for presentationmode being passed in the navigation view. That seems to cause a LOT of undesired behavior. Be careful passing presentationMode as it seems to cause data to reload (because it is being updated).

Upvotes: 1

Related Questions