UICollectionView image flickering - swift

I have a UICollectionView that shows cells at fullscreen (i.e. a gallery).
Sometimes, when swiping for the new image, an image other than the right one is displayed for less than a second, than the image is updated.
Other times, the wrong image is shown, but if you swipe left and than right (i.e. the image disappear and reappear) than the right image comes up.
I don't understand the cause of this behavior.
Images are downloaded asynchronously when the collection view needs them.
Here is the code:
let blank = UIImage(named: "blank.png")
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UICollectionViewCell
let image = images[indexPath.row]
if let imageView = cell.viewWithTag(5) as? UIImageView {
imageView.image = blank
if let cachedImage = cache.objectForKey(image.url) as? UIImage {
imageView.image = cachedImage
} else {
UIImage.asyncDownloadImageWithUrl(image.url, completionBlock: { (succeded, dimage) -> Void in
if succeded {
self.cache.setObject(dimage!, forKey: image.url)
imageView.image = dimage
}
})
}
}
return cell
}
where UIImage.asyncDownloadImageWithUrl is:
extension UIImage {
static func asyncDownloadImageWithUrl(url: NSURL, completionBlock: (succeeded: Bool, image: UIImage?) -> Void) {
let request = NSMutableURLRequest(URL: url)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: { (response, data, error) in
if error == nil {
if let image = UIImage(data: data) {
completionBlock(succeeded: true, image: image)
}
} else {
completionBlock(succeeded: false, image: nil)
}
})
}
}
and for the first image shown:
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if let index = self.index {
let newIndexPath = NSIndexPath(forItem: index, inSection: 0)
self.collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
self.index = nil
}
}

This is what i think it's happening:
Cell A appears
Request for image A is made to load on Cell A
Cell A disappears from screen
Cell A reappears (is reused)
Request for image B is made to load on Cell A
Request for image A is complete
Image A loads on to the Cell A
Request for image B is complete
Image B loads on to the Cell A
This is happening because you are not keeping track of which image url should load on the completion block.
Try this:
One way to do that is to store the url of the image that should be there in your UICollectionViewCell. To do that, create a subclass:
class CustomCollectionViewCell:UICollectionViewCell {
var urlString = ""
}
Then:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CustomCollectionViewCell
let image = images[indexPath.row]
if let imageView = cell.viewWithTag(5) as? UIImageView {
imageView.image = blank
cell.urlString = image.url.absoluteString
if let cachedImage = cache.objectForKey(image.url) as? UIImage {
imageView.image = cachedImage
} else {
UIImage.asyncDownloadImageWithUrl(image.url, completionBlock: { (succeded, dimage) -> Void in
if succeded {
self.cache.setObject(dimage!, forKey: image.url)
//
// This can happen after the cell has dissapeared and reused!
// check that the image.url matches what is supposed to be in the cell at that time !!!
//
if cell.urlString == image.url.absoluteString {
imageView.image = dimage
}
}
})
}
}
return cell
}
For a more accurate reply, post the project (or a barebones version of it). It's a lot of work to reproduce your setup and test.

Related

Swift, Stop Images from loading in table View

Here I load my images, I want to stop the images from loading when I click on the path. How can this be done? I tried setting the URL to nil but that didn't work.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as? CustomCell
let pintrestsUrl = pintrest[indexPath.row].urls?.thumb
Library().parseImages(ImagesUrlArrayPath: pintrestsUrl!, completion: { (image) -> Void in
if let imageFromCache = imageCache.object(forKey: pintrestsUrl as AnyObject ) as? UIImage {
cell?.ImageView.image = imageFromCache
}
})
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// stop images from loading
}
EDIT -- added ParseImages Function
func parseImages(ImagesUrlArrayPath: String, completion: #escaping (UIImage)-> Void) {
if let imageFromCache = imageCache.object(forKey: ImagesUrlArrayPath as AnyObject ) as? UIImage {
completion(imageFromCache)
}
else
{
if let imageURL = URL(string: (ImagesUrlArrayPath)){
DispatchQueue.global().async{
let data = try? Data(contentsOf: imageURL)
if let data = data{
let imageToCache = UIImage(data: data)
// let image = imageToCache
DispatchQueue.main.async {
imageCache.setObject(imageToCache!, forKey: ImagesUrlArrayPath as AnyObject)
completion(imageToCache!)
print("sucess")
//cell?.videoImageView.image = image //?.resizeImage(targetSize: size)
}
}
}
}
}
}
Solved this awhile back
You have to set the images to nil before loading new images on them

