Swift iOS -how to cancel DispatchGroup() from managing a loop - ios

I loop through several Urls, convert them to Data, then send the data to Firebase Storage and then when everything is done send the gathered info to Firebase Database
I use DispatchGroup()'s .enter() to start the loop and once I send the data to Storage and obtain a value url string absoluteString I use .leave() to start the next iteration.
What I realized is that during the loop, there are several points where errors can occur:
once inside the UrlSession
once inside Storage's .putData function
once inside Storage's .downloadURL(completion:... completion handler
and again if the final downloadURL's ?.absoluteString is nil
If I get an error at any of those points I show an alert function showAlert() that shows the alert and cancel's the UrlSession with session.invalidateAndCancel(). I cancel everything because I want the user to start all over again.
Since the DispatchGroup() is left hanging at .enter(), how do I cancel the DispatchGroup() to stop the loop?
var urls = [URL]()
var picUUID = UUID().uuidString
var dict = [String:Any]()
let session = URLSession.shared
let myGroup = DispatchGroup()
var count = 0
for url in urls{
myGroup.enter()
session.dataTask(with: url!, completionHandler: {
(data, response, error) in
if error != nil {
self.showAlert() // 1st point of error
return
}
DispatchQueue.main.async{
self.sendDataToStorage("\(self.picUUID)_\(self.count).jpg", picData: data)
self.count += 1
}
}).resume()
myGroup.notify(queue: .global(qos: .background) {
self.sendDataFromDictToFirebaseDatabase()
self.count = 0
self.session.invalidateAndCancel()
}
}
func sendDataToStorage(_ picId: String, picData: Data?){
dict.updateValue(picId, forKey:"picId_\(count)")
let picRef = storageRoot.child("pics")
picRef.putData(picData!, metadata: nil, completion: { (metadata, error) in
if error != nil{
self.showAlert() // 2nd point of error
return
}
picRef?.downloadURL(completion: { (url, error) in
if error != nil{
self.showAlert() // 3rd point of error
return
}
if let picUrl = url?.absoluteString{
self.dict.updateValue(picUrl, forKey:"picUrl_\(count)")
self.myGroup.leave() //only leave the group if a Url string was obtained
}else{
self.showAlert() // 4th point of error
}
})
})
}
func showAlert(){
// the DispatchGroup() should get cancelled here
session.invalidateAndCancel()
count = 0
UIAlertController...
}
func sendDataFromDictToFirebaseDatabase(){
}

In the comments below the question #rmaddy said "You need to call leave whether is succeeds or not". I did that but the loop still ran and sendDataFromDictToFirebaseDatabase() still fired event hough there was an error.
The only work around I could find was to put the loop inside a function with a completion handler and use a bool to decide wether or not the sendDataFromDictToFirebaseDatabase() should fire:
var urls = [URL]()
var picUUID = UUID().uuidString
var dict = [String:Any]()
let session = URLSession.shared
let myGroup = DispatchGroup()
var count = 0
var wasThereAnError = false // use this bool to find out if there was an error at any of the error points
func loopUrls(_ urls: [URL?], completion: #escaping ()->()){
for url in urls{
myGroup.enter()
session.dataTask(with: url!, completionHandler: {
(data, response, error) in
if error != nil {
self.showAlert() // 1st point of error. If there is an error set wasThereAnError = true
return
}
DispatchQueue.main.async{
self.sendDataToStorage("\(self.picUUID)_\(self.count).jpg", picData: data)
self.count += 1
}
}).resume()
myGroup.notify(queue: .global(qos: .background) {
completion()
}
}
}
// will run in completion handler
func loopWasSuccessful(){
// after the loop finished this only runs if there wasn't an error
if wasThereAnError == false {
sendDataFromDictToFirebaseDatabase()
count = 0
session.invalidateAndCancel()
}
}
func sendDataToStorage(_ picId: String, picData: Data?){
dict.updateValue(picId, forKey:"picId_\(count)")
let picRef = storageRoot.child("pics")
picRef.putData(picData!, metadata: nil, completion: { (metadata, error) in
if error != nil{
self.showAlert() // 2nd point of error. If there is an error set wasThereAnError = true
return
}
picRef?.downloadURL(completion: { (url, error) in
if error != nil{
self.showAlert() // 3rd point of error. If there is an error set wasThereAnError = true
return
}
if let picUrl = url?.absoluteString{
self.dict.updateValue(picUrl, forKey:"picUrl_\(count)")
self.myGroup.leave() // leave group here if all good on this iteration
}else{
self.showAlert() // 4th point of error. If there is an error set wasThereAnError = true
}
})
})
}
func showAlert(){
wasThereAnError = true // since there was an error set this to true
myGroup.leave() // even though there is an error still leave the group
session.invalidateAndCancel()
count = 0
UIAlertController...
}
func sendDataFromDictToFirebaseDatabase(){
}
And to use it:
#IBAction fileprivate func postButtonPressed(_ sender: UIButton) {
wasThereAnError = false // set this back to false because if there was an error it was never reset
loopUrls(urls, completion: loopWasSuccessful)
}

Related

dispatchgroup executes task in different order in testflight compared to simulator

So my goal is to have congruent functionality both on the iOS simulator in Xcode and as well as a physical device on TestFlight. So currently, I have a function that handles refunds in my app. On the simulator the function runs perfectly fine in the order I expect it to, but the print statements execute in the wrong order which I'm assuming is the reason for misbehaviour on TestFlight simulations.
Here is the method:
#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.makeRefundRequest()
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
let group = DispatchGroup()
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The guests couldn't be fetched.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user did not bring any guests.")
return
}
for guest in querySnapshot!.documents {
let name = guest.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
guard error == nil else {
print("The guests couldn't be deleted.")
return
}
print("Guests deleted with purchase refund.")
group.leave()
}
}
}
group.notify(queue: .main) {
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
guard error == nil else {
print("Error trying to delete the purchased event.")
return
}
print("The purchased event was succesfully removed from the database!")
}
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)
}
Basically what I have going on is a simple refund request being made to Stripe and a second after I have an asyncAfter code block with some database cleanup in it. I have to do the asyncAfter or else the refund request gets beat out by the other async tasks by speed.
So I took my knowledge of DispatchGroups and decided to implement it since I have an async task in a for loop that I need to be completed before every other task. So I expected this to work fine, despite the order of the print statements being incorrect, but when I ran the exact block of code on my phone via TestFlight, I made a refund and the cell was still showing up in the tableview, meaning the document wasn't deleted from the database properly.
I've been having some terrifying experience recently with DispatchGroups and TestFlight and I just honestly hope to fix all this and have these problems come to an end temporarily. Any suggestions on how I can fix this method to prevent incorrect order on TestFlight?
UPDATE Decided to use a completion handler instead to do the same functionality:
func makeRefundRequest(refundMade: #escaping ((Bool) -> ())) {
let backendURLForRefund = "https://us-central1-xxxxxx-41f12.cloudfunctions.net/createRefund"
getStripePaymentIntentID { (paymentid) in
guard let id = paymentid else { return }
let url = URL(string: backendURLForRefund)!
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
guard let taskError = error?.localizedDescription else { return }
guard let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let _ = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
self?.showAlert(title: "Refund Request Error", message: "There was an error making the refund request. \(taskError)")
refundMade(false)
return
}
}
task.resume()
refundMade(true)
}
}
And then I just slapped this method in the actual refund process method itself:
#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.makeRefundRequest { (response) in
if response == false {
return
} else {
let group = DispatchGroup()
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The guests couldn't be fetched.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user did not bring any guests.")
return
}
for guest in querySnapshot!.documents {
let name = guest.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
guard error == nil else {
print("The guests couldn't be deleted.")
return
}
print("Guests deleted with purchase refund.")
group.leave()
}
}
}
group.notify(queue: .main) {
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
guard error == nil else {
print("Error trying to delete the purchased event.")
return
}
print("The purchased event was succesfully removed from the database!")
}
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)
}
This actually does not work fine, yes the refund goes through on Stripe and the database is cleaned up for 3 minutes, but the print statements print in incorrect order and also the document magically reappears in the Firestore database 3 minutes after physically seeing it be deleted, how can I prevent this and make sure they print in correct order and execute in correct order to work properly on TestFlight? Is this an issue in my DispatchGroup implementation? Or is it something completely different?
I think it's important that no matter what you end up doing you should learn how to do everything anyway. I would advise against this approach below but it's what you originally started so let's finish it regardless, so you know how dispatch grouping works. Once you've gotten a handle on this, refine it by replacing the dispatch group with a Firestore transaction or batch operation. The point of the transaction or batch operation is so all of the documents are deleted atomically, meaning they all go or none go. This simplifies things greatly and they are very basic! And the documentation for them is very clear.
The final thing I would suggest is perhaps integrating some recursion, meaning that if something fails it can retry automatically. Recursive functions are also very basic so just learn how to write one in Playground first and then apply it here. Just take it one step at a time and you'll get it down within a day or two. But this is the first step so carefully read what I wrote and understand why I did what I did.
func makeRefundRequest(refundMade: #escaping (_ done: Bool) -> Void) {
getStripePaymentIntentID { (paymentid) in
guard let id = paymentid,
let url = URL(string: "https://us-central1-xxxxxx-41f12.cloudfunctions.net/createRefund") else {
refundMade(false) // always call completion when you exit this function
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) // completion: true
} else {
if let error = error {
print(error)
}
refundMade(false) // completion: false
}
}
task.resume()
// do not call the completion of this function here because you just made a network call
// so call the completion of this function in that call's completion handler
}
}
#IBAction func cancelPurchasePressed(_ sender: UIButton) {
guard let nameOfEvent = selectedEventName,
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
// put the UI in a loading state
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
self.refundLoading.alpha = 1
self.refundLoading.startAnimating()
self.makeRefundRequest { (done) in
if done {
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (snapshot, error) in
guard let snapshot = snapshot,
!snapshot.isEmpty else {
if let error = error {
print(error)
}
return
}
let group = DispatchGroup() // instatiate the dispatch group outside the loop
for doc in snapshot.documents {
group.enter() // enter on each loop iteration
let name = doc.documentID
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
if let error = error {
print(error)
}
group.leave() // leave no matter what the outcome, error or not
// what do you do when this document didn't delete?
// by doing all your deleting in a transaction or batch
// you can ensure that they all delete or none delete
}
}
group.notify(queue: .main) { // done with loop, make final network call
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
if let error = error {
print(error)
}
// put the UI back to normal state
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
} else { // refund was not made, put the UI back into normal state
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)
}

