Swift: How to increase loading speed of images from documents? - ios

My app saved photo to the local documents folder and I used the UICollectionView to display all the image from that folder. But whenever I try to open the CollectionView it often took several seconds to open. I'm thinking that maybe the image files are too big, each photo is around 10MB. I also tried using thumbnails to display in collectionview but it still too slow. Any idea how to speed that up?
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SPCell
// Configure the cell
cell.imageView.image = loadImage(fileName: self.fileURLs[indexPath.row])
return cell
}
func loadImagesFromDocuments(){
let fileManager = FileManager.default
let documentsURL = NSHomeDirectory() + "/Documents/Secure/"
do {
fileURLs = try fileManager.contentsOfDirectory(at: URL(string: documentsURL)!, includingPropertiesForKeys: nil)
} catch {
print("Error while enumerating files : \(error.localizedDescription)")
}
}
func loadImage(fileName: URL) -> UIImage? {
do {
let imageData = try Data(contentsOf: fileName)
return UIImage(data: imageData)
} catch {
print("Error loading image : \(error)")
}
return nil
}

Current problem is that you load the image every time the cell appears so Instead of
var fileURLs = [URL]()
Make it
var fileImages = [UIImage]()
Then inside viewDidLoad
fileImages = fileURLs.compactMap { self.loadImage(fileName: $0) }

you are synchronously loading images while returning every cell at indexPath.
cell.imageView.image = loadImage(fileName: self.fileURLs[indexPath.row])
Instead, you can create a custom UICollectionViewCell implementation with a variable in global scope say imageURL.
after cell initialization, do this:
cell.imageURL = self.fileURLs[indexPath.row]
and, in ViewDidLoad() of your custom class, add this line:
self.imageView.image = loadImage(fileName: self.imageURL)
doing so, will lead to images being loaded in each custom implementation of cell unblocking the thread which is invoking the DataSource of your CollectionView.

Related

Download and cache images in UITableViewCell

Note: Please no libraries. This is important for me to learn. Also, there are a variety of answers on this but none that I found solves the issue nicely. Please don't mark as duplicate. Thanks in advance!
The problem I have is that if you scroll really fast in the table, you will see old images and flickering.
The solution from the questions I read is to cancel the URLSession
data request. But I do not know how to do that at the correct place
and time. There might be other solutions but not sure.
This is what I have so far:
Image cache class
class Cache {
static let shared = Cache()
private let cache = NSCache<NSString, UIImage>()
var task = URLSessionDataTask()
var session = URLSession.shared
func imageFor(url: URL, completionHandler: #escaping (image: Image? error: Error?) -> Void) {
if let imageInCache = self.cache.object(forKey: url.absoluteString as NSString) {
completionHandler(image: imageInCache, error: nil)
return
}
self.task = self.session.dataTask(with: url) { data, response, error in
if let error = error {
completionHandler(image: nil, error: Error)
return
}
let image = UIImage(data: data!)
self.cache.setObject(image, forKey: url.absoluteString as NSString)
completionHandler(image: image, error: nil)
}
self.task.resume()
}
}
Usage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let myImage = images[indexPath.row]
if let imageURL = URL(string: myImage.urlString) {
photoImageView.setImage(from: imageURL)
}
return cell
}
Any thoughts?
Swift 3:
Flickering can be avoided by this way:
Use the following code in public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
cell.photoImageView.image = nil //or keep any placeholder here
cell.tag = indexPath.row
let task = URLSession.shared.dataTask(with: imageURL!) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() {
if cell.tag == indexPath.row{
cell.photoImageView.image = UIImage(data: data)
}
}
}
task.resume()
By checking cell.tag == indexPath.row, we are assuring that the imageview whose image we are changing, is the same row for which the image is meant to be. Hope it helps!
A couple of issues:
One possible source of flickering is that while you're updating the image asynchronously, you really want to clear the image view first, so you don't see images for prior row of reused/dequeued table view cell. Make sure to set the image view's image to nil before initiating the asynchronous image retrieval. Or, perhaps combine that with "placeholder" logic that you'll see in lots of UIImageView sync image retrieval categories.
For example:
extension UIImageView {
func setImage(from url: URL, placeholder: UIImage? = nil) {
image = placeholder // use placeholder (or if `nil`, remove any old image, before initiating asynchronous retrieval
ImageCache.shared.image(for: url) { [weak self] result in
switch result {
case .success(let image):
self?.image = image
case .failure:
break
}
}
}
}
The other issue is that if you scroll very quickly, the reused image view may have an old image retrieval request still in progress. You really should, when you call your UIImageView category's async retrieval method, you should cancel and prior request associated with that cell.
The trick here is that if you're doing this in a UIImageView extension, you can't just create new stored property to keep track of the old request. So you'd often use "associated values" to keep track of prior requests.

