Images not displayed properly when scrolled iOS Swift - ios

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

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.

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

Swift 3: Caching images in a collectionView

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.

Swift - Concurrency issue when loading search result. I load the images, send the requests, but the requests are too late

So I have an app that loads movie search results from the web live as the user types. I throttle my search requests so that it only reloads once every 0.3 seconds. (This probably isn't relevant at all, but what the hell). Now my problem is this.
1 - I type in a search term, let's say "Search1". In order to save time, I load up each result's title, year and genre instantly (almost). I keep the poster black, and send an asynchronous request to load the image, because it takes a lot more time. I wait for the image to load.
2 - Before the images load, I then type in another term, let's say "Search2". So I get the text results for "Search2", and maybe some images.
3 - But then the old requests for "Search1" start rolling in, and they replace those of Search2, because they loaded slower. What I get is a combination of old and new images because I couldn't cancel the old requests.
How should I solve this? I need a way to tell the device to stop loading the old images if the user started typing again, and I can't cancel asynchronous requests. How do I fix this?
Code:
The cell throttling stuff
func updateSearchResultsForSearchController(searchController: UISearchController) {
// to limit network activity, reload 0.3 of a second after last key press.
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload:", object: searchController)
if (!UIApplication.sharedApplication().networkActivityIndicatorVisible) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
self.tableView.reloadData()
}
self.performSelector("reload:", withObject: searchController, afterDelay: 0.3)
}
Here is the code that loads up each cell's info:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")! as! CustomCell
//Safe-Guard. This shouldn't be needed if I understood what I was doing
if (indexPath.row < matchingItems.count) {
cell.entry = matchingItems[indexPath.row] //404
/*
.
.
omitted code that loads up text info (title, year, etc.)
.
.
*/
//Poster
cell.poster.image = nil
if let imagePath = matchingItems[indexPath.row]["poster_path"] as? String {
//Sessions and Stuff for request
let url = NSURL(string: "http://image.tmdb.org/t/p/w185" + imagePath)
let urlRequest = NSURLRequest(URL: url!)
let session = NSURLSession.sharedSession()
//Asynchronous Code:
let dataTask = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) -> Void in
if let poster = UIImage(data: data!) {
//I want to stop this piece from running if the user starts typing again:
dispatch_async(dispatch_get_main_queue()) {
//Animate the poster in
cell.poster.alpha = 0.0 // hide it
cell.poster.image = poster // set it
UIView.animateWithDuration(1.0) {
cell.poster.alpha = 1.0 // fade it in
}
}
}
})
dataTask.resume()
} else {
//Just use placeholder if no image exists
cell.poster.image = UIImage(named: "Placeholder.jpg")
}
}
return cell
}
You can just check the indexPath of the cell and see if it's the same or if it's been dequeued and reused for another another row.
let dataTask = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) -> Void in
if let poster = UIImage(data: data!) {
//I want to stop this piece from running if the user starts typing again:
dispatch_async(dispatch_get_main_queue()) {
//Animate the poster in
if tableView.indexPathForCell(cell) == indexPath {
cell.poster.alpha = 0.0 // hide it
cell.poster.image = poster // set it
UIView.animateWithDuration(1.0) {
cell.poster.alpha = 1.0 // fade it in
}
}
}
}
})
For future people stuck with the same issue, I used Alamofire, a networking library that allows you to cancel asynchronous requests. If you use Objective-C then look up AFNetworking. It's the same thing but for Objective-C.

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)

Resources