Making multiple URL calls to retrieve images - ios

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

Related

Images in tableview load another for a few seconds

I have an image in tableview that is downloaded from a Json, everything works perfect but when scrolling before seeing the corresponding image it loads another for a few seconds (these images are those that are already visible in the table).
The structure of my data is:
struct Data: Decodable {
let name: String
let img: String
let phone: String
let linktaller: String
let web: String
}
The code of my cell where the image is loaded is:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? AseguradorasTableViewCell else { return UITableViewCell() }
cell.titleLbl.text = company[indexPath.row].name
.
.
.
// load image
if let imageURL = URL(string: company[indexPath.row].img) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: imageURL)
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.myImage.image = image
}
}
}
}
return cell
}
The function to load the data is:
func downloadJSON() {
let url = URL(string: "http://myserver.com/data.json")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error == nil {
do {
self.company = try JSONDecoder().decode([Data].self, from: data!)
print(self.company)
DispatchQueue.main.async {
self.tableView.reloadData()
self.refreshControl.endRefreshing()
}
} catch let jsonError{
print("error + \(jsonError)")
}
}
}.resume()
}
See image for more detail:
Any suggestions are welcome to fix this problem.
In UITableView dequeueReusableCell- Each UITableViewCell will be reused several times with different data(image).
In your case, every cellForRowAt is called, the image will be load from server so it will have delay.
Solution: You must to cache image with url in local app when the image load finish.
(1)- Use SDWebImage - with cache support
(2)- You can save image in a array -> in cellForRowAt load from this array if existed and load from server if does not exist
(image from internet)
Add the following class for cache image support:
class ImageLoader {
var cache = NSCache<AnyObject, AnyObject>()
class var sharedInstance : ImageLoader {
struct Static {
static let instance : ImageLoader = ImageLoader()
}
return Static.instance
}
func imageForUrl(urlString: String, completionHandler:#escaping (_ image: UIImage?, _ url: String) -> ()) {
let data: NSData? = self.cache.object(forKey: urlString as AnyObject) as? NSData
if let imageData = data {
let image = UIImage(data: imageData as Data)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
return
}
let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL.init(string: urlString)!) { (data, response, error) in
if error == nil {
if data != nil {
let image = UIImage.init(data: data!)
self.cache.setObject(data! as AnyObject, forKey: urlString as AnyObject)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
}
} else {
completionHandler(nil, urlString)
}
}
downloadTask.resume()
}
}
In the cell, load the image as follows:
// Load image
let fimage = company[indexPath.row].img
ImageLoader.sharedInstance.imageForUrl(urlString: fimage, completionHandler: { (image, url) in
if image != nil {
cell.myImage.image = image
}
})
With that, the download of the images should work correctly
Because of when ever the cell is showing, you download the image from internet by
let data = try? Data(contentsOf: imageURL)
You should
Check if image in imageURL has cached or not
If cached, load image from local
If not cache, download it from internet, then cache it.
Or just simple using SDWebImage or anything else, it will auto check the step 1 to 3 for you :D
For example by using SDWebImage
import SDWebImage
imageView.sd_setImage(with: URL(string: "your_image_url"))
This is a classic cell reuse problem. You should install a placeholder image, or nil, into the image view of each cell in your tableView(cellForRowAt:) method before you begin the download. That will clear out the previous image that was installed into the cell, and then the async download can run in the background and install the image once it's done loading.
To resolve similar issues, I changed my code to coordinate the downloading of images with the creation of tableView cells, storing the images in a local array.
I create a dictionary array to hold the downloaded images, using the url string as the key:
imagesArray = [String:UIImage]()
Then, at the point in the code where each image completes downloading, I add the image to the array and insert one new row into the tableView:
imagesArray.updateValue(UIImage(data: data!)!, forKey: imageURL as! String)
tableView.beginUpdates()
tableView.insertRows(at:[IndexPath(row: imagesArray.count-1, section: 0)], with: .automatic)
tableView.endUpdates()
tableView.reloadData()
I also maintain a separate array of information elements for each image, including the image url string as one element. This allows me to present the correct items in the tableView cell:
cell.itemNameLabel.text = itemRecords[indexPath.row].itemName
cell.itemImage.image = imagesArray[itemRecords[indexPath.row].imageURL]
While the images are downloading, I present a progress indicator spinner.
Once the images are all downloaded and are loaded into the imagesArray, there is NO delay in presenting as the user scrolls up and down to view the listed cells, and reused cells are loaded with the correct images.

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.