iOS - How do I wait for the code to finish before going to the next view controller?

I'm very new to coding and I'm stuck on what to do. I'm trying to get the user's geocoordinates from an address, use the coordinates to figure out some values then go to a different view controller where some code will be run to display the values that I figured out. The problem is it finds the users coordinates, Then goes to the next view controller where it doesn't have the calculated data needed to display it then tries to calculate the values needed from the first controller. How do I get this code to run in order?
My Code
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
} else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
}
}
performSegue(withIdentifier: "TimeNavigation", sender: self) // Goes here second
}
The Function
func getCoordinate(addressString: String, completionHandler: #escaping (CLLocationCoordinate2D, NSError?) -> Void ) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
getData Function
func getData(savedLat: String, savedLong: String) {
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
}
}.resume()
}
Write it inside the closure because your performSegue execute before the closure result ... so write it inside the closure but on main thread
Update you getData function
typealias CompletionHandler = (_ success:Bool) -> Void
func getData(savedLat:String,savedLong:String, completionBlock:#escaping CompletionHandler){
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completionBlock(false)
return
}
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
completionBlock(true)
}
}.resume()
}
And then your BSearch method
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: "AdressInput") { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: "user_lat", savedLong: "user_long", completionBlock: {[weak self] success in
DispatchQueue.main.async {
self?.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}) // Lastly goes here
}
}
}
You are calling your performSegue outside the scope of getCordinate function, which is why its getting called on click of the button and not waiting for the completion handler to finish.
Just move it inside and it will work fine.
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
DispatchQueue.main.async { //when performing UI related task, it should be on main thread
self.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}
}
}

