how to fix app freeze after dispatchsemaphore call

388 views Asked by At

I was reading up on this question about app freezes and semaphores and I tried to implement the answer into my code, but it still freezes my app despite calling the UI work on the main thread. My goal is to stop the app from freezing once all the entries are called and have the UI work continue like normal.

This is the alert action I have in the deletion method so far:

let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            let semaphore = DispatchSemaphore(value: 0)
            
            
            self.deleteButton.isHidden = true
            self.loadingToDelete.alpha = 1
            self.loadingToDelete.startAnimating()
            
            DispatchQueue.global(qos: .userInitiated).async {
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("The docs couldn't be retrieved for deletion.")
                        return
                    }
                    
                    guard querySnapshot?.isEmpty == false else {
                        print("The user being deleted has no events purchased.")
                        return
                    }
                    
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        
                        self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
                            guard querySnap?.isEmpty == false else {
                                print("The user being deleted has no guests with his purchases.")
                                return
                            }
                            
                            for doc in querySnap!.documents {
                                let guest = doc.documentID
                                self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
                                    guard error == nil else {
                                        print("Error deleting guests while deleting user.")
                                        return
                                    }
                                    print("Guests deleted while deleting user!")
                                    semaphore.signal()
                                }
                                semaphore.wait()
                            }
                        }
                    }
                }
    
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("There was an error retrieving docs for user deletion.")
                        return
                    }
                    guard querySnapshot?.isEmpty == false else {
                        return
                    }
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        
                        self.db.document("student_users/\(user.uid)/events_bought/\(docID)").delete { (err) in
                            guard err == nil else {
                                print("There was an error deleting the the purchased events for the user being deleted.")
                                return
                            }
                            print("Purchases have been deleted for deleted user!")
                            semaphore.signal()
                        }
                        semaphore.wait()
                    }
                }

                
                self.db.document("student_users/\(user.uid)").delete(completion: { (error) in
                    
                    guard error == nil else {
                        print("There was an error deleting the user document.")
                        return
                    }
                    print("User doc deleted!")
                    semaphore.signal()
                })
                semaphore.wait()
                
                user.delete(completion: { (error) in
                    guard error == nil else {
                        print("There was an error deleting user from the system.")
                        return
                    }
                    print("User Deleted.")
                    semaphore.signal()
                })
                semaphore.wait()
                
                DispatchQueue.main.async {
                    self.loadingToDelete.stopAnimating()
                    self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
                }
            }
        }

So this actually deletes everything cleanly with no residual data in the Firestore database, which is what I wanted to happen all along, the only issue is that the app freezes. I thought that the answer in the question I linked above would work in my case, but it didn't.

Also to mention, I've had suggestions of using Cloud Functions for this issue but my app has two types of users with different logic and syntax in the deletion process so I couldn't just use a simple auth().onDelete() in Cloud Functions and clean up residue. Even if I could, it would be the same issue I'm facing here but just on the server side, trying to order the tasks correctly, which in my opinion is repetitive and not the most sensible thing to do at this point.

Any other suggestions to overcome this issue? Thanks in advance.

EDIT Since semaphores are not the way to go, I resorted to this :