Swift Image Download TableView

I'm trying to fix a problem with downloading an image asynchronously in a TableView in Swift. This is my Problem: I download the image from a url asynchronously, but if I scroll quickly the TableView my pictures begin to rotate.(The images alternate until the correct one appears).
This is my Download Async Code and imageCache
let imageCache = NSCache()
//DOWNLOAD Image ASINC
extension UIImageView {
public func imageFromServerURL(url: String){
if(imageCache.objectForKey(url) != nil){
self.image = imageCache.objectForKey(url) as? UIImage
}else{
let sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
let task = session.dataTaskWithURL(NSURL(string: url)!, completionHandler: { (data, response, error) -> Void in
if error == nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: url)
self.image = downloadedImage
}
})
}
else {
print(error)
}
})
task.resume()
}
}
}
and Which I recall in the TableView so:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("record_charts", forIndexPath: indexPath) as! myTableViewCell
let url_img = "https://image/download.jpg"
cell.immagine.imageFromServerURL(url_img)
return cell
}
This is the gif to show you the problem better
This is due to the reuse mechanism of iOS's table view.
You can make some modification to your code to fix this:
class AsyncImageView: UIImageView {
private var currentUrl: String? //Get a hold of the latest request url
public func imageFromServerURL(url: String){
currentUrl = url
if(imageCache.objectForKey(url) != nil){
self.image = imageCache.objectForKey(url) as? UIImage
}else{
let sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
let task = session.dataTaskWithURL(NSURL(string: url)!, completionHandler: { (data, response, error) -> Void in
if error == nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let downloadedImage = UIImage(data: data!) {
if (url == currentUrl) {//Only cache and set the image view when the downloaded image is the one from last request
imageCache.setObject(downloadedImage, forKey: url)
self.image = downloadedImage
}
}
})
}
else {
print(error)
}
})
task.resume()
}
}
}
Note #1: I was whiteboard coding the modification, so not sure if the code has correct syntax.
Note #2: Instead of declaring a new subclass of UIImageView, you can use associated objects.
Note #3: I strongly suggest you use AlamoFireImage, it has a category for UIImageView which is exactly what you need in this case (and future cases too).
This is because of cell reuse. I will try to explain. Suppose you have 10 cells each having a different image (Images 1 to 10) but only 5 cells fit on the screen. The table starts to load and the first cell requests image 1 to be put in an image view and that starts happening in the background but the table is scrolled before the background loading of the image finishes and the first cell is scrolled of the screen. Now that cell will be reused let's say by the sixth cell which requests image 6. You background request for image 1 then finishes and as it is still holding a reference to the cell image 1 is put in the image view. Then your background process for image 6 finishes and that replaces the image with the new version. It will be even worse if image 6 finishes loading before image 1 as you then get image 6 put in the cell and it's then replaced by image 1.
What you need to do is implement some method so that when the image is available you can check that it is still the correct one to use. I don't think you are going to be able to do that making the function an extension of ImageView so you probably need some kind of central image provider or something similar.
You need to add cancellation method in UIImageView extension, and call it or in tableView(_:willDisplay:forRowAt:) or in prepareForReuse() of UITableViewCell
or you can cancel request as in SDWebImage's web cache

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
...

caching images in collectionViewCell in Swift?