How can I run 2 asynchrones code in one function and escape them?

I want to run 2 pieces of asynchronous code in one function and escape them. I want first to download the Reciter information and then download with these information the images that is associated with the Reciter. I'm using Firestore. I tried to work with DispatchQueue and DispatchGroup but I couldn't figure something out. I hope someone can help me :)
func getReciters(completion: #escaping (Bool) -> Void) {
var reciters = [Reciter]()
self.BASE_URL.collection(REF_RECITERS).getDocuments { (snapchot, error) in
if let error = error {
debugPrint(error)
completion(false)
// TODO ADD UIALTERCONTROLLER MESSAGE
return
}
guard let snapchot = snapchot else { debugPrint("NO SNAPSHOT"); completion(false); return }
for reciter in snapchot.documents {
let data = reciter.data()
let reciterName = data[REF_RECITER_NAME] as? String ?? "ERROR"
let numberOfSurahs = data[REF_NUMBER_OF_SURAHS] as? Int ?? 0
// **HERE I WANT TO DOWNLOAD THE IMAGES**
self.downloadImage(forDocumentID: reciter.documentID, completion: { (image) in
let reciter = Reciter(name: reciterName, image: nil, surahCount: numberOfSurahs, documentID: reciter.documentID)
reciters.append(reciter)
})
}
}
UserDefaults.standard.saveReciters(reciters)
completion(true)
}
You need DispatchGroup.
In the scope of the function declare an instance of DispatchGroup.
In the loop before the asynchronous block call enter.
In the loop inside the completion handler of the asynchronous block call leave.
After the loop call notify, the closure will be executed after all asynchronous tasks have finished.
func getReciters(completion: #escaping (Bool) -> Void) {
var reciters = [Reciter]()
self.BASE_URL.collection(REF_RECITERS).getDocuments { (snapchot, error) in
if let error = error {
debugPrint(error)
completion(false)
// TODO ADD UIALTERCONTROLLER MESSAGE
return
}
guard let snapchot = snapchot else { debugPrint("NO SNAPSHOT"); completion(false); return }
let group = DispatchGroup()
for reciter in snapchot.documents {
let data = reciter.data()
let reciterName = data[REF_RECITER_NAME] as? String ?? "ERROR"
let numberOfSurahs = data[REF_NUMBER_OF_SURAHS] as? Int ?? 0
group.enter()
self.downloadImage(forDocumentID: reciter.documentID, completion: { (image) in
let reciter = Reciter(name: reciterName, image: nil, surahCount: numberOfSurahs, documentID: reciter.documentID)
reciters.append(reciter)
group.leave()
})
}
group.notify(queue: .main) {
UserDefaults.standard.saveReciters(reciters)
completion(true)
}
}
}