let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            
            
            self.deleteButton.isHidden = true
            self.loadingToDelete.alpha = 1
            self.loadingToDelete.startAnimating()
            
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("The docs couldn't be retrieved for deletion.")
                        return
                    }
                    
                    guard querySnapshot?.isEmpty == false else {
                        print("The user being deleted has no events purchased.")
                        return
                    }
                    
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        
                        self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
                            guard querySnap?.isEmpty == false else {
                                print("The user being deleted has no guests with his purchases.")
                                return
                            }
                            let group = DispatchGroup()
                            for doc in querySnap!.documents {
                                let guest = doc.documentID
                                group.enter()
                                self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
                                    guard error == nil else {
                                        print("Error deleting guests while deleting user.")
                                        return
                                    }
                                    print("Guests deleted while deleting user!")
                                    group.leave()
                                }
                            }
                        }
                    }
                }
    
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("There was an error retrieving docs for user deletion.")
                        return
                    }
                    guard querySnapshot?.isEmpty == false else {
                        return
                    }
                    let group = DispatchGroup()
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        group.enter()
                        self.db.document("student_users/\(user.uid)/events_bought/\(docID)").delete { (err) in
                            guard err == nil else {
                                print("There was an error deleting the the purchased events for the user being deleted.")
                                return
                            }
                            print("Purchases have been deleted for deleted user!")
                            group.leave()
                        }
                    }
                }
            
            self.db.collection("student_users").whereField("userID", isEqualTo: user.uid).getDocuments { (querySnapshot, error) in
                guard error == nil else {
                    print("There was an error deleting the user document.")
                    return
                }
                guard querySnapshot?.isEmpty == false else {
                    return
                }
                let group = DispatchGroup()
                for document in querySnapshot!.documents {
                    let docID = document.documentID
                    group.enter()
                    self.db.document("student_users/\(docID)").delete { (err) in
                        guard err == nil else {
                            return
                        }
                        print("User doc deleted!")
                        group.leave()
                    }
                }
            }
            let group = DispatchGroup()
            group.enter()
                user.delete(completion: { (error) in
                    guard error == nil else {
                        print("There was an error deleting user from the system.")
                        return
                    }
                    print("User Deleted.")
                    group.leave()
                })
                
            group.notify(queue: .main) {
                
                self.loadingToDelete.stopAnimating()
                self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
            }
         
        }

This still leaves residual data and does not execute the tasks in order. Any other suggestions?

1

There are 1 answers

6
trndjc On

Let me give you some ideas because I think your solution should incorporate some or all of these. First is how dispatch groups work and how you can nest them to execute blocks of async tasks in order:

func deleteUser(completion: @escaping (_ done: Bool) -> Void) {
    // put UI into loading state
    
    db.collection("someCollection").getDocuments { (snapshot, error) in
        if let snapshot = snapshot {
            if snapshot.isEmpty {
                completion(true) // no errors, nothing to delete
            } else {
                let dispatchGroup = DispatchGroup() // instantiate the group outside the loop
                var hasErrors = false
                
                for doc in snapshot.documents {
                    dispatchGroup.enter() // enter on every iteration
                    
                    db.document("someDocument").delete { (error) in
                        if let error = error {
                            print(error)
                            hasErrors = true
                        }
                        
                        dispatchGroup.leave() // leave on every iteration regardless of outcome
                    }
                }
                
                dispatchGroup.notify(queue: .main) {
                    if hasErrors {
                        completion(false) // failed to delete
                    } else {
                        // execute next task and repeat
                    }
                }
            }
        } else {
            if let error = error {
                print(error)
                completion(false) // failed to delete
            }
        }
    }
}

deleteUser { (done) in
    if done {
        // segue to next view controller
    } else {
        // retry or alert user
    }
}

The example above is the basics of how dispatch group can work for you. When you leave the group the same number of times you've entered it, the completion handler is called. This example does not have any recursion and doesn't check if everything was actually deleted. Here is an example of how you could add some of that:

func deleteUser(completion: @escaping (_ done: Bool) -> Void) {
    var retries = 0
    
    func task() {
        db.collection("someCollection").getDocuments { (snapshot, error) in
            if let snapshot = snapshot {
                if snapshot.isEmpty {
                    completion(true) // done, nothing left to delete
                } else {
                    // delete the documents using a dispatch group or a Firestore batch delete
                    
                    task() // call task again when this finishes
                           // because this function only exits when there is nothing left to delete
                           // or there have been too many failed attempts
                }
            } else {
                if let error = error {
                    print(error)
                }
                retries += 1 // increment retries
                run() // retry
            }
        }
    }

    func run() {
        guard retries < 5 else {
            completion(false) // 5 failed attempts, exit function
            return
        }
        if retries == 0 {
            task()
        } else { // the more failures, the longer we wait until retrying
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(retries)) {
                task()
            }
        }
    }
    
    run()
}

This doesn't answer your question directly but it should help you with the task overall. You can also forego some of the looping and deleting and do it all inside a Firestore batch operation, which comes with its own completion handler. There are lots of ways to tackle this but these are some things I'd consider.