How to upload images to Firebase Storage in particular order? - ios

I am uploading an array of images to firebase, which was previously filled by up to three photos taken by camera.
After each upload, I save the downloadURL.
But I see, that the order of the images is random.
I suspect that it depends on the photosize, which photo is uploaded first.
How can I ensure, that the first image in imageArray will be also the first image uploaded and therefore the first downloadURL I get?
func storeInFirestore(var1:String, var2:String, var3:String, var4:String, var5: String) {
guard let user = Auth.auth().currentUser?.uid else {return}
var data = NSData()
var i = 0
for items in imageArray {
i += 1
if i <= imageArray.count {
data = items.images.jpegData(compressionQuality: 0.8)! as NSData
let filePath = "\(user)/images"
let metaData = StorageMetadata()
let ref = Storage.storage().reference().child("\(user)_\(var1)_\(i)")
metaData.contentType = "image/jpg"
ref.putData(data as Data, metadata: metaData){(metaData,error) in
if let error = error {
print(error.localizedDescription)
return
}else{
[get the downloadURL and store in array...]

Just enumerate the loop and use n, the index value, to construct the array. You can also use a dictionary instead of an array and simply use n as the key (and the file name as the value).
for (n, img) in imageArray.enumerated() {
let data = img.images.jpegData(compressionQuality: 0.8)
let filePath = "\(user)/images"
let storageRef = Storage.storage().reference().child("\(user)_\(var1)_\(n)")
let metaData = StorageMetadata()
metaData.contentType = "image/jpg"
storageRef.putData(data, metadata: metaData) { (metaData, error) in
if let _ = metaData {
// image successfully saved to storage
// Remember, `n` is still in scope which is the array index
// (i.e. 0 is the first image, 1 is the second, etc.) so
// simply construct your array using these indices. To simplify
// things, you can use a dictionary here instead of an array,
// which could look something like `remoteImagePaths[n] = remotePath`.
} else {
if let error = error {
print(error.localizedDescription)
}
}
}
}

Solution
Well kind of... knowing if an uploaded image is the first in the list when the callback is triggered can be tricky as the first image could be massive, so it takes a while to upload, then the second image is small, so it doesn't take as long, therefore the second image is uploaded first. I gather you already know this from your post though, just wanted to clarify this for others who visit this issue.
Now as for fixing it, there's a few ways, but I think the cleanest one is this.
func storeInFirestore(var1:String, var2:String, var3:String, var4:String, var5: String, completion: #escaping([URL]) -> Void) {
guard let user = Auth.auth().currentUser?.uid else {return}
var data = NSData()
var imageToUrlsPair = [UIImage: URL]()
var imagesDownloadedCount: Int = 0
for items in imageArray {
let data = items.images.jpegData(compressionQuality: 0.8)
let filePath = "\(user)/images"
let metaData = StorageMetadata()
let ref = Storage.storage().reference().child("\(user)_\(var1)_\(i)")
metaData.contentType = "image/jpg"
ref.putData(data, metadata: metaData){(metaData,error) in
if let error = error {
imagesDownloadedCount += 1 // Handle this error however you please, you could fail this entire request.
print(error.localizedDescription)
return
} else {
imagesDownloadedCount += 1
imageToUrlsPair[items.images] = [get the downloadURL and store in array...]
if imagesDownloadedCount == imageArray.count {
completion(imageToUrlsPair.map { $0.value })
}
}
}
}
}
So to sum up what I've done:
I've made a dictionary to contain a pair of images to urls (the imageToUrlsPair variables)
Once an image is downloaded, it adds the url to the associated image and increments the downloaded image counter (imagesDownloadedCount)
Once the final image is downloaded, the imagesDownloadedCount will equal the imageArray.count so it will trigger the completion callback.
I have added a completion callback so that this function performs its network requests asynchronously and returns the urls once all requests have been completed in the background.

Related

Display Image downloaded from downloadURL generated by Firebase

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"))
}
}

Saving Data as UIImage while preserving the original image characteristics

I am removing exif and location metadata from images using Photos and image I/O frameworks:
First I get Data from PHAssets:
let manager = PHImageManager()
manager.requestImageData(for: currentAsset, options: options) { (data, dataUTI, orientation, info) in
if let data = data {
dataArray.append(data)
}
}
Then I use this function to remove metadata:
fileprivate func removeMetadataFromData(data: Data) -> NSMutableData? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {return nil}
guard let type = CGImageSourceGetType(source) else {return nil}
let count = CGImageSourceGetCount(source)
let mutableData = NSMutableData(data: data)
guard let destination = CGImageDestinationCreateWithData(mutableData, type, count, nil) else {return nil}
let removeExifProperties: CFDictionary = [String(kCGImagePropertyExifDictionary) : kCFNull, String(kCGImagePropertyGPSDictionary): kCFNull] as CFDictionary
for i in 0..<count {
CGImageDestinationAddImageFromSource(destination, source, i, removeExifProperties)
}
guard CGImageDestinationFinalize(destination) else {return nil}
return mutableData
}
Then I use this to create UIImage from NSMutableData objects that I get from previous function:
let image = UIImage(data: mutableData as Data)
and I save the image to user's library like so:
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
let placeholder = request.placeholderForCreatedAsset
let albumChangeRequest = PHAssetCollectionChangeRequest(for: collection)
if let placeholder = placeholder, let albumChangeRequest = albumChangeRequest {
albumChangeRequest.addAssets([placeholder] as NSArray)
}
return mutableData
}
The problem I have is that using this method, the output file is compressed, and also the name and DPI of the resulting image is different from the original image. I want to keep everything the same as the original image and just remove the metadata. Is there a way to do that?
The problem is the round-trip through UIImage. Just save the Data obtained from requestImageDataAndOrientation.
func saveCopyWithoutLocation(for asset: PHAsset) {
let options = PHImageRequestOptions()
manager.requestImageDataAndOrientation(for: asset, options: options) { data, dataUTI, orientation, info in
guard let data = data else { return }
self.library.performChanges {
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, data: data, options: nil)
request.location = nil
} completionHandler: { success, error in
if success {
print("successful")
} else {
print(error?.localizedDescription ?? "no error?")
}
}
}
}
Now, that only removes location. If you really want to remove more EXIF data obtained through CGImageSourceCreateWithData, you can do that. But just avoid an unnecessary round-trip through a UIImage. It is the whole purpose to use CGImageSource functions, namely that you can change metadata without changing the underlying image payload. (Round-tripping through UIImage is another way to strip meta data, but as you have discovered, it changes the image payload, too, though often not observable to the naked eye.)
So, if you want, just take the data from CGImageDestination functions directly, and pass that to PHAssetCreationRequest. But I might advise being a little more discriminating about which EXIF metadata you choose to remove, because some of it is important, non-confidential image data (e.g., likely the DPI is in there).
Regarding the filename, I'm not entirely sure you can control that. E.g., I've had images using the above location-stripping routine, and some preserve the file name in the copy, and others do not (and the logic of which applies is not immediately obvious to me; could be the sourceType). Obviously, you can use PHAssetChangeRequest rather than PHAssetCreationRequest, and you can just update the original PHAsset, and that would preserve the file name, but you might not have intended to edit the original asset and may have preferred to make a new copy.

