I'm using Google Firebase in a Swift iOS project. I have a part of my app where the user selects more than 1 photo from their device to upload. I'm trying to find the best practice for uploading all the photos they selected at once to Firebase Storage. I know how to upload one photo.
I looked through the Docs and didn't see any methods about uploading multiple NSData objects, so would I just run a for loop and upload each image individually?
Thanks! All feedback is appreciated.
Swift 3
This is how I am uploading multiple images to Firebase. I am making a count of successful uploads, if its equal to the number of images supplied, thats when I am calling a completion handler.
Not sure if this is the best practice, but it works!!
import Foundation
import Firebase
import FirebaseDatabase
import FirebaseStorage
class UploadImages: NSObject{
static func saveImages(imagesArray : [UiImage]){
Auth.auth().signInAnonymously() { (user, error) in
//let isAnonymous = user!.isAnonymous // true
//let uid = user!.uid
if error != nil{
print(error)
return
}
guard let uid = user?.uid else{
return
}
uploadImages(userId: uid,imagesArray : imagesArray){ (uploadedImageUrlsArray) in
print("uploadedImageUrlsArray: \(uploadedImageUrlsArray)")
}
}
}
static func uploadImages(userId: String, imagesArray : [UIImage], completionHandler: #escaping ([String]) -> ()){
var storage = Storage.storage()
var uploadedImageUrlsArray = [String]()
var uploadCount = 0
let imagesCount = imagesArray.count
for image in imagesArray{
let imageName = NSUUID().uuidString // Unique string to reference image
//Create storage reference for image
let storageRef = storage.reference().child("\(userId)").child("\(imageName).png")
guard let myImage = image else{
return
}
guard let uplodaData = UIImagePNGRepresentation(myImage) else{
return
}
// Upload image to firebase
let uploadTask = storageRef.putData(uplodaData, metadata: nil, completion: { (metadata, error) in
if error != nil{
print(error)
return
}
if let imageUrl = metadata?.downloadURL()?.absoluteString{
print(imageUrl)
uploadedImageUrlsArray.append(imageUrl)
uploadCount += 1
print("Number of images successfully uploaded: \(uploadCount)")
if uploadCount == imagesCount{
NSLog("All Images are uploaded successfully, uploadedImageUrlsArray: \(uploadedImageUrlsArray)")
completionHandler(uploadedImageUrlsArray)
}
}
})
observeUploadTaskFailureCases(uploadTask : uploadTask)
}
}
//Func to observe error cases while uploading image files, Ref: https://firebase.google.com/docs/storage/ios/upload-files
static func observeUploadTaskFailureCases(uploadTask : StorageUploadTask){
uploadTask.observe(.failure) { snapshot in
if let error = snapshot.error as? NSError {
switch (StorageErrorCode(rawValue: error.code)!) {
case .objectNotFound:
NSLog("File doesn't exist")
break
case .unauthorized:
NSLog("User doesn't have permission to access file")
break
case .cancelled:
NSLog("User canceled the upload")
break
case .unknown:
NSLog("Unknown error occurred, inspect the server response")
break
default:
NSLog("A separate error occurred, This is a good place to retry the upload.")
break
}
}
}
}
}
Usage
let arrayOfImages : [UIImage] = [Image1, Image2, Image3]
UploadImages.saveImages(imagesArray: arrayOfImages)
Answered by #FrankVanPuffellen in comments...
"You'd indeed just loop and upload them individually. Your iOS user might also appreciate if you upload them one at a time, so that they can abort the upload midways through without losing progress on all images." – Frank van Puffelen
Related
I need to display the images that have been stored on the storage on Firebase. Right now, I only tracked the images using the link generated by function downloadURL:
func UploadImage(imageData: Data, path: String, completion: #escaping (String) -> ()){
let storage = Storage.storage().reference()
let uid = Auth.auth().currentUser?.uid
storage.child(path).child(uid ?? "").putData(imageData, metadata: nil) { (_, err) in
if err != nil{
completion("")
return
}
// Downloading Url And Sending Back...
storage.child(path).child(uid ?? "").downloadURL { (url, err) in
if err != nil{
completion("")
return
}
completion("\(url!)")
}
}
}
So all I can get is a hyperlink that is like: https://firebasestorage.googleapis.com/v0/b/getting-started-20f2f.appspot.com/o/profile_Photos%2FGQ1KR9H1mLZl2NAw9KQcRe7d72N2?alt=media&token=473ce86c-52ba-42ec-be71-32cc7dc895d7.
I refer to the official documentation, it seems that only when I have the name of the image file can I download it to an ImageView or UIImageView object. However, the link does not make any sense to me, so what can I do?
EDIT
I actually tried a solution provided by the official documentation:
func imageDownloader(_ imageURL: String) {
let store = Database.database().reference()
let uid = Auth.auth().currentUser?.uid
let imageRef = store.child(imageURL)
var myImageView = UIImageView()
imageRef.getData(completion: { (error, data) in
if let error = error {
// Uh-oh, an error occurred!
} else {
// Data for "images/island.jpg" is returned
let image = UIImage(data: data!)
}
})
}
But it suggests that I need to change something because Cannot convert value of type 'DataSnapshot' to expected argument type 'Data'.
If you're storing the image paths in Firestore, actually the exact file name does not matter if there is only one file available under the fork. So you just need to specify the path.
To then download the image from Storage, construct the path and download:
let uid = Auth.auth().currentUser?.uid
Storage.storage().reference().child("the\path\to\your\uid\collection").child(uid).getData(maxSize: 1048576, completion: { (data, error) in
if let data = data,
let img = UIImage(data: data) {
// do something with your image
} else {
if let error = error {
print(error)
}
// handle errors
}
})
You are uploading to Storage.storage(), but then in your imageDownloader, you're attempting to use Database.database(), which has a similar-looking API, but is, in fact, different.
Make sure to use Storage.storage() and that the closure parameters are in the order data, error in.
Finally, right now in your imageDownloader, it doesn't look like you're doing anything yet with var myImageView = UIImageView(), but keep in mind that you won't have access to the UIImage until the async getData completes.
Store your images at Firebase Storage & then retrieve using this code.
Storage.storage().reference.child("ProfilePhotos").child("ImageName").downloadURL {(url, _) in
DispatchQueue.main.async {
guard let url = url else { return }
imageView.setImage(with: url, placeholder: UIImage(named: "dummyImage"))
}
}
I created the below function to fetch image from firebase storage:
func downloadImageFromFirebase(_ imageNameOnFireBase: String, imageViewToBeFilled: UIImageView) {
let storage = Storage.storage()
let reference: StorageReference = storage.reference().child(imageNameOnFireBase)
reference.downloadURL { url , error in
if error != nil {
print(error!.localizedDescription)
imageViewToBeFilled.image = UIImage(named: "default")
} else {
if let url = url {
do {
let data = try Data.init(contentsOf: url)
imageViewToBeFilled.image = UIImage(data: data)
} catch {
print("Error fetching URL")
imageViewToBeFilled.image = UIImage(named: "default")
}
}
}
}
}
When using the function many times it causes loading delay , How can i approach faster and more reliable way of downloading the images ?
You should be able to fetch images from Firebase with URLs, and using SDWebImage it will enable your app to have very fast loading and scrolling times as the images are only loaded once. An excellent tutorial for this can be found here: https://www.youtube.com/watch?v=XPAaxF0rQy0
(He teaches it with Firebase Storage)
I'm trying to upload an image to firebase storage and get the download url to save it in database, but I get a nil value and the function returns upon checking it. I followed the documentation and solutions from other posts and I can't see where I'm mistaking.
The function is :
func uploadImage(completed: #escaping (Bool) -> (),_ image: UIImage ){
print(" ############## UPLOAD STARTED ###########")
// let stringURL: String?
// guard let uid = Auth.auth().currentUser?.uid else {return} // use the userUid to sign the alert
// Create a root reference
let storageRef = Storage.storage().reference()
// Create a reference to "mountains.jpg"
// let alertsRef = storageRef.child("userAlertImages.jpg")//("user/\(uid)") // change path for userAlertImages path
// Create a reference to 'images/mountains.jpg'
let alertsImagesRef = storageRef.child("Alert Images/userAlertImages.jpg")
// While the file names are the same, the references point to different files
// alertsRef.name == alertsImagesRef.name; // true
// alertsRef.fullPath == alertsImagesRef.fullPath; // false
let imageData = UIImageJPEGRepresentation(image, 0.5)
let metaData = StorageMetadata()
metaData.contentType = " jpeg " // data type
metaData.customMetadata = ["k1": "",
"k2" : " ",
"k3" : "",
"k4" : ""]
alertsImagesRef.putData(imageData! as Data , metadata: metaData) { metaData, error in
if(error != nil){
print(error as Any)
return
}
}
// Fetch the download URL
alertsImagesRef.downloadURL { (url,error) in
guard let downloadURL = url else {
print("########## downloaded url is: \(url) #############")
return
}
NewMapViewController.alertImageURL = (url?.absoluteString) ?? ""
// NewMapViewController.alertImageURL = (downloadURL)
print("######### url is:\(String(describing: url)) #########")
completed(true)
// self.postAlertNotification()
self.tapCounter = 0
self.performSegue(withIdentifier: "chooseIconSegue", sender: self)
}
}
can you see where's the error?
Many thanks.
Thinking about it I realised that downloadURL fetch was actually done before than the image upload was complete, that's because Firebase is asynchronous. So I added a completion block to the uploading part, and in the completion scope I put the downloadURL fetching part.
It's not blinking eyes fast and I will appreciate any suggestion on speeding up things, because this way there is a little lag before the seguegets performed.
It's not annoying at all an I could, and probably should, add a little spinning wheel to show users that the app has not frozen, but I rather just avoid the lag altogether if possible. I left the prints written in hope that this post will help others new to Firebase as I am, giving detailed almost step-by-step guidance, as this kind of answers really helped me before, and I haven't found any on the subject.
Rewritten function is:
func uploadImage(completed: #escaping (Bool) -> (),_ image: UIImage ){
print(" ############## UPLOAD STARTED ###########")
// Create a root reference
let storageRef = Storage.storage().reference()
// Create a reference to Images folder
let alertsImagesRef = storageRef.child("Alert Images")
// Create a reference for new images
let uuid = UUID()
let imageRef = alertsImagesRef.child("userAlertImage \(uuid).jpg")
let imageData = UIImageJPEGRepresentation(image, 0.5)
let metaData = StorageMetadata()
metaData.contentType = " jpeg " // data type
metaData.customMetadata = ["k1": "",
"k2" : " ",
"k3" : "",
"k4" : ""]
imageRef.putData(imageData! as Data , metadata: metaData, completion: { metaData, error in
if(error != nil){
print(error as Any)
return
}
print(" ####### image uploaded #######")
self.tapCounter = 0
self.performSegue(withIdentifier: "chooseIconSegue", sender: self)
// fetch url v2
imageRef.downloadURL(completion: { (url, err) in
if let err = err {
print("Error downloading image file, \(err.localizedDescription)")
return
}
guard let url = url else { return }
//Now you have the download URL for the image, and can do whatever you want with it.
NewMapViewController.alertImageURL = url.absoluteString
print(" ######### url is:\(String(describing: url)) #########")
completed(true)
// self.postAlertNotification()
// self.tapCounter = 0
// self.performSegue(withIdentifier: "chooseIconSegue", sender: self)
print(" ############## UPLOAD ENDED ###########")
})
})
}
Before writing question I would say that I'm new in iOS development and in firebase too :) That's why I apologize in advance for a silly question :)
When I load profile.png image from firebase storage programmatically it's loaded correctly without any problems.
static func getUserImage(_ uid: String, completion: #escaping (UIImage?, NSError?) -> Void) {
// Create a reference to the file you want to download.
let storage = FIRStorage.storage()
let userUID: String = String(describing: uid)
let storageRef = storage.reference(forURL: "...some address...")
let downloadRef = storageRef.child("\(userUID)/profilePhoto.png")
// Download in memory with a maximum allowed size of 10MB (10 * 1024 * 1024 bytes).
downloadRef.data(withMaxSize: 10 * 1024 * 1024) {
data, error in
guard let data = data, error == nil else {
NSLog("Retrieve image failed:\n\(error?.localizedDescription)")
completion(nil, error as! NSError)
return
}
guard let image = UIImage(data: data) else {
NSLog("Image decoding failed:\n\(data)")
completion(nil, nil)
return
}
assert(error == nil)
completion(image, nil)
}
}
But when I go to firebase storage in browser and try to search it by uid manually from list I can't find it - it doesn't exist. Could you explain me how it's possible? It problem exists only for several images in firebase storage.
Thank you in advance!
Folder name should start with capital letter.
I didn't find an answer that satisfied me and hope you have any idea. I want to upload my images to the Firebase storage and save the imageUrls into the Firebase database.
var imageUrls = [String]()
func uploadImagesToStorage(imagesArray: [UIImage]) {
for i in imagesArray {
guard let uploadData = UIImageJPEGRepresentation(i, 0.3) else { return }
let fileName = NSUUID().uuidString
FIRStorage.storage().reference().child("post_Images").child(fileName).put(uploadData, metadata: nil) { (metadata, err) in
if let err = err {
return
}
guard let profileImageUrl = metadata?.downloadURL()?.absoluteString else { return }
self.imageUrls.append(profileImageUrl)
}.resume()
} //..End loop
saveToDatabaseWithImageUrl(imageUrls: imageUrls)
Uploading the images works with the uploadImagesToStorage(imagesArray: [UIImage]) method. This method gets an array as argument which contains the images that I want to upload. While uploading the images I'm downloading the imageUrl information from the metadata that firebase is giving me and save that imageUrl into the imageUrls array. For loop is necessary to save the imageUrl information for every single image. When the images are uploaded and the imageUrls Array is filled with the imageUrl information I call the function func saveToDatabaseWithImageUrl(imageUrls: [String]) to save the imageUrls into the database. Checking Firebase I see that the images are saved into the Firebase storage, but the imageUrls are not saved into the Firebase database. While debugging my code I found out that the reason for this behavior is that while the images are uploaded the code jumps to the next line. So it calls the saveToDatabaseWithImageUrls with an empty imageUrls array. I read the [Documentation][1] and tried to manage the upload with the .resume() method. Still it jumped to the saveToDatabaseWithImageUrl method. I don't know how to guarantee that the upload is finished and then the next line of code is executed. Thanks for your help.
Its happen because success block of your .child("post_Images").child(fileName).put call asynchronously. Rest of code go sync. So your for start uploading photos and after that you are saving URLs to database but urls are empty because you don't wait for finish uploading.
I give you a perfect solution based on DispathGroup
//Create DispatchGroup
let fetchGroup = DispatchGroup()
for i in imagesArray {
guard let uploadData = UIImageJPEGRepresentation(i, 0.3) else { return }
let fileName = NSUUID().uuidString
//Before every iteration enter to group
fetchGroup.enter()
FIRStorage.storage().reference().child("post_Images").child(fileName).put(uploadData, metadata: nil) { (metadata, err) in
if let err = err {
fetchGroup.leave()
return
}
guard let profileImageUrl = metadata?.downloadURL()?.absoluteString else { return }
self.imageUrls.append(profileImageUrl)
//after every completion asynchronously task leave the group
fetchGroup.leave()
}.resume()
}
And know id magic
fetchGroup.notify(queue: DispatchQueue.main) {
//this code will call when number of enter of group will be equal to number of leaves from group
//save your url here
saveToDatabaseWithImageUrl(imageUrls: imageUrls)
}
This solution don't block a thread, everything works asynchronously here.