как исправить зависание приложения после вызова семафора

Я читал этот вопрос о зависаниях приложений и семафорах, и я попытался реализовать ответ в своем коде, но он все еще зависает в моем приложении, несмотря на вызов пользовательского интерфейса в основном потоке. Моя цель - предотвратить зависание приложения после вызова всех записей и продолжить работу пользовательского интерфейса, как обычно.

Это предупреждающее действие, которое у меня есть в методе удаления до сих пор:

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

Таким образом, это на самом деле удаляет все чисто, без остаточных данных в базе данных Firestore, что я и хотел, чтобы это произошло все время, единственная проблема заключается в том, что приложение зависает. Я думал, что ответ на вопрос, который я дал выше, сработает в моем случае, но это не так.

Также следует отметить, что у меня были предложения по использованию облачных функций для решения этой проблемы, но в моем приложении есть два типа пользователей с разной логикой и синтаксисом в процессе удаления, поэтому я не мог просто использовать простой в Cloud Functions и очистить остатки. Даже если бы я мог, это была бы та же проблема, с которой я столкнулся здесь, но только на стороне сервера, пытаясь правильно упорядочить задачи, что, на мой взгляд, является повторяющимся и не самым разумным делом на данный момент.

Есть ли другие предложения по преодолению этой проблемы? Заранее спасибо.

ИЗМЕНИТЬ Поскольку семафоры не подходят , я прибег к следующему:

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

Это по-прежнему оставляет остаточные данные и не выполняет задачи по порядку. Есть другие предложения?

1 ответ

Позвольте мне дать вам несколько идей, потому что я считаю, что ваше решение должно включать некоторые или все из них. Во-первых, как работают группы диспетчеризации и как вы можете вложить их для выполнения блоков асинхронных задач по порядку:

      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
    }
}

Приведенный выше пример представляет собой основы того, как диспетчерская группа может работать на вас. Когда вы покидаете группу столько же раз, сколько входили в нее, вызывается обработчик завершения. В этом примере нет рекурсии и не проверяется, действительно ли все было удалено. Вот пример того, как вы могли бы добавить что-то из этого:

      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()
}

Это не дает прямого ответа на ваш вопрос, но должно помочь вам с задачей в целом. Вы также можете отказаться от некоторых циклов и удалений и сделать все это внутри пакетной операции Firestore, которая имеет собственный обработчик завершения. Есть много способов решить эту проблему, но я бы рассмотрел некоторые из них.