Firebase Storage Image Not Downloading in Tableview

Firebase Storage Image Not Downloading in Tableview. If I replace the line let tempImageRef = storage.child("myFiles/sample.jpg"), it's showing the single image. But if try to grab all the images inside 'myFiles' , it doesn't work. Please help
func fetchPhotos()
{
//let database = FIRDatabase.database().reference()
let storage = FIRStorage.storage().reference(forURL: "gs://fir-test-bafca.appspot.com")
let tempImageRef = storage.child("myFiles/")
tempImageRef.data(withMaxSize: (1*1024*1024)) { (data, error) in
if error != nil{
print(error!)
}else{
print(data!)
let pic = UIImage(data: data!)
self.picArray.append(pic!)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
You are referencing an entire folder (myFiles/). That wont work. You need to reference each image individually.
One way to do this is to write the image metadata to your realtime database when you upload an image. Then read the metadata (more specifically the path) and then reference the image that way.
If you have 3 images you want to upload and store, you could write the metadata as follows:
/images
/image1key
/metdata
/path
/image2key
/metdata
/path
/image3key
/metdata
/path
Then you can query your database for your images path's like so
let ref = Firebase.database().ref("images")
ref.observe(.value, withCompletion: { snapshot in
if let values = snapshot.value as? [String : Any] {
if let metadata = values["metadata"] as? [String: Any] {
let imagePath = metadata["path"]
//Now download your image from storage.
}
}
})
This isn't the cleanest code and you can definitely improve, but it will work :)

How to finish the upload of images to Firebase Storage and then save the imageUrl to the Firebase Database

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.

uploading/downloading multiple images the right way?

i'm trying to upload or download images using Nuke(framework for downloading and Caching images) And Firebase to upload images as the backend
for single file it's easy to deal with without any problem
but for multiple ones i don't really know what to do right
i'm having an issues where it don't do it job synchronously
it downloads second image before the first one sometimes
i'll show my way of downloading and uploading multiple images
For download i put the code in for-loop
func downloadImages(completion: (result: [ImageSource]) -> Void){
var images = [ImageSource]()
for i in 0...imageURLs.count-1{
let request = ImageRequest(URL: NSURL(string:imageURLs[i])!)
Nuke.taskWith(request) { response in
if response.isSuccess{
let image = ImageSource(image: response.image!)
images.append(image)
if i == self.imageURLs.count-1 {
completion(result: images)
}
}
}.resume()
}
}
And for uploading where the user chooses the images
form image picker and return it as NSData array
And then perform this code
func uploadImages(completion: (result: [String]) -> Void){
let storageRef = storage.referenceForURL("gs://project-xxxxxxxxx.appspot.com/Uploads/\(ref.childByAutoId())")
var imageUrl = [String]()
var imgNum = 0
for i in 0...imageData.count-1 {
let imagesRef = storageRef.child("\(FIRAuth.auth()?.currentUser?.uid) \(imgNum)")
imgNum+=1
let uploadTask = imagesRef.putData(imageData[i], metadata: nil) { metadata, error in
if (error != nil) {
print("error")
imageUrl = [String]()
completion(result: imageUrl)
} else {
print("uploading")
// Metadata contains file metadata such as size, content-type, and download URL.
let downloadURL = metadata!.downloadURL()?.absoluteString
print(downloadURL)
imageUrl.append(downloadURL!)
if i == imageUrl.count-1{ //end of the loop
print("completionUpload")
completion(result: imageUrl)
}
}
}}
this is good way to do this task ?
what should i do to make each image downloads in order ?
please give me anything that may help example code , link etc ..
Thanks in advance
We highly recommend using Firebase Storage and the Firebase Realtime Database together to accomplish lists of downloads:
Shared:
// Firebase services
var database: FIRDatabase!
var storage: FIRStorage!
...
// Initialize Database, Auth, Storage
database = FIRDatabase.database()
storage = FIRStorage.storage()
Upload:
let fileData = NSData() // get data...
let storageRef = storage.reference().child("myFiles/myFile")
storageRef.putData(fileData).observeStatus(.Success) { (snapshot) in
// When the image has successfully uploaded, we get it's download URL
let downloadURL = snapshot.metadata?.downloadURL()?.absoluteString
// Write the download URL to the Realtime Database
let dbRef = database.reference().child("myFiles/myFile")
dbRef.setValue(downloadURL)
}
Download:
let dbRef = database.reference().child("myFiles")
dbRef.observeEventType(.ChildAdded, withBlock: { (snapshot) in
// Get download URL from snapshot
let downloadURL = snapshot.value() as! String
// Now use Nuke (or another third party lib)
let request = ImageRequest(URL: NSURL(string:downloadURL)!)
Nuke.taskWith(request) { response in
// Do something with response
}
// Alternatively, you can use the Storage built-ins:
// Create a storage reference from the URL
let storageRef = storage.referenceFromURL(downloadURL)
// Download the data, assuming a max size of 1MB (you can change this as necessary)
storageRef.dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
// Do something with data...
})
})
For more information, see Zero to App: Develop with Firebase, and it's associated source code, for a practical example of how to do this.

Resources