Asynchronous image loading from Core Data in Swift - ios

I am building an application which uses a Core Data database to store its data on products. These products are displayed in a UICollectionView. Each cell in this collection view displays basic information on the product it contains, including an image.
Although the cells are relatively small, the original images they display are preferably quite large as they should also be able to be displayed in a larger image view. The images are loaded directly from Core Data in my CellForItemAtIndexPath: Method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionWineCell
var current: Product!
//product details are set on labels
//image is set
if current.value(forKey: "image") != nil {
let image = current.value(forKey: "image") as! WineImage
let loadedImage = UIImage(data: image.image)
cell.imageview.image = loadedImage
} else {
cell.imageview.image = UIImage(named: "ProductPlaceholder.png")
}
return cell
}
When the collection of products grows, scrolling gets bumpier and a lot of frames are dropped. This makes sense to me, but so far I haven't found a suitable solution. When looking online a lot of documentation and frameworks are available for asynchronous image loading from a URL (either online or a file path), but doing this from Core Data does not seem very common.
I have already tried doing it using an asynchronous fetch request:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName:"ProductImage")
fetchRequest.predicate = NSPredicate(format: "product = %#", current)
let asyncRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { results in
if let results = results.finalResult {
let result = results[0] as! ProductImage
let loadedImage = UIImage(data: result.image)
DispatchQueue.main.async(execute: {
cell.wineImage.image = loadedImage
})
}
}
_ = try! managedObjectContext.executeRequest(asyncRequest)
However, this approach does not seems to smoothen things out either
QUESTION
When displaying large sets of data, including images, from Core Data, how does one load images in a way that it does not cause lags and frame drops in a UICollectionView?

If the images can be, as you say, quite large, a better approach is not to save them in Core Data but to put them in files. Store the filename in Core Data and use that to look up the file.
But that's not the immediate problem. Even with that you'll get slowdowns from spending time opening and decoding image data. A better approach is, basically, don't do that. In your collection views the images are probably displayed much smaller than their full size. Instead of using the full size image, generate a thumbnail at a more appropriate size and use that in the collection view. Do the thumbnail generation whenever you first get the image, whether from a download or from the user's photo library or wherever. Keep the thumbnail for use in the collection view. Only use the full size image when you really need it.
There are many examples online of how to scale images, so I won't include that here.

Related

Add and remove UIImage from array in UICollectionView