Making multiple URL calls to retrieve images

I am new to iOS programming and I have a conceptual and a functional question. I tried looking at SO threads but did not get an exact question matching my situation.
I am building a simple screen where I display a list of user names along with their avatars - something like your typical Contacts screen.
I am using a UITableview for this purpose.
I first make a HTTP GET call to retrieve the list of users which returns a JSON with their names and the url to download their image. I then will store this info into Core Data and cache the images as well.
I am struggling with the part where I download the images and set it into the UIImageView.image.
Conceptually, which method should I use to get the names and the image urls - viewDidLoad or viewWillAppear? It seems to me that I should use viewWillAppear as in subsequent calls, I will be getting the list from Core Data and there is no network activity?
tableView:cellForRowAtIndexPath is the function that I use to get the image corresponding to each row. Is this correct?
Any help or pointing towards a duplicate question will help much! Thanks!
You can download all images Async process from below code...
Swift 3
private let downloadQueue = DispatchQueue(label: "me.readytoImage.downloadQueue", attributes: [])
class MainViewController: UIViewController {
fileprivate var photos = [URL]()
fileprivate var cache = NSCache<AnyObject, AnyObject>()
// MARK: - Image Downloading block
fileprivate func downloadPhoto(_ url: URL, completion: #escaping (_ url: URL, _ image: UIImage) -> Void) {
downloadQueue.async(execute: { () -> Void in
if let data = try? Data(contentsOf: url) {
if let image = UIImage(data: data) {
DispatchQueue.main.async(execute: { () -> Void in
self.cache.setObject(image, forKey: url as AnyObject)
completion(url, image)
})
}
}
})
}
Call Block
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! PhotoCell
let url = photos[indexPath.item]
//check cache images
let image = cache.object(forKey: url as AnyObject) as? UIImage
cell.imageView.backgroundColor = UIColor(white: 0.95, alpha: 1)
cell.imageView.image = image
//Downloading images
if image == nil {
downloadPhoto(url, completion: { (url, image) -> Void in
let indexPath_ = collectionView.indexPath(for: cell)
if indexPath == indexPath_ {
cell.imageView.image = image
}
})
}
return cell
}
otherwise you can also user Kingfisher SDK for download your images in Swift 3.
let url = json["image"] as? String
cell.imageProfile.kf.setImage(with: URL(string: url!))
Get the names and urls in viewDidLoad.
Make a custom UITableViewCell that takes a url for it's image. In didSet (for the url property), download the image and set the image for the UIImageView:
class CustomTableViewCell: UITableViewCell
var url: URL? = nil {
didSet {
//download and set image.
//example code can be found at the link below
}
}
}
Download image from url

How to remove a specific image from NSCache?

I have a collection view which has 12 images I retrieve from a network call. I use NSCache to cache them. I want to know how I can delete a specific image from there? I have provided some code below to show how I cached the images. Thanks!
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("imageReuseCell", forIndexPath: indexPath) as! ImageCollectionViewCell
let image = hingeImagesArray[indexPath.row]
//Start animating activity indicator
cell.actitivityIndicator.startAnimating()
if let imageURL = image.imageUrl {
if let url = NSURL(string: imageURL) {
//Check for cached images and if found set them to cells - works if images go off screen
if let myImage = HomepageCollectionViewController.imageCache.objectForKey(image.imageUrl!) as? UIImage {
cell.collectionViewImage.image = myImage
}else {
// Request images asynchronously so the collection view does not slow down/lag
let task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
// Check if there is data returned
guard let data = data else {
print("There is no data")
return
}
if let hingeImage = UIImage(data: data){
//Cache images/set key for it
HomepageCollectionViewController.imageCache.setObject(hingeImage, forKey: image.imageUrl!)
// Dispatch to the main queue
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Hide activity indicator and stop animating
cell.actitivityIndicator.hidden = true
cell.actitivityIndicator.stopAnimating()
//Set images to collection view
cell.collectionViewImage.image = hingeImage
})
}
})
task.resume()
}
}
}
return cell
}
NSCache is the smarter version of NSDictionary class which shares the same API for retrieving, adding or removing items.
Thus, you can delete an item from it using same method as if you do from a dictionary:
HomepageCollectionViewController.imageCache.removeObjectForKey(image.imageUrl!)
You can update your code to remove the image from cache that you are just about to show:
if let myImage = HomepageCollectionViewController.imageCache.removeObjectForKey(image.imageUrl!) as? UIImage {
// myImage was removed from cache.
cell.collectionViewImage.image = myImage
...

Image Loading takes too long using https or encoding/decoding db blobs

I've tried loading images from my database(encoding decoding medium blobs) and I've also tried storing the images on my server but it takes way too much time to load when I'm searching for 10+ users and attaching images to the cell. The search works extremely fast without images...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.myTable.dequeueReusableCellWithIdentifier("Cell")
if (self.countrySearchController.active)
{
cell!.textLabel?.text! = self.searchArray[indexPath.row]
if (cell!.textLabel!.text! != "")
{
let imageData = NSData(contentsOfURL: NSURL(string: "https://www.mywebsite.com/profileimages/\(cell!.textLabel!.text!).jpg")!)
if imageData != nil
{
let d = UIImage(data: imageData!)
cell!.imageView?.image = d
}
}
return cell!
}
else
{
cell!.textLabel!.text! = MyVariables.users[indexPath.row] as! String
return cell!
}
}
}
You are downloading image synchronously, thats why its taking long to perform, i have added Async GCD block to download image and set. Replace your cellForRowAtIndexPath method with following code.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.myTable.dequeueReusableCellWithIdentifier("Cell")
if (self.countrySearchController.active)
{
cell!.textLabel?.text! = self.searchArray[indexPath.row]
if (cell!.textLabel!.text! != "")
{
// *** GCD queue to perform Asynchronous Task ***
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Perform Image Downloading Task here
let imageData = NSData(contentsOfURL: NSURL(string: "https://www.mywebsite.com/profileimages/\(cell!.textLabel!.text!).jpg")!)
dispatch_async(dispatch_get_main_queue(), ^{
// update your Imageview with Downloaded Image
if imageData != nil
{
let d = UIImage(data: imageData!)
cell!.imageView?.image = d
}
});
});
}
return cell!
}
else
{
cell!.textLabel!.text! = MyVariables.users[indexPath.row] as! String
return cell!
}
}
Even to enhance performance you can save downloaded image into your app's Documents directory and load image from it next onwards.
In your case you can save Images into NSCache and display images from it once its downloaded.
You should load your images using multitasking. Easiest way to do this is using SDWebImage framework. It's also allows caching and setting placeholders. All what you need with this framework is something like this:
cell.photoView.sd_setImageWithURL(NSURL(string: friend.imageUrl), placeholderImage: placeHolderImage)

