UI won't update with background API Call

438 views Asked by At

So my goal is to have the UI update based on a network call depending if there is an error or not. Currently, my network call succeeds and the completion handler that updates the UI fails to show the correct UI despite the successful network call.

here is my current network API call:

func makeRefundRequest(refundMade: @escaping (_ done: Bool) -> Void) {
    getStripePaymentIntentID { (paymentid) in
        guard let id = paymentid,
              let url = URL(string: "https://us-central1-xxxxx-41f12.cloudfunctions.net/createRefund") else {
            refundMade(false)
            return
        }
        let json: [String: Any] = ["payment_intent": id]

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try? JSONSerialization.data(withJSONObject: json)
        
        let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
            if let response = response as? HTTPURLResponse,
                response.statusCode == 200,
                let data = data,
                let _ = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                refundMade(true)
            } else {
                if error != nil {
                    self?.showAlert(title: "Refund Request Error", message: "There was an error making the refund request. Please check your connection and try again.")
                }
                refundMade(false)
            }
        }

        task.resume()
        
    }
}

This is how it was before I decided to update the UI in a failure case, which made sense, because you wouldn't want to see the UI update as if things have succeeded when they really failed.

So I have the function that processes the refund and updates the UI, I will specifically add the completion handler where I'm having the issue:

let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in

        self.viewPurchaseButton.isHidden = true
        self.cancelPurchaseButton.isHidden = true
        self.refundLoading.alpha = 1
        self.refundLoading.startAnimating()
        
        self.getEventDocumentID { (id) in
            guard let id = id else { return }
            self.makeRefundRequest { (done) in
                if done == false {
                    DispatchQueue.main.async {
                        self.refundLoading.stopAnimating()
                        self.refundLoading.alpha = 0
                        self.viewPurchaseButton.isHidden = false
                        self.cancelPurchaseButton.isHidden = false
                    }
                    return
                } else {
                    let group = DispatchGroup()
                    self.db.collection("student_users/\(user.uid)/events_bought/\(id)/guests").getDocuments { (querySnapshot, error) in
                        guard error == nil else { return }
                        guard querySnapshot?.isEmpty == false else { return }
                        
                        for guest in querySnapshot!.documents {
                            let name = guest.documentID
                            let batch = self.db.batch()
                            let docRef = self.db.document("student_users/\(user.uid)/events_bought/\(id)/guests/\(name)")
                            batch.deleteDocument(docRef)
                            group.enter()
                            batch.commit { (err) in
                                guard err == nil else { return }
                                group.leave()
                            }
                        }
                    }

                    group.notify(queue: .main) {
                        DispatchQueue.main.asyncAfter(deadline: .now()+2) {
                            self.db.document("student_users/\(user.uid)/events_bought/\(id)").delete { (error) in
                                guard error == nil else { return }
                                
                                self.refundLoading.stopAnimating()
                                self.refundLoading.alpha = 0
                                self.ticketFormButton.isHidden = false
                                self.cancelPurchaseButton.isHidden = true
                                self.viewPurchaseButton.isHidden = true
                            }
                        }
                    }
                }
            }
        }
    }

So where it says if done == false, I basically want the UI to return to it's previous state so that the user can clearly see the refund was a failure. Now if I was to do that without the DispatchQueue.main.async call, the app would crash and show a thread error with the purple highlighting saying the " self.refundLoading.stopAnimating() can only be called on the main thread".

This was working fine before I implemented the extra async call in the if done == false {} block but nonetheless, I need this block of code. When I currently run it, the network call always succeeds, meaning the completion call would be true, hence it should show the correct UI. Instead, it shows the UI as if the network call failed and nothing gets deleted from Firestore. How can I solve this thread/Firestore issue?

UPDATE -> so I looked up on some more articles and decided to add a [weak self] into the makeRefundRequest call inside my alert action and also decided to remove the first call of refundMade(false) since calling it twice apparently causes an app crash.

func makeRefundRequest(refundMade: @escaping (_ done: Bool) -> Void ) {
    getStripePaymentIntentID { (paymentid) in
        guard let id = paymentid,
              let url = URL(string: "https://us-central1-xxxxx-41f12.cloudfunctions.net/createRefund") else {
            return
        }
        let json: [String: Any] = ["payment_intent": id]

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try? JSONSerialization.data(withJSONObject: json)
        
        let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
            if let response = response as? HTTPURLResponse,
                response.statusCode == 200,
                let data = data,
                let _ = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                refundMade(true)
            } else {
                if error != nil {
                    self?.showAlert(title: "Refund Request Error", message: "There was an error making the refund request. Please check your connection and try again.")
                }
                refundMade(false)
            }
        }

        task.resume()
    }
}