My app loads pictures in to the UICollectionView from iPhone gallery, Facebook and Instagram. Everything works fine but I have a small problem and I'm stuck.. The user can selected some of the images (from gallery, Fb, or IG) and load it to the another ViewController, With Facebook and Instagram the thing is simple because when user selected the images I load the URL to the array and when he deselect and just simply remove this URL from an array. The problem appears with an iPhone gallery. He can selected the images but he can not unsealed (I just can't remove a deselected image from the array).
This is the code for ViewController which is responsible for selecting and "deselecting" images from gallery.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
_selectedCells.add(indexPath)
collectionView.reloadItems(at: [indexPath])
CounterManager.addPhotos()
// These lines should add selected images to the array
let asset = fetchResult.object(at: indexPath.row)
let image = getUIImage(asset: asset)
tempUIImageArray.append(image!)
CounterManager.updateText(label: numberOfPickslabel)
GalleryManager.selectedGalleryImages.append(image!)
if CounterManager.flag && CounterManager.counter >= 20 {
present(CounterManager.alert(), animated: true, completion: nil)
} else if CounterManager.flag == false && CounterManager.counter >= 40 {
present(CounterManager.alert(), animated: true, completion: nil)
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
collectionView.allowsSelection = true
_selectedCells.remove(indexPath)
collectionView.reloadItems(at: [indexPath])
collectionView.deselectItem(at: indexPath, animated: true)
CounterManager.subPhotos()
CounterManager.updateText(label: numberOfPickslabel)
//These lines should remove images from the Array
let asset = fetchResult.object(at: indexPath.row)
let image = getUIImage(asset: asset)
GalleryManager.selectedGalleryImages = GalleryManager.selectedGalleryImages.filter({$0 != image})
}
GalleryManager.selectedGalleryImages is the global array for all the images (from gallery, Fb and IG).
The getUIImage is the method that converts PHAsset to UIImage
It seems that the selected and deselected image keeps changing some data. Below is a picture were I am selecting and deselecting the same image. I do not understand why it's happening..
I will be very grateful for all kind of help.
Thanks M.
You're getting an actual UIImage out of the gallery and storing it in an array, not a unique text reference to the image location. I suspect this is something to do with the way GalleryManager loads images (because you're just comparing raw image data using !=, which doesn't work). But without seeing the code, I can't be sure.
I'd suggest using ALAssetsLibrary instead to get a reference URL to the image, and storing that in your array - this way you can guarantee that the image ref is unique, and you can easily remove it from your array.
Some side notes:
Storing binary data in an array is going to eat memory like crazy. It is not efficient, and you're going to run into problems (especially on older devices).
This code is likely to break whenever asset is nil:
let image = getUIImage(asset: asset)
tempUIImageArray.append(image!)
This is a good candidate for guard or if x let:
guard let image = getUIImage(asset: asset) else { return }
tempUIImageArray.append(image)
Your problem is in comparing the image with the other to filter and remove the the deselected one here
GalleryManager.selectedGalleryImages = GalleryManager.selectedGalleryImages.filter({$0 != image})
This can't be valid as image comparison is a way far from that , so you should associate each image with id or index to select or deselect according to that
The problem is that you compare two different UIImage instances, even though they are created from the same data, they won't be evaluated as the same.
Instead of keeping the selected images in a separate array (GalleryManager.selectedGalleryImages), keep just the original array of images, and to detect which are selected, create an array of selected images' indexes (e.g., GalleryManager.selectedGalleryImagesIndixes) in the original array of images. That way you can have access to the selected images by accessing the original array through the index and you want have problem of comparing two UIImages, because you will work witn Ints.
UPDATE
Based on the UIImage documentation:
Comparing Images
The isEqual(_:) method is the only reliable way to determine whether two images contain the same image data. The image objects you create may be different from each other, even when you initialize them with the same cached image data. The only way to determine their equality is to use the isEqual(_:) method, which compares the actual image data. Listing 1 illustrates the correct and incorrect ways to compare images.
Listing 1
Comparing two images
let image1 = UIImage(named: "MyImage")
let image2 = UIImage(named: "MyImage")
if image1 != nil && image1!.isEqual(image2) {
// Correct. This technique compares the image data correctly.
}
if image1 == image2 {
// Incorrect! Direct object comparisons may not work.
}
Therefore your problem might be also solvable by swapping comparisong using == and != with .isEqual(_:).
Change:
GalleryManager.selectedGalleryImages = GalleryManager.selectedGalleryImages.filter({$0 != image})
To:
GalleryManager.selectedGalleryImages = GalleryManager.selectedGalleryImages.filter({ !($0.isEqual(image)) })

Why UITableViewCell displays previous value at first and just later updates self?

I have a UITableView with a UIImageView on it. On these imageViews I'm downloading images from the server and display them. The problem is that when I scroll my UITableView, at first it shows another cell's image and just later(in 2-3 seconds) it updates the cell's imageView to the properly image.
How can I fix it? I've tried to use background and main thread mix, like that:
extension UIImageView {
func getDataFromUrl(url:String, completion: ((data: NSData?) -> Void)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) in
completion(data: NSData(data: data!))
}.resume()
})
}
func downloadImage(url:String){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
self.getDataFromUrl(url) { data in
dispatch_async(dispatch_get_main_queue()) {
self.contentMode = UIViewContentMode.ScaleAspectFill
self.image = UIImage(data: data!)
}
}
})
}
}
and use these extension on my cellForRowAtIndexPath as:
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
but it doesn't help me. It works as previously. What I do wrong? If you can, please give a description(explanation) to your answer. It will be more useful! Thanks!
UPDATE
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: newsCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! newsCell
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
return cell
}
What is actually happening in your code is that all cells are given a reuse identifier, meaning that they all get put into the same object pool. This means that when you use [tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier]; that you are retrieving a cell from this one pool. It already has a image set up which you need to clear.
Before setting up the cell to download Image you need to clear the image before.
cell.coverImage.image = nil
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
Let me explain the problem.
Imagine you have the first row, i call it ROW_A object. ROW_A should download and display image with URL_1, by using a thread THREAD_1. When it's in you cellForRow function, you started THREAD_1 to download the image for ROW_A
When you scroll, the first row become invisible, so the ROW_A view object will be reused for displaying another row, for example, row 11th. In the cellForRow, the same process as before, you started thread THREAD_11 to download the image with URL_11 and display that image using ROW_A object (as i said before, the ROW_A object is reused)
The problem is, after a long time downloading IMAGE_1 from URL_1, THREAD_1 finishes its works and set IMAGE_1 to ROW_A. You will see IMAGE_1 being showed.
After that, THREAD_11 finishes too, it set IMAGE_11 to the ROW_A. But ROW_A is displaying IMAGE_1, so that why you see IMAGE_1 then IMAGE_11 on row 11th.
To fix this problem, you should check the image view to see if it's assigned to other downloading thread. I mean, if THREAD_1 finish, THREAD_1 should know that ROW_A is not belong to THREAD_1 anymore. You can create a UIImageView subclass with a url:String property, then when the downloading thread finishes, you compare the url string of downloaded image with value of url property, if it's different, don't show the image.
Another option is using AFNetworking for downloading and showing the remote images. That's very simple and you can find many tutorial on Google.

