Search code examples
swiftfirebaseuitableviewgoogle-cloud-firestorensindexpath

firestore document deletes and crashes with an error in logic but deletes fine without logic


So my goal is to delete a firestore document if the condition is false with no errors. At first, I had this function for deleting firestore documents:

  override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    
   
    let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (deleted, view, completion) in
        let alert = UIAlertController(title: "Delete Event", message: "Are you sure you want to delete this event?", preferredStyle: .alert)
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (cancel) in
            self.dismiss(animated: true, completion: nil)
        }
        let deleteEvent = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            guard let user = Auth.auth().currentUser else { return }
            let documentid = self.documentsID[indexPath.row].docID
//                let eventName = self.events[indexPath.row].eventName
            let deleteIndex = client.index(withName: IndexName(rawValue: user.uid))
            
            deleteIndex.deleteObject(withID: ObjectID(rawValue: self.algoliaObjectID[indexPath.row].algoliaObjectID)) { (result) in
                if case .success(let response) = result {
                    print("Algolia document successfully deleted: \(response.wrapped)")
                }
            }

            
            self.db.document("school_users/\(user.uid)/events/\(documentid)").delete { (error) in
                guard error == nil else {
                    print("There was an error deleting the document.")
                    return
                }
                print("Deleted")
            }
            
            self.events.remove(at: indexPath.row)
            tableView.reloadData()
            
        }

        alert.addAction(cancelAction)
        alert.addAction(deleteEvent)
        self.present(alert, animated: true, completion: nil)
    }

    deleteAction.backgroundColor = UIColor.systemRed
    

    let config = UISwipeActionsConfiguration(actions: [deleteAction])
    config.performsFirstActionWithFullSwipe = false
    return config

}

So this deletes the cell fine and the document in cloud firestore as well. Now i came across an issue in within my app, if a school user wants to delete an event they created but some student users have already purchased this event, this event will delete yes, but if the student user who purchased the event tries to view the event they purchased, the values will be nil and the app will crash because the details of the event they purchased solely rely on the event created (this is not a concept I will not be changing, it's just how the app works).

EDITED CODE In order to fix this I decided to add some logic to the deletion process:

   override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    
    
    let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (deleted, view, completion) in
        let alert = UIAlertController(title: "Delete Event", message: "Are you sure you want to delete this event?", preferredStyle: .alert)
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (cancel) in
            self.dismiss(animated: true, completion: nil)
        }
        let deleteEvent = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            guard let user = Auth.auth().currentUser else { return }
            let documentid = self.documentsID[indexPath.row].docID
            let eventName = self.events[indexPath.row].eventName
            let deleteIndex = client.index(withName: IndexName(rawValue: user.uid))
            
            
            self.getTheSchoolsID { (id) in
                guard let id = id else { return }
                self.db.collection("student_users").whereField("school_id", isEqualTo: id).getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("Couldn't fetch the student users.")
                        return
                    }
                    for document in querySnapshot!.documents {
                        let userUUID = document.documentID
                        self.db.collection("student_users/\(userUUID)/events_bought").whereField("event_name", isEqualTo: eventName).getDocuments { (querySnapshotTwo, error) in
                            guard error == nil else {
                                print("Couldn't fetch if users are purchasing this event")
                                return
                            }
                            guard querySnapshotTwo?.isEmpty == true else {
                                self.showAlert(title: "Students Have Purchased This Event", message: "This event cannot be deleted until all students who have purchased this event have completely refunded their purchase of this event. Please be sure to make an announcement that this event will be deleted.")
                                return
                            }
                        }
                    }
                    
                    deleteIndex.deleteObject(withID: ObjectID(rawValue: self.algoliaObjectID[indexPath.row].algoliaObjectID)) { (result) in
                        if case .success(let response) = result {
                            print("Algolia document successfully deleted: \(response.wrapped)")
                        }
                    }
                    
                    
                    self.db.document("school_users/\(user.uid)/events/\(documentid)").delete { (error) in
                        guard error == nil else {
                            print("There was an error deleting the document.")
                            return
                        }
                        print("Deleted")
                    }
                    self.events.remove(at: indexPath.row)
                    tableView.reloadData()
                }
            }
        }
        
        alert.addAction(cancelAction)
        alert.addAction(deleteEvent)
        self.present(alert, animated: true, completion: nil)
    }
    
    deleteAction.backgroundColor = UIColor.systemRed
    
    
    let config = UISwipeActionsConfiguration(actions: [deleteAction])
    config.performsFirstActionWithFullSwipe = false
    return config
    
}

UPDATE So I completely quit out of the loop and then would move forward with the deletion process if the query was empty. Now I tested this by purchasing an event as a student user, and then trying to delete the same event as a school user. For some reason when I pressed the delete action in the first alert, the event deleted first and then the validation alert showed up right after but crashed with no errors. Yes, the crash went away which is great but the return method in my query doesn't actually return and break out of the deletion method, it deletes and then shows the error, which I don't get.

Any suggestions?


Solution

  • Ok, I think I found the problem. When you are going through each of the documents representing users that go to a particular school, you check for every one of these users whether they are attending the event that is trying to be deleted. The problem is, for each time you do this check to see if a user is attending the event, you also do one of the following (based on whether or not the querySnapshotTwo?.isEmpty is true for that user):

    1. If the user is attending the event, you display the alert saying that the event cannot be deleted (thus, this will happen for as many times as there are users that are attending that event--not just once), or:
    2. If the user is not attending the event, it performs the delete event operation (thus, this will also happen multiple times if multiple users are not attending the event).

    I'm not sure what exactly is causing the issue with the app crashing at self.events.remove(at: indexPath.row) due to a nil value. But I would start by making sure that you completely quit looping through all the user documents if you find that a user is attending the event and then show the alert. And then you only perform the delete operation if, after going through all the user documents, you find that no one is attending the event.

    Response to Update

    Awesome, what you've done is definitely an improvement. The reason it's not returning out of the delete method when you find a user attending the event (i.e., when guard querySnapshotTwo?.isEmpty == true evaluates to false) is because you are only returning inside the closure. Essentially, you are just returning out of the line: self.db.collection("student_users/\(userUUID)/events_bought").whereField("event_name", isEqualTo: eventName).getDocuments { (querySnapshotTwo, error) And then you will continue with the next document in querySnapshot!.documents.

    So even if every student at the school is attending the event, you will always finish the for loop and continue to delete the event anyways!

    The other important reason it does not work is because the closure that you pass into the getDocuments() call is run asynchronously. This means that each time you make the call, it will schedule the closure to be run at some random time in in the future, and then immediately return from the getDocuments() call and perform the next iteration of the for loop (likely before the closure has been completed).

    To fix this, I believe you just need to make 2 changes:

    1. Add a variable before the for document in querySnapshot!.documents to keep track of whether or not you found a student who is attending the event, and update this variable to true inside the closure if you find a student attending the event (in place of the current ineffective return statement).
    2. You will need to use a DispatchGroup so that you only perform the delete operation once every student has been asynchronously checked. Here's a brief tutorial on how to use Dispatch Groups. In your case:
      • Create the dispatch group before the for loop (write let group = DispatchGroup())
      • Call group.enter() on the first line inside the for loop
      • Call group.leave() on the last line inside the getDocuments() closure inside the for loop
      • Call the following right after the for loop:
    group.notify(queue: .main) {
      // deletion code goes here
    }