Adding image from Firebase to UITableViewCell - ios

I want to retrieve the image that is stored in the storage of an user and place it next to his name in a custom UITableViewCell. The problem now is that the tableview will load when the images aren't done downloading (I think?), causing the application to crash because the image array is nil. So what is the correct way to load the tableview? I think, for the user experience, it is important that the tableviewcell image should be shown even if the images aren't done downloading, and present them a default image that is saved in the assists. I thought about making an array with UIImages that links to the default asset of loading an image and changing the image to the profile picture when it is done downloading. But I really have no clue how to do that. This is what I got so far about downloading the image:
let storage = FIRStorage.storage()
let storageRef = storage.reference(forURL: "link.appspot.com")
channelRef?.observeSingleEvent(of: .value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict{
let UIDs = each.value["userID"] as? String
if let allUIDS = UIDs{
let profilePicRef = storageRef.child((allUIDS)+"/profile_picture.png")
profilePicRef.data(withMaxSize: 1 * 500 * 500) { data, error in
if let error = error {
}
if (data != nil)
{
self.playerImages.append(UIImage (data: data!)!)
}
}
}
let userNames = each.value["username"] as? String
if let users = userNames{
self.players.append(users)
}
}
}
self.tableView.reloadData()
})
This is in the cellForRow
cell.playersImage.image = playerImages[indexPath.row] as UIImage
My rules, haven't changed it from the default rules:
service firebase.storage {
match /b/omega-towers-f5beb.appspot.com/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
Thank you.

Regarding user experience, you are correct. It is standard to have some sort of default image when loading an image from a URL. A great library to use for image caching and using default assets in its' place is AlamofireImage
Vandan Patel's answer is correct in saying you need to ensure your array is not nil when loading the tableview. You will be given a completion block to handle any extra work you would like to do with your image, using the AlamofireImage library.
This is all assuming you are getting a correct image URL back for your Firebase users.

You should call tableView.reloadData() when the images are done downloading. One important thing, initialize your playerImages as playerImages = [UIImage]() instead of playerImages: [UIImage]!. if it's empty, it wouldn't show your array is nil.
Update:
if let players = playerImages {
//code
}

Related

Retrieve and load images from Firebase Storage without using Realtime Database using while loop instead?

I am developing a photo gallery app in Swift right now using Firebase to store the images. My goal is to load all the images in a collection view when the view loads.
The images are stored in folders called images_(user email)_(user id). The user uploads images which are stored in the Firebase Storage with the names image0.png, image1.png, image2.png, etc. Right now, I have successfully created accounts and stored the images. Now, I am trying to load the images in a collection view in the viewDidLoad(). I am not using the Firebase Realtime Database because I feel it would be overkill for what I am trying to accomplish.
while (isDownloading == true) {
let storage = Storage.storage()
let storageRef = storage.reference();
let imageLoadRef = storageRef.child(("images_" + (Auth.auth().currentUser?.email)! + "_" + (Auth.auth().currentUser?.uid)!) + "/image" + String(self.imageCount) + ".png")
imageLoadRef.getData(maxSize: 10 * 1024 * 1024) { data, error in
if let error = error {
self.isDownloading = false
} else {
let loadedImage = UIImage(data: data!)
//Updates collection view with loaded image
self.collectionView.reloadData()
//moves on to next image
self.imageCount += 1
}
}
}
As soon as the view loads, I run a while loop that iterates through all the images in the user's folder. Since I don't have any way to get the number of images in the folder, I had to use a while loop and break it when the next image is not found. For example, if there are only 5 images in the folder, the code will return an error when it tries to load the sixth nonexistent image. When this happens, I try to catch the error and break the while loop by setting the condition isDownloading to false.
However, the issue I am running in to is that the while loop never breaks even though it should return an error and set isDownloading to false.
To clarify, the imageCount variable is increased with each iteration of the while loop to get the next image in the folder. So when imageCount = 1 for example, the code will try to load a photo with name "image1.png". However, if it doesn't exist, it catches the error and breaks the while loop.
I know the way I structured it is a little confusing but I find it more efficient than using downloadURLs and the Firebase Database.
Any help is appreciated. Thank you!
Do not know will it work or not you can just give it a try For now and let me know if issue exists
//Loop is Fine
while (isDownloading == true) {
//Provided Required References
let storage = Storage.storage()
let storageRef = storage.reference();
//Name of file to download
let imageLoadRef = storageRef.child(("images_" + (Auth.auth().currentUser?.email)! + "_" + (Auth.auth().currentUser?.uid)!) + "/image" + String(self.imageCount) + ".png")
//handler That Run in async Mode
imageLoadRef.getData(maxSize: 10 * 1024 * 1024) { data, error in
//Here I think mistake is
if let error = error {
//You are catching error But not handling data error
self.isDownloading = false
}
else {
if data != nil{ // if image found
//here you need to check weather this handler is returning data o not as you got error of no data So Do All functioning here
// User photo here
let loadedImage = UIImage(data: data!)
//Updates collection view with loaded image
self.collectionView.reloadData()
//moves on to next image
self.imageCount += 1
}
else {
//If Image is not found This will cause you an error which you will handle , According to me this was need to be handled
self.isDownloading = false
}
}
}
}
In case this solution is not working You can check Below Demo project on GitHub and use this Method to easily manage images stored under a user
Link - https://github.com/RockinGarg/FirebaseDemo

Parse array images saving and fetching

I have a mosaic app that takes multiple size photos and breaks them into smaller photos. Depending on the size of the photo, the amount of smaller photos could vary. Now I have an NSMutableArray named imageNameList2 that holds all of the smaller images taken from the larger image. For this example I showed an example with the images being called from the image assets list to make it easier to answer this question.
Here is the imageNameList (NSMutableArray that holds all the smaller images)
var imageNameList: [String] {
var imageNameList2:[String] = [] //[NSMutableArray]()
for i in 0...149 {
let imageName = String(format: "pic_%03d", Int(i))
imageNameList2.append(imageName)
}
return imageNameList2
}
What I'd like to do is have a continue button that will save all these images in order as piffles or any other format to parse that works best and have another button called retrieve that will retrieve all these photos from parse. I basically have a parse server that utilizes parse frameworks to help speed up the backend process. Can you please show me how I would save and retrieve this NSMutableArray if there are different numbers of stored images each time?
I think you're trying to do something like this. This is just an example. There's a lot of work to be done but hopefully this will get you started. I did not run or test this code.
The idea is to save your images as PFFiles, and create a 'tile' PFObject for each file. Then save all the 'tile' PFObjects to a 'tiles' key of the image PFObject. Then recall the image when you need it by objectId.
Good luck.
let appleTiles = ["apple1, apple2, apple3"]
let orangeTiles = ["orange1, orange2, orange3, orange4, orange5"]
func usage() {
//dont literally run these synchronously like this
post(appleTiles)
post(orangeTiles)
download()
}
func post(_ tileNames: [String]) {
let image = PFObject(className: "Image")
let tilesPF = tileNames.map({ name in
let data = UIImagePNGRepresentation(UIImage(named: name))!
let file = PFFile(data: data)
let tile = PFObject(className: "Tile")
tile["tile"] = file
})
image["tiles"] = tilesPF
image?.saveInBackground(block: { responseObject, error in
//you'll want to save the object ID of the PFObject if you want to retrieve a specific image later
})
}
func download() {
let query = PFQuery(className: "image")
//add this if you have a specific image you want to get
query.whereKey("objectId", equalTo: "someObjectId")
query.findObjectsInBackground({ result, error in
//this is probably close to how you'd unwrap everything but again, I didn't test it so...
if let objects = result as? [PFObject], let first = objects.first, let image = first["image"] as? PFObject, let tiles = image["tiles"] as? [PFObject] {
tiles.forEach({ tile in
let file = tile["tile"]
//now you have an individual PFFile for a tile, do something with it
})
}
})
}

Accessing URL and Array from within a Block of JSON Data

Let's say I have JSON data structured in the following way:
{ "fruits" : {
"apple": {
"name": "Gala"
"color": "red",
"picture": "//juliandance.org/wp-content/uploads/2016/01/RedApple.jpg",
"noOfFruit": [1, 2]
}
}
How would I access the picture and noOfFruit array using the iOS version of Firebase? I want to make a table view with a cell that lists the apple's name, the color, a picture of the apple, and then lists the number of fruit. I get how to obtain the "color" and "name" values but how would I access the array and turn it into a string and the image so that it shows the image in the table view? Any help is appreciated!
For the array, it's really simple. Wherever you have your function that listenes for the firebase changes, I'll imagine that you have the info under the apple key stored in a variable like let apple
Then, you could cast the value of noOfFruit to an array, like the following:
let apple = // what you had before
guard let noOfFruit = apple["noOfFruit"] as? [Int] else {
return
}
//Here you have the array of ints called noOfFruit
For the image, theres several options out there. The first (and bad one) is to synchrounsly fetch the data of the url and set it to an image view as the following:
let url = URL(string: picture)
let data = try? Data(contentsOf: url!) //this may break due to force unwrapping, make sure it exists
imageView.image = UIImage(data: data!)
The thing with this approach is that it's NOT OK. It will block the main thread while its making the request and dowloading the image, leaving the app unresponsive.
The better approach would be to go fetch it asynchronously.There are several libraries that really help, such as AlamofireImage, but it can be done with barebones Foundation really easy. To do that, you should use URLSession class as the following:
guard let url = URL(string: picture) else {
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print(error)
return
}
//Remember to do UI updates in the main thread
DispatchQueue.main.async {
self.myImageView.image = UIImage(data: data!)
}
}.resume()

Fetching CKAsset Image From CloudKit is Very Slow

I am using CloudKit as a server backend for my iOS application. I'm using it to house some relatively static data along with a handful of images(CKAsset). I ran into a problem when the time came for me to actually fetch those assets from the public database. They load at an excruciatingly slow speed.
My use case is to load an image into every cell inside of a collection view. The images are only 200kb in size, but the fetch process took an average of 2.2 seconds for the download to complete and set the image in a cell. For comparison, I took URLs of similar sized stock images and loaded them in using NSURLSession. It took a mere 0.18 - 0.25 seconds for each image to load.
I have tried multiple different ways of downloading the images from CK: direct fetch of the record, query, and operation query. All of them have similar results. I am also dispatching back to the main queue within the completion block prior to setting the image for the cell.
My database is setup to have a primary object with several fields of data. I then setup a backwards reference style system for the photos, where each photo just has a reference to a primary object. That way I can load the photos on demand without bogging down the main data.
It looks something like this:
Primary Object:
title: String, startDate: Date
Photo Object:
owner: String(reference to primary object), image: Asset
Here is an example request that I tried to directly fetch one of the photos:
let publicDb = CKContainer.defaultContainer().publicCloudDatabase
let configRecordId = CKRecordID(recordName: "e783f542-ec0f-46j4-9e99-b3e3ez505adf")
publicDb.fetchRecordWithID(configRecordId) { (record, error) -> Void in
dispatch_async(dispatch_get_main_queue()) {
guard let photoRecord = record else { return }
guard let asset = photoRecord["image"] as? CKAsset else { return }
guard let photo = NSData(contentsOfURL: asset.fileURL) else { return }
let image = UIImage(data: photo)!
cell.cardImageView.image = image
}
}
I can't seem to figure out why these image downloads are taking so long, but it's really quite the showstopper if I can't get them to load in a reasonable about of time.
Update: I tried the fetch operation with a smaller image, 23kb. The fetch was faster, anywhere from 0.3 - 1.1 seconds. That's better, but still doesn't meet the expectation that I had for what CloudKit should be able to provide.
I am using CKQueryOperation. I found that once I added the following line to my code that downloading CKAssets sped up by about a factor of 5-10x.
queryOperation.qualityOfService = .UserInteractive
Here is my full code:
func getReportPhotos(report:Report, completionHandler: (report:Report?, error:NSError?) -> ()) {
let photo : Photo = report.photos![0] as! Photo
let predicate : NSPredicate = NSPredicate(format: "recordID = %#", CKRecordID(recordName: photo.identifier!))
let query : CKQuery = CKQuery(recordType: "Photo", predicate: predicate)
let queryOperation : CKQueryOperation = CKQueryOperation()
queryOperation.query = query
queryOperation.resultsLimit = numberOfReportsPerQuery
queryOperation.qualityOfService = .UserInteractive
queryOperation.recordFetchedBlock = { record in
photo.date = record.objectForKey("date") as? NSDate
photo.fileType = record.objectForKey("fileType") as? String
let asset : CKAsset? = record.objectForKey("image") as? CKAsset
if asset != nil {
let photoData : NSData? = NSData(contentsOfURL:asset!.fileURL)
let photo : Photo = report.photos![0] as! Photo
photo.image = UIImage(data:photoData!)
}
}
queryOperation.queryCompletionBlock = { queryCursor, error in
dispatch_async(dispatch_get_main_queue(), {
completionHandler(report: report, error: error)
})
}
publicDatabase?.addOperation(queryOperation)
}
There seems to be something slowing down your main thread which introduces a delay in executing the capture block of your dispatch_async call. Is it possible that your code calls this record fetching function multiple times in parallel ? This would cause the NSData(contentsOfURL: asset.fileURL) processing to hog the main thread and introduce cumulative delays.
In any case, if only as a good practice, loading the image with NSData should be performed in the background and not on the main thread.

Get PHAsset from iOS Share Extension

I am developing a share extension for photos for my iOS app. Inside the extension, I am able to successfully retrieve the UIImage object from the NSItemProvider.
However, I would like to be able to share the image with my container app, without having to store the entire image data inside my shared user defaults. Is there a way to get the PHAsset of the image that the user has chosen in the share extension (if they have picked from their device)?
The documentation on the photos framework (https://developer.apple.com/library/ios/documentation/Photos/Reference/Photos_Framework/) has a line that says "This architecture makes it easy, safe, and efficient to work with the same assets from multiple threads or multiple apps and app extensions."
That line makes me think there is a way to share the same PHAsset between extension and container app, but I have yet to figure out any way to do that? Is there a way to do that?
This only works if the NSItemProvider gives you a URL with the format:
file:///var/mobile/Media/DCIM/100APPLE/IMG_0007.PNG
which is not always true for all your assets, but if it returns a URL as:
file:///var/mobile/Media/PhotoData/OutgoingTemp/2AB79E02-C977-4B4A-AFEE-60BC1641A67F.JPG
then PHAsset will never find your asset. Further more, the latter is a copy of your file, so if you happen to have a very large image/video, iOS will duplicate it in that OutgoingTemp directory. Nowhere in the documentation says when it's going to be deleted, hopefully soon enough.
I think this is a big gap Apple has left between Sharing Extensions and PHPhotoLibrary framework. Apple should've be creating an API to close it, and soon.
You can get PHAsset if image is shared from Photos app. The item provider will give you a URL that contains the image's filename, you use this to match PHAsset.
/// Assets that handle through handleImageItem:completionHandler:
private var handledAssets = [PHAsset]()
/// Key is the matched asset's original file name without suffix. E.g. IMG_193
private lazy var imageAssetDictionary: [String : PHAsset] = {
let options = PHFetchOptions()
options.includeHiddenAssets = true
let fetchResult = PHAsset.fetchAssetsWithOptions(options)
var assetDictionary = [String : PHAsset]()
for i in 0 ..< fetchResult.count {
let asset = fetchResult[i] as! PHAsset
let fileName = asset.valueForKey("filename") as! String
let fileNameWithoutSuffix = fileName.componentsSeparatedByString(".").first!
assetDictionary[fileNameWithoutSuffix] = asset
}
return assetDictionary
}()
...
provider.loadItemForTypeIdentifier(imageIdentifier, options: nil) { imageItem, _ in
if let image = imageItem as? UIImage {
// handle UIImage
} else if let data = imageItem as? NSData {
// handle NSData
} else if let url = imageItem as? NSURL {
// Prefix check: image is shared from Photos app
if let imageFilePath = imageURL.path where imageFilePath.hasPrefix("/var/mobile/Media/") {
for component in imageFilePath.componentsSeparatedByString("/") where component.containsString("IMG_") {
// photo: /var/mobile/Media/DCIM/101APPLE/IMG_1320.PNG
// edited photo: /var/mobile/Media/PhotoData/Mutations/DCIM/101APPLE/IMG_1309/Adjustments/FullSizeRender.jpg
// cut file's suffix if have, get file name like IMG_1309.
let fileName = component.componentsSeparatedByString(".").first!
if let asset = imageAssetDictionary[fileName] {
handledAssets.append(asset)
imageCreationDate = asset.creationDate
}
break
}
}
}

Resources