I have read a similar post in Reusable cell old image showing and I am still getting the same issues. Essentially I have a TableView that downloads images from amazon s3 as you scroll down everything works good until you get to about the 12th or 13th image. What happens is that the image in the row before shows up in the new row for about 2 seconds while the new image is being downloaded . This is my code ( I'm still new at swift and learning IOS) . The stream_image_string as the full URL to download the images and PicHeight is an integer saved with the image height since every image usually has a different height .
var Stream_Cache = NSCache<AnyObject, AnyObject>()
var stream_image_string = [String]()
var PicHeight = [Int]()
This below is inside the UITableViewCell, first I check if there is a url which it will contain more than 0 characters . I then check if the image/url is saved in the Cache if not then I download it .
if stream_image_string[indexPath.row].characters.count > 0 {
if let image = Stream_Cache.object(forKey: stream_image_string[indexPath.row] as AnyObject) as? UIImage {
DispatchQueue.main.async(execute: { () -> Void in
cell.stream_image.image = image
})
} else {
if cell.stream_image != nil {
let strCellImageURL = self.stream_image_string[indexPath.row]
let imgURL: NSURL = NSURL(string: strCellImageURL)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
cell.Stream_Image_Height.constant = CGFloat(Float(cell.pic_height!))
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
if data != nil {
cell.stream_image.image = UIImage(data: data!)
} else {
cell.Stream_Image_Height.constant = 0
cell.stream_image.image = nil
}
})
});
task.resume()
}
}
} else {
cell.Stream_Image_Height.constant = 0
}
In my UITableViewCell file I set the image to a default image in case it wasn't done loading the new image but it hasn't worked
class HomePageTVC: UITableViewCell {
#IBOutlet weak var stream_image: UIImageView!
var pic_height: Int?
override func awakeFromNib() {
super.awakeFromNib()
stream_image.image = #imageLiteral(resourceName: "defaultImage")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
any suggestions would be great
You're facing a pretty common cell reuse issue. When you dequeue a cell that was used before, it may already have an image installed in its image view. Set the image to nil before beginning an async download:
if let imageString = stream_image_string[indexPath.row].characters,
!imageString.isEmpty {
if let image = Stream_Cache.object(forKey: imageString) as? UIImage {
cell.stream_image.image = image
} else {
//Clear out any old image left in the recycled image view.
cell.stream_image.image = nil
//Your code to download the image goes here
}
}
Note that there's no need to wrap the cell.stream_image.image = image code in a call to DispatchQueue.main.async(). That code will be run on the main thread.
You do, however, need the second DispatchQueue.main.async() wrapper around the code inside the dataTask's completionHandler, since URLSession's completion handers are called on a background queue by default.
Related
I am downloading a couple of images and cache them. Then I am recalling the cached images and add them as a subView to my scrollView (which is horizontal using a page controller). I want to do both of that when view appears without any other actions required. Unfortunately, it already adds the subview before the images were cached, because it goes through the code while the images are getting downloaded. It follows that nothing is added as a subView.
It works when I download and cache the images when the view appears and I add them as a subView when I press a button, but I would like for that to work automatically.
I am not sure if my code helps for a better understanding but here it is.
This is my code to download the images
func downloadImage(url: URL) {
let dataTask = URLSession.shared.dataTask(with: url) { data, responseURL, error in
var downloadedImage:UIImage?
if let data = data {
downloadedImage = UIImage(data: data)
}
// if download actaully got an image
if downloadedImage != nil {
self.sum += 1
self.cache.setObject(downloadedImage!, forKey: url.absoluteString as NSString) // cache the image
// add the url as value to the dictionary
self.imageURLS[self.sum] = url.absoluteString
}
}
dataTask.resume()
}
And this is what I use to add the subviews
func appendImages(){
// sort the values of the dictionary by the greatest
let sortedImageURLs = Array(imageURLS.keys).sorted(by: >)
var stringURLs = [String]() // empty string array to append the URL's
// for loop which goes through all integers in "creation Date" array
for keys in sortedImageURLs {
let url: String? = imageURLS[keys] // url which is connected to the key
stringURLs.append(url!) // append the url to the url array
}
// -> the url string array starts with the latest loaded url
var originSubview = 0 // counter of the origin of the subviews
for urls in stringURLs {
// 1.
frame.origin.x = scrollView.frame.size.width * CGFloat(originSubview)
frame.size = scrollView.frame.size
// 2.
let imageView = UIImageView(frame: frame)
// get the image which is cahed under the url
let image = cache.object(forKey: urls as NSString)
// set the image of image view as the cached image
imageView.image = image
// and add that image view as a subview to the scrollView
scrollView.addSubview(imageView)
// increase counter variable by one
//-> next images origin is at the end of the image before
originSubview += 1
}
// 3.
scrollView.contentSize = CGSize(width: ((scrollView.frame.size.width) * CGFloat(specialsCounter)), height: (scrollView.frame.size.height))
scrollView.delegate = self
pageControl.numberOfPages = specialsCounter
}
You can use a DispatchGroup to notify when all your downloads have completed.
Basically you enter() before you start a download and leave when the download has finished. notify will trigger when all entered tasks has left
Here is an example on how to do it. I made a completion block for your downloadImage function.
let dispatchGroup = DispatchGroup()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
for url in imageUrls {
self.dispatchGroup.enter()
downloadImage(url: url) {
self.dispatchGroup.leave()
}
}
self.dispatchGroup.notify(queue: .main) {
//All images has been downloaded here
}
}
downloadImage function with completion:
func downloadImage(url: URL, completion: () -> ()) {
let dataTask = URLSession.shared.dataTask(with: url) { data, responseURL, error in
var downloadedImage:UIImage?
if let data = data {
downloadedImage = UIImage(data: data)
}
// if download actaully got an image
if downloadedImage != nil {
self.sum += 1
self.cache.setObject(downloadedImage!, forKey: url.absoluteString as NSString) // cache the image
// add the url as value to the dictionary
self.imageURLS[self.sum] = url.absoluteString
completion()
} else {
completion()
}
}
dataTask.resume()
}
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
I have a tableView that is populated by Json. One of the fields returned has the full URL to the image that it needs to download . I do that and everything works however sometimes as I am scrolling down you see the wrong image in a TableView row then it changes back after 2 seconds and this happens after I see about 12 or 13 images then it starts to get slower . I would like to correct that. The code I have so far is this .
stream_image_string: It has the full path to the URL of the image
var Stream_Cache = NSCache() : caches images
The code below is inside the TableView -> UITableViewCell
again everything works the way it's supposed to, just wondering if I can get better performance .
// if more than 0 then it has a URL
if stream_image_string[indexPath.row].characters.count > 0 {
if let image = Stream_Cache.object(forKey: stream_image_string[indexPath.row] as AnyObject) as? UIImage {
cell.stream_image.image = image
} else {
if cell.stream_image != nil {
let strCellImageURL = self.stream_image_string[indexPath.row]
let imgURL: NSURL = NSURL(string: strCellImageURL)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
cell.Stream_Image_Height.constant = 400
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
if data != nil {
cell.stream_image.image = UIImage(data: data!)
} else {
cell.Stream_Image_Height.constant = 0
}
})
});
task.resume()
}
}
} else {
cell.Stream_Image_Height.constant = 0
}
You can set the image to a placeholder (instead of nil) and use prefetching (see WWDC'16 session 219) to start fetching for your images earlier:
protocol UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [NSIndexPath])
optional func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths:
[NSIndexPath])
}
The image downloading is probably fine, what you need to do is implement:
override func prepareForReuse() {
stream_image.image = nil
}
Inside your cell
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
...
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()