App crashes when clicked on back button on a nav controller while function is executing

On a slow connection when I click on back button on a VC it crashes while accessing navigation controller. VC is already deallocated but setNavBarTitle is executed after going back to another view. I understand that function is executing while VC is already deallocated but Im not sure what's the best way to handle such scenario?
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
fetchProfile(clientId: clientId) { (result, error) in
if result?.data != nil {
if (result?.success)! {
self.clientProfile = result!.data!
// Avatar
let clientImageView = UIImageView()
if let url = URL(string: result!.data!.pic_url!) {
clientImageView.image = UIImage(named: "System/defaultAvatar")
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
// WARNING: UIImageView.image must be used from main thread only
clientImageView.image = UIImage(data: data)
self.setNavBarTitle(image: clientImageView.image!)
}
task.resume()
}
}
}
}
}
}
private func setNavBarTitle(image: UIImage) {
// Crashes here -> Thread 11: Fatal error: Unexpectedly found nil while unwrapping an Optional value
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
}
You can try using [weak self] in your method callback, so even though the method is called after the VC deallocated it wont cause a crash, since self wont be existing:
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
fetchProfile(clientId: clientId) { [weak self] (result, error) in
if result?.data != nil {
if (result?.success)! {
self?.clientProfile = result!.data!
// Avatar
let clientImageView = UIImageView()
if let url = URL(string: result!.data!.pic_url!) {
clientImageView.image = UIImage(named: "System/defaultAvatar")
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async {
clientImageView.image = UIImage(data: data)
self?.setNavBarTitle(image: clientImageView.image!)
}
}
task.resume()
}
}
}
}
}
}
private func setNavBarTitle(image: UIImage) {
// Crashes here -> Thread 11: Fatal error: Unexpectedly found nil while unwrapping an Optional value
guard let navigationBarHeight: CGFloat = self.navigationController?.navigationBar.frame.height else {
return
}
}