I have images in my collectionViewCell's that are fetched and parsed via NSURLRequest, how do I cache these images so they don't have to start a new request with every single appearance/disappearance of the view?
here is my code that fetches the images:
class funnyPicture: NSObject {
var pfPicture : PFObject
var coverImage : UIImage!
init(pfPicture: PFObject) {
self.pfPicture = pfPicture
}
func fetchCoverImage(completion: (image: UIImage?, error: NSError?) -> Void) {
let urlString = self.pfPicture["funnyPictures"] as! String
let url = NSURL(string: urlString)
let request = NSURLRequest(URL: url!)
let queue = dispatch_get_main_queue()
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { (response: NSURLResponse?, data: NSData?, error: NSError?) in
if error == nil {
self.coverImage = UIImage(data: data!)
completion(image: self.coverImage, error: nil)
} else {
completion(image: nil, error: error)
}
}
}
}
and here is my collectionView code that parse the images to the collectionViewCell's:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! MyCollectionViewCell
// Configure the cell
let book = self.books[indexPath.row]
let coverImage = book.coverImage
if coverImage == nil {
book.fetchCoverImage({ (image, error) -> Void in
if self.collectionView != nil {
collectionView.reloadItemsAtIndexPaths([indexPath])
}
})
} else {
dispatch_async(dispatch_get_main_queue()){
let imageView = cell.imageView
imageView.image = book.coverImage
}
};
if book.coverImage == nil {
cell.imageView.userInteractionEnabled = false
cell.userInteractionEnabled = false
}else {
cell.imageView.userInteractionEnabled = true
cell.userInteractionEnabled = true
}
return cell
}
While I've received references to third party frameworks, I haven't received any answer on how to implement them with the code I have provided in the question, or even an answer using apples already implemented caching mechanism.. The reason I put the code in the question was for use in an answer.. Thank you.
Here is an example for your collection view cell:
import UIKit
let imageCache = NSCache<AnyObject, AnyObject>.sharedInstance
class myCell: UICollectionViewCell {
#IBOutlet public weak var myImageView: UIImageView?
private var imageUrlString: String?
private var downloadTask: URLSessionDownloadTask?
public var imageURL: URL? {
didSet {
self.downloadItemImageForSearchResult(imageURL: imageURL)
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
public func downloadItemImageForSearchResult(imageURL: URL?) {
if let urlOfImage = imageURL {
if let cachedImage = imageCache.object(forKey: urlOfImage.absoluteString as NSString){
self.myImageView!.image = cachedImage as? UIImage
} else {
let session = URLSession.shared
self.downloadTask = session.downloadTask(
with: urlOfImage as URL, completionHandler: { [weak self] url, response, error in
if error == nil, let url = url, let data = NSData(contentsOf: url), let image = UIImage(data: data as Data) {
DispatchQueue.main.async() {
let imageToCache = image
if let strongSelf = self, let imageView = strongSelf.myImageView {
imageView.image = imageToCache
imageCache.setObject(imageToCache, forKey: urlOfImage.absoluteString as NSString , cost: 1)
}
}
} else {
//print("ERROR \(error?.localizedDescription)")
}
})
self.downloadTask!.resume()
}
}
}
override public func prepareForReuse() {
self.downloadTask?.cancel()
myImageView?.image = UIImage(named: "ImagePlaceholder")
}
deinit {
self.downloadTask?.cancel()
myImageView?.image = nil
}
}
Don't forget to make an extension for NSCache
Like this:
import Foundation
extension NSCache {
class var sharedInstance: NSCache<NSString, AnyObject> {
let cache = NSCache<NSString, AnyObject>()
return cache
}
}
Use NSCache and NSOperationQueue to manage your image loading. There's a good post outlining the technique at https://stackoverflow.com/a/12721899/5271191 (It's Objective-C, but the technique is the same for Swift.)
I highly recommend you to use a clean in place replacement/extension for UIImageView, that will manage caching of the image all transparently to you and avoid unwanted complexity of maintaining operation queues, etc.
If in memory caching suffices your needs - check this out-
https://github.com/nicklockwood/AsyncImageView
If you want persistent caching, then this one will do-
https://github.com/rs/SDWebImage
HTH.
I have images in my collectionViewCell's that are fetched and parsed
via NSURLRequest, how do I cache these images so they don't have to
start a new request with every single appearance/disappearance of the
view?
The URL loading system already provides a cache. Take a look at the docs for NSURLCache. If the resources you need aren't already being sufficiently cached, you probably only need to adjust the disk space allocated to the URL cache for your app.
You should also take a look at the headers (cache-control, expires, etc.) that come back with your resources to make sure that they're not preventing caching. Here's a short tutorial on cache-related headers.
You should use a specialized framework for that. I would not recommend using SDWebImage, it is very outdated and is not stable.
Take a look at those two libraries that are up to date with iOS platform:
DFImageManager - advanced framework written in Objective-C but featuring nullability annotations (works great with Swift). Here's a list of things that make it better, than SDWebImage. Disclosure: it's written by me, opinion might be biased.
Kingfisher - lightweight library written in Swift. Similar to SDWebImage, but has much less features that SDWebImage and DFImageManager.
I have created a library using swift 2 to do the request for image and cache it. it's very simple just give it a try.
https://github.com/georgehadly/GHImageCaching
all you can do is something like this ,
viewImg.getCachedImage("geo", URI: NSURL(string: "https://s-media-cache-ak0.pinimg.com/236x/8e/5a/98/8e5a98795dc2c5322cac97343a6cad6d.jpg")!) { (done) -> Void in
if(done){
// your extra
}
}
in case you want to delete all cached images
UIImageView.deleteAllCaching()

Resources