Home > database >  Update View Only After Aync Is Resolved with Completion Handler
Update View Only After Aync Is Resolved with Completion Handler

Time:10-05

I'm trying to update my view, only after the Async call is resolved. In the below code the arrayOfTodos.items comes in asynchronously a little after TodoListApp is rendered. The problem I'm having is that when onAppear runs, self.asyncTodoList.items is always empty since it hasn't received the values of the array yet from the network call. I'm stuck trying to figure out how to hold off on running onAppear until after the Promise is resolved, like with a completion handler?? And depending on the results of the network call, then modify the view. Thanks for any help! I've been stuck on this longer than I'll ever admit!

    struct ContentView: View {
        
        @StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
        
        var body: some View {
            TodoListApp(asyncTodoList: arrayOfTodos)
        }
    }

    struct TodoListApp: View {
        
        @ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
        @State private var showPopUp: Bool = false
        
        var body: some View {
            NavigationView {
                
                ZStack {
                    
                    VStack {
                        
                        Text("Top Area")
                        Text("List Area")
                        
                    }
                    
                    if self.showPopUp == true {
                        VStack {
                            Text("THIS IS MY POPUP!")
                            Text("No Items Added Yet")
                        }.frame(width: 300, height: 400)
                    }
                    
                }.onAppear {
                    let arrayItems = self.asyncTodoList

                    if arrayItems.items.isEmpty {
                        self.showPopUp = true
                    }
                    
                    /*HERE! arrayItems.items.isEmpty is ALWAYS empty when onAppear 
                    runs since it's asynchronous.  What I'm trying to do is only
                    show the popup if the array is empty after the promise is
                    resolved.
                    What is happening is even if array resolved with multiple todos,
                    the popup is still showing because it was initially empty on
                    first run. */
                    
                }
                
            }
        }
    }

    class AsyncGetTodosNetworkCall: ObservableObject {
        
        @AppStorage(DBUser.userID) var currentUserId: String?
        private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
        @Published var items = [TodoItem]()
        
        func fetchTodos(toDetach: Bool) {
            
            guard let userID = currentUserId else {
                return
            }
            
            let userDoc = REF_USERS.document(String(userID))
                .collection(DBCOLLECTION.todos)
                .addSnapshotListener({ (querySnapshot, error) in
                    
                    guard let documents = querySnapshot?.documents else {
                        print("No Documents Found")
                        return
                    }
                    
                    self.items = documents.map { document -> TodoItem in
                        
                        let todoID = document.documentID
                        let todoName = document.get(ToDo.todoName) as? String ?? ""
                        let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false
                        
                        return TodoItem(
                            id: todoID,
                            todoName: todoName,
                            todoCompleted: todoCompleted
                        )
                    }
                })
            if toDetach == true {
                userDoc.remove()
            }
        }
    }

CodePudding user response:

While preparing my question, i found my own answer. Here it is in case someone down the road might run into the same issue.

    struct ContentView: View {

        @StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
        
        @State var hasNoTodos: Bool = false

        func getData() {
            self.arrayOfTodos.fetchTodos(toDetach: false) { noTodos in
                
                if noTodos {
                    self.hasNoTodos = true
                }
            }
        }

        func removeListeners() {
            self.arrayOfTodos.fetchTodos(toDetach: true)
        }

        var body: some View {

            TabView {
                TodoListApp(asyncTodoList: arrayOfTodos, hasNoTodos : self.$hasNoTodos)
            }.onAppear(perform: {
                self.getData()
            }).onDisappear(perform: {
                self.removeListeners()
            })
        }
    }

    struct TodoListApp: View {

        @ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
        @Binding var hasNoTodos: Bool
        @State private var hidePopUp: Bool = false

        var body: some View {
            NavigationView {

                ZStack {

                    VStack {

                        Text("Top Area")
                        
                        ScrollView {
                            LazyVStack {
                                
                                ForEach(asyncTodoList.items) { item in
                                    
                                    HStack {
                                        Text("\(item.name)")
                                        Spacer()
                                        Text("Value")
                                    }
                                    
                                }
                            }
                        }

                    }
                    
                    if self.hasNoTodos == true {
                        if self.hidePopUp == false {
                            
                            VStack {
                                Text("THIS IS MY POPUP!")
                                Text("No Items Added Yet")
                            }.frame(width: 300, height: 400)
                            
                        }
                    }

                }

            }
        }
    }
    class AsyncGetTodosNetworkCall: ObservableObject {

        @AppStorage(DBUser.userID) var currentUserId: String?
        private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
        @Published var items = [TodoItem]()

        func fetchTodos(toDetach: Bool, handler: @escaping (_ noTodos: Bool) -> ()) {

            guard let userID = currentUserId else {
                handler(true)
                return
            }

            let userDoc = REF_USERS.document(String(userID))
                .collection(DBCOLLECTION.todos)
                .addSnapshotListener({ (querySnapshot, error) in

                    guard let documents = querySnapshot?.documents else {
                        print("No Documents Found")
                        handler(true)
                        return
                    }

                    self.items = documents.map { document -> TodoItem in

                        let todoID = document.documentID
                        let todoName = document.get(ToDo.todoName) as? String ?? ""
                        let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false

                        return TodoItem(
                            id: todoID,
                            todoName: todoName,
                            todoCompleted: todoCompleted
                        )
                    }
                    handler(false)
                })
            if toDetach == true {
                userDoc.remove()
            }
        }
    }
  • Related