Here is the actual method itself being called in the alert action :

    func showFailureUI() {
    DispatchQueue.main.async {
        self.refundLoading.stopAnimating()
        self.refundLoading.alpha = 0
        self.viewPurchaseButton.isHidden = false
        self.cancelPurchaseButton.isHidden = false
    }
  
    
}

//Handles the action of cancelling the purchase
@IBAction func cancelPurchasePressed(_ sender: UIButton) {
    guard let nameOfEvent = selectedEventName else { return }
    guard let user = Auth.auth().currentUser else { return }


    let alertForCancel = UIAlertController(title: "Cancel Purchase", message: "Are you sure you want to cancel your purchase of a ticket to \(nameOfEvent)? You will receive full reimbursement of what you paid within 5 - 10 days.", preferredStyle: .alert)


    let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in

        self.viewPurchaseButton.isHidden = true
        self.cancelPurchaseButton.isHidden = true
        self.refundLoading.alpha = 1
        self.refundLoading.startAnimating()

        self.getEventDocumentID { (id) in
            guard let id = id else { return }
            self.makeRefundRequest { [weak self] (done) in
                if done == false {
                    self?.showFailureUI()
                } else {
                    let group = DispatchGroup()
                    self?.db.collection("student_users/\(user.uid)/events_bought/\(id)/guests").getDocuments { (querySnapshot, error) in
                        guard error == nil else { return }
                        guard querySnapshot?.isEmpty == false else { return }

                        for guest in querySnapshot!.documents {
                            let name = guest.documentID
                            let batch = self?.db.batch()
                            guard let docRef = self?.db.document("student_users/\(user.uid)/events_bought/\(id)/guests/\(name)") else { return }
                            batch?.deleteDocument(docRef)
                            group.enter()
                            batch?.commit { (err) in
                                guard err == nil else { return }
                                group.leave()
                            }
                        }
                    }

                    group.notify(queue: .main) {
                        DispatchQueue.main.asyncAfter(deadline: .now()+2) {
                            self?.db.document("student_users/\(user.uid)/events_bought/\(id)").delete { (error) in
                                guard error == nil else { return }

                                self?.refundLoading.stopAnimating()
                                self?.refundLoading.alpha = 0
                                self?.ticketFormButton.isHidden = false
                                self?.cancelPurchaseButton.isHidden = true
                                self?.viewPurchaseButton.isHidden = true
                            }
                        }
                    }
                }
            }
        }
    }
    alertForCancel.addAction(UIAlertAction(title: "Cancel", style: .cancel))
    alertForCancel.addAction(cancelPurchase)
    present(alertForCancel, animated: true, completion: nil)

}

Thinking this would work, when I run my app with some breakpoints and try to print the value of refundMade, this is the outcome:

error

I actually searched up that exact error and tried to take what I could from the StackOverflow posts but nothing helped. This is like my last major issue for now so if anybody can help out that would be appreciated, thanks.

1

There are 1 answers

0
Jintor On

This is generated with the application PAW

follow this tutorial for how to use it : https://www.youtube.com/watch?v=44APgBnapag

class MyRequestController {
    func sendRequest() {
        /* Configure session, choose between:
           * defaultSessionConfiguration
           * ephemeralSessionConfiguration
           * backgroundSessionConfigurationWithIdentifier:
         And set session-wide properties, such as: HTTPAdditionalHeaders,
         HTTPCookieAcceptPolicy, requestCachePolicy or timeoutIntervalForRequest.
         */
        let sessionConfig = URLSessionConfiguration.default

        /* Create session, and optionally set a URLSessionDelegate. */
        let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)

        /* Create the Request:
           Request (POST https://us-central1-xxxxx-41f12.cloudfunctions.net/createRefund)
         */

        guard var URL = URL(string: "https://us-central1-xxxxx-41f12.cloudfunctions.net/createRefund") else {return}
        var request = URLRequest(url: URL)
        request.httpMethod = "POST"

        // Headers

        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        // JSON Body

        let bodyObject: [String : Any] = [
            "payment_intent": "id"
        ]
        request.httpBody = try! JSONSerialization.data(withJSONObject: bodyObject, options: [])

        /* Start a new Task */
        let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
            if (error == nil) {
                // Success
                let statusCode = (response as! HTTPURLResponse).statusCode
                print("URL Session Task Succeeded: HTTP \(statusCode)")
            }
            else {
                // Failure
                print("URL Session Task Failed: %@", error!.localizedDescription);
            }
        })
        task.resume()
        session.finishTasksAndInvalidate()
    }
}

and later

MyRequestController().sendRequest....