How to create UITableViewCells with complex content in order to keep scrolling fluent

I am working on a project where I have a table view which contains a number of cells with pretty complex content. It will be between usually not more than two, but in exceptions up to - lets say - 30 of them. Each of these complex cells contain a line chart. I am using ios-charts (https://github.com/danielgindi/ios-charts) for this.
This is what the View Controller's content looks like:
The code I use for dequeuing the cells in the viewController's cellForRowAtIndexPath method is kind of the following:
var cell = tableView.dequeueReusableCellWithIdentifier("PerfgraphCell", forIndexPath: indexPath) as? UITableViewCell
if cell == nil {
let nib:Array = NSBundle.mainBundle().loadNibNamed("PerfgraphCell", owner: self, options: nil)
cell = nib[0] as? FIBPerfgraphCell
}
cell.setupWithService(serviceObject, andPerfdataDatasourceId: indexPath.row - 1)
return cell!
and in the cell's code I have a method "setupWithService" which pulls the datasources from an already existing object "serviceObject" and inits the drawing of the graph, like this:
func setupWithService(service: ServiceObjectWithDetails, andPerfdataDatasourceId id: Int) {
let dataSource = service.perfdataDatasources[id]
let metadata = service.perfdataMetadataForDatasource(dataSource)
if metadata != nil {
if let lineChartData = service.perfdataDatasourcesLineChartData[dataSource] {
println("starting drawing for \(lineChartData.dataSets[0].yVals.count) values")
chartView.data = lineChartData
}
}
}
Now the problem: depending on how many values are to be drawn in the chart (between 100 and 2000) the drawing seems to get pretty complex. The user notices that when he scrolls down: as soon as a cell is to be dequeued that contains such a complex chart, the scrolling gets stuck for a short moment until the chart is initialized. That's of course ugly!
For such a case, does it make sense to NOT dequeue the cells on demand but predefine them and hold them in an array once the data that is needed for graphing is received by the view controller and just pull the corresponding cell out of this array when it's needed? Or is there a way to make the initialization of the chart asynchronous, so that the cell is there immediately but the chart appears whenever it's "ready"?
Thanks for your responses!
What you're trying to do is going to inevitably bump into some serious performance issues in one case or another. Storing all cells (and their data into memory) will quickly use up your application's available memory. On the other hand dequeueing and reloading will produce lags on some devices as you are experiencing right now. You'd be better off by rethinking your application's architecture, by either:
1- Precompute your graphs and export them as images. Loading images into and off cells will have much less of a performance knockoff.
2- Make the table view into a drill down menu where you only show one graph at a time.
Hope this helps!

Allow a user to select local images within a app Swift

I have a folder with about 30 image icons. I am trying to allow for the user to select one of the 30 "local" images as their profile picture. I am looking to come up with the best way to do this, however most tutorials are for accessing the camera roll and allowing the user to upload their photos.
I am looking to have a method, maybe a UICollectionView and allow them to select a image that will be the users icon. I understand how to pull in the images from the iPhone itself but the server that I am using is not coded currently to allow this process to happen.
What is the best way to use images that are within the app and allow them to be placed into a image view?
The UICollectionView is the way to go.
You'll need to first load all the local avatar filenames. The following example will load all images in the app directory which start with avatar-, ignoring all retain #2x.png files.
func getAvatarFilenames() -> Array<String> {
var avatarFileNames = Array<String>()
var paths = NSBundle.mainBundle().pathsForResourcesOfType("png", inDirectory: nil)
for path in paths {
var imageName = path.lastPathComponent
// ignore retina images as when the uiimage loads them back out
// it will pick the retina version if required
if (imageName.hasSuffix("#2x.png")) {
continue
}
// only add images that are prefixed with 'avatar-'
if (imageName.hasPrefix("avatar-")) {
avatarFileNames.append(imageName)
}
}
return avatarFileNames
}
You can then create a UICollectionView which loads each of the avatar filenames. Something like this to configure each cell (assuming your AvatarCell has an image with tag 1000 - or better yet, a UICollectionViewCell subclass).
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("AvatarCell", forIndexPath: indexPath) as UICollectionViewCell
var avatarImageView = cell.viewWithTag(1000) as UIImageView
avatarImageView.image = UIImage(named: avatarFileNames[indexPath.row])

UITableView Loading Images Slowly

The TableView fetches images from parse api. However, when scrolling through the tableView, there is lag while the image is loading.
Is there is an efficient way to decrease lag? Some way to load images before (or even after) the user scrolls to the cell? I'm not too familiar with dealing with asynchronous calls.
Fetch Image Function
func fetchImage(restaurantArray: PFObject!, completionHandler: ImageCompletionHandler!){
var imageReference = restaurantArray["PhotoUploaded"] as PFFile
imageReference.getDataInBackgroundWithBlock{
(data, error) -> Void in
if (error != nil){
completionHandler(image: nil, error: error)
}else{
let image = UIImage(data: data)
completionHandler(image: image, error: nil)
}
}
}
TableView Cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
cell.nameLabel.text = restaurantNames[indexPath.row]
// check if image is empty
if foodPhotoObjects.isEmpty {
} else {
self.fetchImage(self.foodPhotoObjects[indexPath.row] as? PFObject, completionHandler: {
(image, error) -> () in
if image != nil {
cell.mainRestaurantImageView.image = image
cell.mainRestaurantImageView.contentMode = .ScaleAspectFill
cell.mainRestaurantImageView.clipsToBounds = true
}else{
//alert user: no image or put placeholder image
}
})
}
return cell
}
You can get this functionality for free by using PFImageView, less code for you to write to achieve the desired effect. Just call loadInBackground: after setting the file property.
https://parse.com/docs/ios/api/Classes/PFImageView.html
As for UI lag, profile in Instruments to determine what is so expensive that it is causing significant noticeable lag.
A couple of thoughts:
You should confirm whether this completion block is running on the main thread (examine NSThread.isMainThread) and if not, make sure to dispatch the update of the image view back to the main thread, e.g. dispatch_async(dispatch_get_main_queue()) { ... }. Trying to perform updates of UIKit controls from a background thread can end up with inconsistent behavior, so make sure you're only updating the UI form the main thread.
You should consider the implications of the user quickly scrolling down and back up in the table view. Your code will result in re-fetching images previously retrieved.
Sometimes the underlying NSURLCache will handle this gracefully, caching the images for you. In other cases it will not. (Sadly, the caching of network requests is dictated by some fairly opaque criteria, and depending upon the server implementation, you might not have a lot of control over that.)
You might want to confirm your behavior, and implement your own caching (e.g. NSCache) of previously downloaded images and checking that before retrieving images again.
I would make sure to test this process in conjunction with the network link conditioner so you can confirm the behavior of the app in less than ideal environments (i.e. real-world situations).
Unrelated to your question there are two issues with updating the cell asynchronously:
If the cell scrolled out of view by the time the image retrieval completed, the cell could have been reused, and the cell may have been reused for another row in the table. (Test this by running the app with the network link conditioner degrading the network response time to something more lke a poor cellular connector.) This problem can manifest itself as a "flickering" of images or replacing images with images associated with other cells.
You generally solve this by calling tableView.cellForRowAtIndexPath(indexPath) (a UITableView method, not to be confused with the similarly named UITableViewDataSource method), which will return nil if this index path has scrolled off of the screen, and will return the UITableViewCell if the cell is visible. Use this rather than the previously established cell value when doing the final asynchronous population of the cell.
If there is any possibility that the table could have been reloaded (i.e. the data source changed) during the process of retrieving the image asynchronously, you might actually want to go back to the model, identifying the appropriate index path associated with the image you just downloaded asynchronously.
Bottom line, imagine some degenerate situation where it took a minute or so to retrieve the image (unlikely, just imagine). Think through the contingencies associated with that (the cell scrolled off and subsequently reused, the indexPath for the image might not longer be valid, etc.). Make sure you handle these contingencies gracefully.

Resources