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).
Related
I currently have a for loop used to download images from urls stored in arrays. I keep having the issue of seeing the same image after every 2 or 3 new image fetches. Every time I fetch new images, I can see it's using different urls to download images but the images in the arrays are the same.
here is my code below:
func downloadImageDetailsFromFlickr(completion: #escaping (Bool, Error?) -> Void) {
self.imageLoading(is: true)
flickrClient.getImageDetailsFromFlickr(lat: latitude, lon: longitude) { (photo, error) in
if error == nil {
self.photoStore = []
for image in photo {
if image.url_m.count == 0 {
self.imageLabel.isHidden = false
self.imageLoading(is: false)
completion(false, nil)
} else {
self.photoStore.append(image.url_m)
print(self.photoStore)
completion(true, nil)
}
}
} else {
print(error?.localizedDescription ?? "Error in the fetch image block")
print("problem in downloading details.")
completion(false, error)
}
}
}
func downloadImage(completion: #escaping(Bool, Error?) -> Void ) {
let flickrImage = FlickrImage(context: self.dataController.viewContext)
for flickr in photoStore {
self.photoUrl = ""
self.photoUrl = flickr
}
flickrClient.getImage(imageUrl: photoUrl ?? "") { (data, error) in
if error == nil {
self.imageLoading(is: false)
flickrImage.photo = data
flickrImage.url = self.photoUrl
flickrImage.locations = self.location
flickrImage.locations?.latitude = self.latitude
flickrImage.locations?.longitude = self.longitude
}
else {
print(error?.localizedDescription ?? "Something went wrong downloading an image")
}
do {
try self.dataController.viewContext.save()
self.photoStore.removeAll()
completion(true, nil)
} catch {
print("Error saving into Core Data")
completion(false, error)
}
}
}
Please ignore the red box, the white box shows the images being fetched all over again. My guess is, it has to do with core data.
i am not sure if this will fix your problem but, may be you try use dispatchgroup()
so it will goes like this
let group = DispatchGroup()
for flickr in photoStore {
//fetch image from url here
group.enter()
flickrClient.getImage(imageUrl: flickr ?? "") { (data, error) in
if error == nil {
self.imageLoading(is: false)
flickrImage.photo = data
flickrImage.url = self.photoUrl
flickrImage.locations = self.location
flickrImage.locations?.latitude = self.latitude
flickrImage.locations?.longitude = self.longitude
group.leave()
}
else {
print(error?.localizedDescription ?? "Something went wrong downloading an image")
group.leave()
}
}
group.notify(queue: .main) { [weak self] in
//save your dowloaded image here
try self?.dataController.viewContext.save()
self?.photoStore.removeAll()
} catch {
print("Error saving into Core Data")
}
}
so basically by using dispatchgroup you can group the task and run it asynchronously. It just to make sure the request is not repeted and you save the image to core data once.
I'm trying to save the stored image's url in the database however it doesn't successfully do that. It doesn't show any url's in the database as it seemingly skips the instructions in the following saveButton function.
#IBAction func saveButtonClicked(_ sender: Any) {
let storageRef = Storage.storage().reference().child("profileImages").child(id!)
if let uploadData = profilePicture.image?.pngData(){
storageRef.putData(uploadData, metadata: nil) { (metadata, error) in
if error != nil{
print(error)
return
}
storageRef.downloadURL(completion: { (url, error) in
if let downUrl = url {
Database.database().reference().child("Users").child(self.id!).child("profileImageUrl").setValue(downUrl)
}
})
}
}else{
Database.database().reference().child("Users").child(self.id!).child("profileImageUrl").setValue("default")
}
self.performSegue(withIdentifier: "goToMainPage", sender: self)
}
}
try to replace
storageRef.downloadURL(completion: { (url, error) in
if let downUrl = url {
Database.database().reference().child("Users").child(self.id!).child("profileImageUrl").setValue(downUrl)
}
})
by
let pat = (metadata?.downloadURL()?.absoluteString.description)
let link = pat! //Link of image
Database.database().reference().child("Users").child(self.id!).child("profileImageUrl").setValue(link)
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)
}
My problem is when I run the application, this error appears in the console
Optional ("Users Profile / Kk1kKMF89BH778vgd788ju7.jpg does not exist.")
But the file exists in my Firebase storage. I try to download the image to a UIImage named phSelfie.
This is my code:
import UIKit
import Firebase
class SeeSelfieViewController: UIViewController {
var storage = FIRStorage.storage()
#IBOutlet var phSelfie: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let gsRef = storage.reference(forURL: "gs://******-****.appspot.com/profileUsers/")
let user = FIRAuth.auth()!.currentUser!
let imageRef = gsRef.child("\(user.uid).jpg")
imageRef.data(withMaxSize: 1 * 1024 * 1024) { (data, error) in
if error != nil {
print("\(error?.localizedDescription)") <------ This run the error!!
} else {
let imageSelfie = UIImage(data: data!)
self.phSelfie.image = imageSelfie
print("Succes")
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I see a few issues:
storage.reference(forURL: "gs://******-****.appspot.com/profileUsers/") should be an HTTP URL, or you should just use storage.reference().child("profileUsers")
Users Profile / Kk1kKMF89BH778vgd788ju7.jpg seems to have spaces in the object, which would be percent escaped %20 in the actual object name.
Users Profile seems to not match profileUsers, which is what the object name above says...
// Press Upload Button Action
#IBAction func uploadButton(_ sender: Any) {
print("Upload Button pressed")
// Create a root reference
let storage = Storage.storage()
let storageReferance = storage.reference()
let mediaFolder = storageReferance.child("media")
print("Media Folder Created")
if let data = UIImageView.image?.jpegData(compressionQuality: 0.5) {
print("Image Selected")
let imageReference = mediaFolder.child("image.jpg")
imageReference.putData(data, metadata: nil) { (metadata,error) in
if error != nil {
print(error?.localizedDescription)
print("Image selection error")
}else{
imageReference.downloadURL { url, error in
if error == nil{
let imageURL = url?.absoluteString
print("Image selection SUCCESS")
print(imageURL)
}
}
}
}
}
}
During the debugging of the code I tested the URL, that url works in the browser and the image is displayed in the browser. But the below code is not loading the image to the image wrapper.
let row = indexPath.row
cell.NewsHeading.font =
UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.NewsHeading.text = SLAFHeading[row]
if let url = NSURL(string: SLAFImages[indexPath.row]) {
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if error != nil {
print("thers an error in the log")
} else {
dispatch_async(dispatch_get_main_queue()) {
cell.NewsImage.image = UIImage(data: data!)
}
}
}
task.resume()
}
return cell
As explained in the comments, you can't return from an asynchronous task - you can't know when the task will be complete and when the data will be available.
The way to handle this in Swift is to use callbacks, often called by convention "completion handlers".
In this example I create a function to run the network task, and this function has a callback for when the image is ready.
You call this function with a syntax named "trailing closure" and there you handle the result.
Here's an example for you.
The new function:
func getNewsImage(stringURL: String, completion: (image: UIImage)->()) {
if let url = NSURL(string: stringURL) {
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
if let data = data {
if let image = UIImage(data: data) {
completion(image: image)
} else {
print("Error, data was not an image")
}
} else {
print("Error, no data")
}
}
}
task.resume()
}
}
Your elements from your example:
let row = indexPath.row
cell.NewsHeading.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.NewsHeading.text = SLAFHeading[row]
And how you call the new function:
getNewsImage(SLAFImages[indexPath.row]) { (image) in
dispatch_async(dispatch_get_main_queue()) {
cell.NewsImage.image = image
// here you update your UI or reload your tableView, etc
}
}
It's just an example to show how it works, so you might have to adapt to your app, but I believe it demonstrates what you need to do.