Swift - Adding image to tableview cell from asset library

I am trying to add an image to a tableview cell but not having much luck getting it to display.
The image is being loaded from the file system (I println() the result) as a UIImage but I cannot seem to get it into the cell. Placing a println() after the closure shows me that the images are all loaded after the cell has been returned.
Here is my code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
let note = notes[indexPath.row]
let nsURL = NSURL(string: note.valueForKey("url") as! String)!
var loadError: NSError?
var image: UIImage?
assetsLibrary!.assetForURL(nsURL, resultBlock: { (asset) -> Void in
if let ast = asset {
let iref = ast.defaultRepresentation().fullResolutionImage().takeUnretainedValue()
image = UIImage(CGImage: iref)
println("The loaded image: \(image)")
}
}, failureBlock: {(error) -> Void in
loadError = error
})
cell.textLabel!.text = note.valueForKey("title") as? String
cell.imageView!.image = image
return cell
}
When I replace the closure with the following to load a image from the project itself it shows in the table. This leads me to believe it not due to an issue with the way the story board is set up.
UIImage(named: transportItems[indexPath.row])
Any help would be much appreciated.
It doesn't work that way. cellForRowAtPathIndex is a synchronous routine. assetForURL is an asynchronous routine. It will return the data long time after cellForRowAtPathIndex has returned.
Here's what you should do: Have a method cachedAssetForURL which returns the asset immediately, or returns nil. If it returns an asset, store it. Remember this has to be as efficient as possible, because this is called while the user scrolls up and down through the images.
If the method returns nil, trigger a download in the background. When that download finishes, don't even try to store the image in the cell - by this time, the same cell could display an entirely different object! Instead store the data so that cachedAssetForURL will be able to return the asset, and invalidate the row of your table view.
Fire a notification in the block (it's asynchronous), manage it by setting your imageview and reload your view.
The underlying issue I was encountering, as pointed out by #gnasher729, was the mishmash of synchronous and asynchronous calls. Rewriting the code to take this into account and to include a cache, I have the following working solution.
var cachedImages = [String:UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! UITableViewCell
let note = notes[indexPath.row]
let url = note.valueForKey("url") as! String
cell.textLabel!.text = note.valueForKey("title") as? String
cell.imageView!.image = UIImage(named: "note-icon")
if let image = cachedImages[url]{
cell.imageView!.image = image
} else {
let nsURL = NSURL(string: url)!
var loadError: NSError?
assetsLibrary!.assetForURL(nsURL, resultBlock: { (asset) -> Void in
if let ast = asset {
let image = UIImage(CGImage: ast.defaultRepresentation().fullResolutionImage().takeUnretainedValue())
self.cachedImages[url] = image
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
}
},failureBlock: {(error) -> Void in
loadError = error
})
}
return cell
}
This could be further improved to take more off of the main thread.

Resources