UITableViewCell continues to change its image when scrolling

When I scrolling table view, the tableViewCell's image keep change incorrect image.
I tried this.
#IBOutlet weak var imageView: UIImageView!
override prepareForReuse() {
super.prepareForReuse()
self.imageView.image = nil
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? MyCustomCell else { return UITableViewCell() }
cell.tag = indexPath.row
if cell.tag == indexPath.row {
// download image
downloadManager.download { (image) in
cell.imageView.image = image
}
}
}
But, this still incorrect image when scrolling.
How can I fix it??
once you downloaded the image (async) you need to check that your cell is actually related to that image.
eg. cell may be reused for another indexPath so image downloaded is not correct anymore.
I have added index (indexPath.row) to download completion closure so you can check if the cell.tag is equals to index
downloadManager.download(index: indexPath.row) { (image, index) in
if cell.tag == index {
cell.imageView.image = image
}
}
You can set a placeholder image to fix this issue. Please use https://github.com/onevcat/Kingfisher for async image loading
if let url = URL(string: "url_of_your_image") {
imageView.kf.setImage(with: url)
}
Here is working code to Download and load image from URL with Display placeholder image upto load the actual image with NSCache.
https://stackoverflow.com/a/51746517/10150796
Try this:
import UIKit
class CustomViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
imageView.image = nil
}
func configureWith(url string: String) {
guard let url = URL(string: string) else { return }
task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
task?.resume()
}
}

TableView Cell show image with dispatch_async shows "unexpected non-void return value in void function" issue

I want to show the image in the TableViewCell. There are the codes:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "myCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! DIYSquareALLCell
cell.titles!.text = titles[indexPath.row]
cell.leftImages!.image = getPic(leftImages[indexPath.row])
return cell
}
func getPic(PicURL: String) -> UIImage! {
let image = self.imageCache[PicURL] as UIImage?
if image == nil {
let url = NSURL(string: PicURL.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)
if let data = NSData(contentsOfURL: url!) {
imageCache[PicURL] = UIImage(data: data)
return UIImage(data: data)!
}
} else {
return image
}
return nil
}
But scrolling the TableView is very lag so I change the function and add some dispatch_async feature in it.
It shows the issue "unexpected non-void return value in void function" in my getPic function.
After I changed, there are the codes:
func getPic(PicURL: String) -> UIImage! {
let image = self.imageCache[PicURL] as UIImage?
if image == nil {
let url = NSURL(string: PicURL.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let data = NSData(contentsOfURL: url!)
if data != nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.imageCache[PicURL] = UIImage(data: data!)
return UIImage(data: data!)// here is the issue
})
}
}
} else {
return image
}
return nil
}
Anyone can tell me how to fix it? Thanks!
You can't return a value r an object when using the Asynchronous task, The function which is running on the main thread and it won't wait for your async task to be finish.
Lets do this with the Closure.
Your code should be like this:
typealias CompletionHandler = (image: UIImage) -> Void
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: testCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! testCell
downloadFileFromURL(NSURL(string: "http://img.youtube.com/vi/PCwL3-hkKrg/sddefault.jpg")!, completionHandler:{(img) in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.imgView.image = img
})
})
return cell
}
func downloadFileFromURL(url1: NSURL?,completionHandler: CompletionHandler) {
// download code.
if let url = url1{
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
dispatch_async(dispatch_get_global_queue(priority, 0)) {
let data = NSData(contentsOfURL: url)
if data != nil {
print("image downloaded")
completionHandler(image: UIImage(data: data!)!)
}
}
}
}
Sample project uploaded to GIT.

Async images change every time while scrolling?

