I'm currently working through my app, updating it to work with Swift 3 and have one problem left. Previously, my image caching worked really well, but since the update the UIImageViews aren't being populated when the image is fetched.
Here is the code (in the ...cellForItemAt... function):
if let img = imageCache[imageUrl] {
print("CACHE HIT: \(indexPath)")
cell.image.image = img
} else {
print("CACHE MISS: \(indexPath)")
var imgUrl: = URL(string: imageUrl)
let request: URLRequest = URLRequest(url: imgUrl!)
let dataTask = session.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if(data != nil) {
print("IMAGE DOWNLOADED: \(imgUrl?.query))")
let image = UIImage(data: data!) // create the image from the data
self.imageCache[imageUrl] = image // Store in the cache
DispatchQueue.main.async(execute: {
if let cell = collectionView.cellForItem(at: indexPath) as? MyCollectionViewCell {
print("APPLYING IMAGE TO CELL: \(indexPath)")
cell.image.image = image
self.collectionView.reloadData()
} else {
print("CELL NOT FOUND: \(indexPath)")
}
})
}
})
dataTask.resume()
}
As you can see, I've added in some prints to find out what is going on. When the view loads, I can see cache misses, and the images loading and the UIImageViews being populated for the visible rows, however when I scroll down, the UIImageViews are never populated, and the log shows CELL NOT FOUND: [0, x] for each indexPath.
If I scroll up again after scrolling down, images are populated from the cache as expected.
The code hasn't changed since the previous version of Swift / iOS / Xcode, and used to work perfectly.
I'm not interested in any 3rd party extensions etc.; I want to understand what is wrong with the code above. Any other improvements/suggestions to the code however are welcome.
Any ideas would be very much appreciated!
Rather than getting the cell via the cellForItem(at: indexPath) method, just use the cell variable you use to set the image in the cache hit. This will create a strong reference to the cell's image view.
DispatchQueue.main.async(execute: { [weak collectionView] in
cell.image.image = image
collectionView?.reloadData()
})
Consider renaming your UIImageView to imageView rather than image.
Related
I am trying to display downloaded images from XML in tableview with columns.
I have to use UITableView, otherwise I would use UICollectionView for this type of funcionality right away. I am able to download images from XML and I am displaying columns by UIStackview filled with UIImageView in custom UITableViewCell. Problem is that there is only first downloaded image shown for both columns. I need to somehow distinguish these images and add them to particular place. What's the best practice for this problem?
EDITED: I solve this by passing downloaded images to cell and updating UI. But I am still curious about other approaches, if they are better.
for i in startInterval...self.currentBookIndex {
group.enter()
let url = URL(string: books[i].thumbNailURL!)!
URLSession.shared.dataTask(with: url, completionHandler: { (imgData, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async(execute: {() -> Void in
if cell.tag == indexPath.row {
let tmpImage = UIImage(data: imgData!)
if let image = tmpImage {
downLoad.append(image)
}
cell.bookImage = tmpImage
cell.updateUI(withIndex: self.currentBookIndex, imageWithURL: url)
}
})
self.group.leave()
}).resume()
self.group.notify(queue: .main) {
print(downLoad.count)
}
There are right images in the download array, I need to display them in the UIImageView. There is always only first of 2 displayed, if I have 2 columns. Thanks in advance.
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.
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
When downloading images from server and populating the collection view (grid 3 * n). I am getting glitch .
I tried almost everything possible like making image view nil before reusing cell and using GCD for updating image view from main thread.
But I'm still facing the glitch.
Glitch is when I scroll the collection view , images downloaded are overlapped and sometimes disappear from the collection view cell.
I have no idea what is causing the image to disappear (which were once downloaded).
Any Suggestions.
private func cellForDropbox(cell:GridCell,indexPath:IndexPath) -> GridCell {
let filename = self.filenames[(indexPath as NSIndexPath).row]
cell.imageView.backgroundColor = UIColor.lightGray
cell.imageView.image = nil
DropboxClientsManager.authorizedClient?.files.getTemporaryLink(path: filename).response(completionHandler: { (response, error) in
if let url = response {
cell.imageView.sd_setImage(with: URL(string:url.link), placeholderImage: nil, options: .refreshCached, completed: { (img, error, cacheType, url) in
})
} else {
print("Error downloading file from Dropbox: \(error!)")
}
})
return cell
}
Try this if do not want completion callback of image download.
private func cellForDropbox(cell:GridCell,indexPath:IndexPath) -> GridCell {
let filename = self.filenames[(indexPath as NSIndexPath).row]
cell.imageView.backgroundColor = UIColor.lightGray
DropboxClientsManager.authorizedClient?.files.getTemporaryLink(path: filename).response(completionHandler: { (response, error) in
if let url = response {
cell.imageView.sd_setImage(with: URL(string: url.link), placeholderImage: UIImage(named: "placeholder.png"))
} else {
print("Error downloading file from Dropbox: \(error!)")
}
})
return cell
}
In my app i am having tableview with sections.The issue is with images. When the user scrolls the list the images displayed are not proper.I know the issue is because of the recycling but still i cannot find any solution.
Code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myeventCell = self.tableView.dequeueReusableCellWithIdentifier("MyEventsTableViewCell", forIndexPath: indexPath) as! MyEventsTableViewCell
myeventCell.wedImage.clipsToBounds=true;
myeventCell.tag=indexPath.row+indexPath.section;
//to download image
if wedImgDownload[indexPath.section][indexPath.row] == false
{
// myeventCell.wedImage.image = UIImage(data: self.webImgData[indexPath.section][indexPath.row]);
if let url = NSURL(string: wedImageUrl[indexPath.section][indexPath.row] as String) {
let request = NSURLRequest(URL: url)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
(response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in
if let imageData = data as NSData? {
if myeventCell.tag == indexPath.row+indexPath.section {
self.wedImgDownload[indexPath.section].removeAtIndex(indexPath.row)
self.wedImgDownload[indexPath.section].insert(true, atIndex: indexPath.row)
self.webImgData[indexPath.section].insert(data!, atIndex: indexPath.row)
myeventCell.wedImage.image = UIImage(data: imageData);
}
}
}
}
}
else{
if self.webImgData[indexPath.section][indexPath.row] != ""{
if myeventCell.tag == indexPath.row+indexPath.section {
myeventCell.wedImage.image = UIImage(data: self.webImgData[indexPath.section][indexPath.row])
}
}
}
return myeventCell;
}
Please lemme know how to solve this issue?
It's because the cells are being reused. The cells are kicking off requests each time they are reused. The order in which the requests finish can't be determined since they are asynchronous. Once a request does finish and the image is set, the cell gets reused and shows the previous image while the current request is in progress.
NSURLConnection is deprecated, you should be using NSURLSession. You will need to cache these images instead of kicking off a request each time they are displayed. You will also need to clear the image each time a cell is reused so it doesn't show the previous image when displayed.
There are many open source libraries available which do exactly these things and are extremely well tested and used by millions of users. It would be foolish to not take advantage of them unless it is a hard requirement of the project.
https://github.com/pinterest/PINRemoteImage
https://github.com/rs/SDWebImage
https://github.com/Alamofire/AlamofireImage