Segue after firebase download/upload completes

Im trying to run some code after firebase has finished its downloading/uploading (eg segue or refresh)
for eg
I have x3 save functions which all have code to update both storage and database of certain data (eg text and images)
save1()
save2()
save3()
when an IBAction is performed I would like these functions to run, and on completion if their is no error to perform another function on completion (segue or refresh)
these 3 save function currently work within the IBAction
#IBAction func saveTap(_ sender: Any) {
save1()
save2()
save3()
}
Save function as follows:
(I check if image has been changed, then upload process begins)
func save1(){
if image1.image == nil {
let gender = userGender.text
self.databaseRef.child("users").child(gender!).child(Auth.auth().currentUser!.uid).child("images").child("imageOne").removeValue { (error, ref) in
if error != nil {
print(error!)}
}
let imageRef = self.storage.child(Auth.auth().currentUser!.uid).child("image1")
imageRef.delete { error in
if let error = error {
print(error)
} else {
print("Photo 1 image deleted")}
}
} else {
//Firebase child references
let profileImageRef = storage.child(Auth.auth().currentUser!.uid).child("image1")
let metaData = StorageMetadata()
metaData.contentType = "image1/jpeg"
//Firebase child references
//change uiimageview to uiimage for upload
guard let image = image1.image else
{return}
//change uiimageview to uiimage for upload
//Send to firebase storage
profileImageRef.putData(image.jpegData(compressionQuality: 0.1)!, metadata: metaData) { (data, error) in
if error == nil
{print("Photo 1 uploaded to storage")}
else
{print(error?.localizedDescription as Any)}}
//Send to firebase storage
//Update firebase database
profileImageRef.downloadURL(completion: { (url, error) in
if error != nil{
print(error!)
return}
if let profilePhotoUrl = url?.absoluteString{
let newValuesForProfile = profilePhotoUrl
let gender = self.userGender.text
self.databaseRef.child("users").child(gender!).child(Auth.auth().currentUser!.uid).child("images").child("imageOne").setValue(newValuesForProfile, withCompletionBlock: { (error, ref) in
if error != nil{
print(error!)
return}
print("Photo 1 updated in database")})}})
//Update firebase database
}
I need the uploads to complete before the segues are performed as the next view will be refreshing to the saved data that i'm trying to upload.
any help would be great, been at this for weeks now :( iv tried completion handlers but no luck as of yet.
thank you in advance
I think dispatchGroup fits with your case
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
save1 { dispatchGroup.leave() }
dispatchGroup.enter()
save2 { dispatchGroup.leave() }
dispatchGroup.notify(queue: .main) {
self.perFormSegue//////
}
//
// e.x structure
func save1(completion:#escaping()->()) {
firesCallBack {
if success {
completion()
}
}
}
You can check if the changes are saved with .childChanged somehow like this:
save1()
save2()
save3()
let ref: DatabaseReference!
ref = /YourDirectDatabaseReferenceForTheChangedNode/
ref.observe(.childChanged, with: {(snaphost) -> Void in
}
})
Maybe you should use UIActivityIndicatorView aswell to show something is going in the background.
#IBAction func saveTap(_ sender: Any) {
save1()
save2()
save3()
let activity: UIActivityIndicatorView = UIActivityIndicatorView()
activity.center = self.view.center
activity.hidesWhenStopped = true
activity.activityIndicatorViewStyle = .whiteLarge
self.view.addSubview(activity)
let ref: DatabaseReference!
ref = /YourDirectDatabaseReferenceForTheChangedNode/
ref.observe(.childChanged, with: {(snaphost) -> Void in
activity.stopAnimating()
if UIApplication.shared.isIgnoringInteractionEvents {
UIApplication.shared.endIgnoringInteractionEvents()
}
self.performSegue(withIdentifier: "backhome", sender: self)
}
})
}
Replace /YourDirectDatabaseReferenceForTheChangedNode/ with the correct child (what should change after save).

Resources