So I'm creating an iOS app that lets you browse through the Unsplash wallpapers and I used UICollectionView to load the images in cells but whenever I scroll through an image, I go back the image changes into a different one.
Here's the code
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! ImageCollectionViewCell
let downloadQueue = dispatch_queue_create("com.donbytyqi.Papers", nil)
dispatch_async(downloadQueue) {
let imageURL = NSURL(string: "https://unsplash.it/200/300/?random")
let imageData = NSData(contentsOfURL: imageURL!)
var image: UIImage?
if imageData != nil {
image = UIImage(data: imageData!)
}
dispatch_async(dispatch_get_main_queue()) {
cell.imageView.image = image
}
}
return cell
}
EDIT: Two things going on:
collectionView.dequeueReusableCellWithReuseIdentifier reuses a cell that has already been created (if there's one available). So you're dequeueing one of your previous cells.
The URL your loading your images from generates a random image each time it is called.
Thus, when you scroll to the point where the first row of your collectionview is off screen, those cells get reused. Then when you scroll back up, the cells are recreated with a new random image from "https://unsplash.it/200/300/?random"
A way of circumventing this would be to keep an array of all your images indexed based on the cell index. Of course, if your images are very big and/or you have a really large collectionView, you may run out of memory.
Take a look at this code that I have mocked up. I have not verified that the code actually works.
//instance var to store your images
var imageArray: [UIImage]?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! ImageCollectionViewCell
// Check if we have already loaded an image for this cell index
if let oldImage: UIImage = imageArray[indexPath.row] {
cell.imageView.image = oldImage
return cell
} else {
// remove the old image, before downloading the new one
cell.imageView.image = nil
}
let downloadQueue = dispatch_queue_create("com.donbytyqi.Papers", nil)
dispatch_async(downloadQueue) {
let imageURL = NSURL(string: "https://unsplash.it/200/300/?random")
let imageData = NSData(contentsOfURL: imageURL!)
var image: UIImage?
if imageData != nil {
image = UIImage(data: imageData!)
// Save image in array so we can access later
imageArray.insert(image, atIndex: indexPath.row)
}
dispatch_async(dispatch_get_main_queue()) {
cell.imageView.image = image
}
}
return cell
}
#toddg solution is correct. But still it have a problem in reusing the cell.
If the cell is reused before the network call completion then it will assign the downloaded image to another cell.
So I changed the code like following.
var imageArray: [UIImage]?
let downloadQueue = dispatch_queue_create("com.donbytyqi.Papers", nil)
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! ImageCollectionViewCell
if let oldImage: UIImage = imageArray[indexPath.row] {
cell.imageView.image = oldImage
return cell
} else {
cell.imageView.image = nil;
downloadImage(indexPath);
}
return cell
}
func downloadImage(indexPath: NSIndexPath) {
dispatch_async(downloadQueue) {
let imageURL = NSURL(string: "https://unsplash.it/200/300/?random")
let imageData = NSData(contentsOfURL: imageURL!)
var image: UIImage?
if imageData != nil {
image = UIImage(data: imageData!)
}
let cell = self.collectionView .cellForItemAtIndexPath(indexPath) as! ImageCollectionViewCell
dispatch_async(dispatch_get_main_queue()) {
cell.imageView.image = image
}
}
}
Hope this helps.
Let me explain what is going on actually.
When you scroll and go back you actually see the previously displayed cell with previously downloaded image (because of dequeueReusableCellWithReuseIdentifier:), and you will keep seeing that image until your new image will not downloaded, i.e. until execution of cell.imageView.image = image line.
So, you have to do following:
set cell.imageView.image = nil after dequeueReusableCellWithReuseIdentifier: line, like so:
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! ImageCollectionViewCell
cell.imageView.image = nil;
//...
This will remove previously downloaded image from imageView until new image download.
You should use something like SDWebImage or UIImageView+AFNetworking for async image downloading with cache support, because every time that your method is called the images will be downloaded again and again instead of getting cached image, and that is waste of traffic.
Good luck!

Taking a snapshot of a UICollectionViewCell

I am making a tvOS app and I want it to look similarly to the Movies app. Therefore I have a UICollectionView. Now my cells are not just simple UIImageViews, but are rather somewhat more complicated.
I still want to have the nice focus visual effect (making the cell image bigger and having the light effect on it when the user swipes the remote). So what I am trying to do is render my cell, then take a snapshot of it and then show this snapshot instead of the cell itself. This is how I do it:
extension UIView {
var snapshot : UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0.0)
drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
...
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = NSBundle.mainBundle().loadNibNamed("ContentCell", owner: self, options: nil)[0] as! ContentCell
cell.update()
let cellSnapshot = cell.snapshot
let snapshotCell = collectionView.dequeueReusableCellWithReuseIdentifier("SnapshotCell", forIndexPath: indexPath) as! SnapshotCell
snapshotCell.snapshotImageView.image = cellSnapshot
return snapshotCell
}
However, all this does is show a black cell. Any ideas what might I be doing wrong?
You should look here
In Swift it will be smth like that:
extension UIView {
var snapshot : UIImage? {
var image: UIImage? = nil
UIGraphicsBeginImageContext(bounds.size)
if let context = UIGraphicsGetCurrentContext() {
self.layer.renderInContext(context)
image = UIGraphicsGetImageFromCurrentImageContext()
}
UIGraphicsEndImageContext()
return image
}
}