UICollectionView cell images keep changing while scrolling - swift

I am using CollectionView to display images of gif and png format. All the gif images are loading properly but when I scroll the CollectionView, the png images keep being replaced by other gif images.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StickerCell", for: indexPath)as! StickerCell
let stickerString = String(describing: self.stickerArray[indexPath.row])
if (stickerString.hasSuffix(".gif")){
let url = self.stickerArray[indexPath.row]
if let data = try? Data(contentsOf: url){
let image = UIImage()
image.setGifFromData(data, levelOfIntegrity: 0.5)
cell.sticker.setGifImage(image)
}
} else {
let url = self.stickerArray[indexPath.row]
if let data = try? Data(contentsOf: url){
let image: UIImage = UIImage(data: data)!
cell.sticker.image = image
}
}
return cell
}
I have also tried using DispatchQueue.main.async, its not working either.

I don't think it's a good idea to download the image data every time a collection view cell is loaded. Try using https://github.com/onevcat/Kingfisher for downloading and caching images. You need to download the images once and then images will get cached, which it means next time you scroll it will use cached images and you will not get anymore this issue.

Related

Populate UICollectionView with images from CoreData

I am trying to populate my UICollectionView with my data from CoreData database. The problem is that I want to show a photo in Collection Cell and using data to create UIImage - this task can take a while. With the current solution the images are loaded approx. after 3 seconds but all the other data is already shown in collection view.
How should I add the loading overlay and know when all the images are ready to hide it, or what is correct approach?
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.reuseIdentifier, for: indexPath) as? LocationCollectionViewCell {
cell.name.text = viewModel.locations[indexPath.row].name
cell.unlocked.text = viewModel.locations[indexPath.row].unlocked ? "Unlocked" : "Locked"
if let data = viewModel.locations[indexPath.row].image {
DispatchQueue.global(qos: .background).async {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.image.image = self.viewModel.locations[indexPath.row].unlocked ? image : image?.grayscale()
}
}
} else {
cell.image.image = viewModel.locations[indexPath.row].unlocked ? UIImage(named: "noun_Akropolis_403786") : UIImage(named: "noun_Akropolis_403786")?.grayscale()
}
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.reuseIdentifier, for: indexPath)
return cell
}
}
So #peter you can try follow things:
You can add loader in cell and show/hide it when image is nil or not.
For that create an array where when any image is loaded just append that index there. Until array doesnt contain all index show loader in screen else hide loader.
For handling error you can simply use try catch. If image is not loading or it is falied then you shouldn provide dummy or placeholder image. So that loader will be removed at one time.

How to pass download image to another view controller

I'm currently learning iOS development and was wondering how I would go about passing a downloaded image to another view controller to be displayed there after tapping on a cell. So, a user would tap on a cell that has an image and then another view controller would pop up with that image. I downloaded the image using URLSession and then cached the image so that the user wouldn't have download the image every time they scroll back up or down to another cell that they've already seen. My issue now is how would I get that downloaded image to another view controller? I tried this, but videoController.thumbnailImageView.image is still coming back as nil.
let imageCache = NSCache<AnyObject, AnyObject>()
override func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
let videoController = VideoController()
let videoUrl = URL(string: Videos[indexPath.item].thumbnail_image_name)
videoController.titleLabel.text = Videos[indexPath.item].title
videoController.thumbnailImageView.image = self.imageCache.object(forKey: videoUrl as AnyObject) as? UIImage
show(videoController, sender: Videos[indexPath.item])
}
Here is how I'm caching the images in cellForItemAt indexPath function.
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
self.imageCache.setObject(imageToCache!, forKey: urlString as AnyObject)
cell.thumbnailImageView.image = UIImage(data: data!)
}
I figured it out. For anyone wondering you have to do this:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! VideoCell
let videoController = VideoController()
videoController.titleLabel.text = Videos[indexPath.item].title
videoController.thumbnailImageView.image = cell.thumbnailImageView.image
show(videoController, sender: self)
}
You need to create the cell for the cell item at that selected Index path. I'm not sure why the cached way didn't work, but when I found out I will update this question for anyone else wondering.

I am trying to fetch asset from local identifier and displaying images in collectionview but it makes scroll jerkiness

I am trying following method to fetch asset from local identifier and displaying it in collection view but scroll become jerky while loading.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GridCell", for: indexPath) as! GridCell
cell.representedAssetIdentifier = "some uri"
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = false
let asset = PHAsset.fetchAssets(withLocalIdentifiers: ["some uri"!], options: .none).firstObject
imageManager.requestImage(for: asset!, targetSize: thumbnailSize, contentMode: .aspectFit, options: requestOptions, resultHandler: { result, info in
if cell.representedAssetIdentifier =="someuri" {
cell.imageview.image = result
}
})
expensive operations should not be performed in the cellForItemAt method. The choppyness is because scrolling is delayed till that method has completed. This is specifically cause by the line:
let asset = PHAsset.fetchAssets(withLocalIdentifiers: ["some uri"!], options: .none).firstObject
remove that line and you will see that scrolling will be as smooth as butter. To fix this you need to be smarter about where and when that line gets executed if its reusable then do it once on viewDidLoad and store it as a variable.
The PHAsset.fetchAssets( .. ) call happens synchronously, which blocks rendering of the collectionView and will make scrolling stutter. For smooth scrolling, always fetch assets asynchronously to not block the main thread.
For loading PHAssets, suggest looking at the PHImageManager class provided by Cocoa Touch.

Using SDWebImage to copy an image as NSData to Pasteboard

I'm using SDWebImage to load images into a UICollectionView. Then when the user clicks on the image I want to copy it to the Pasteboard so they can paste it into other apps.
The code I am currently using in the didSelectItemAtIndexPath method goes back to the web to copy the image. But the image should already be in the user's cache.
How do I use SDWebImage to grab an image for NSData?
That way the app will first check the cache for the image. I keep running into DataType issues when I try to use SDWebImage.
This is my current code. I want to fix the code in the didSelectItemAtIndexPath:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let iCell = collectionView.dequeueReusableCellWithReuseIdentifier("ImageCollectionViewCell", forIndexPath: indexPath) as! ImageCollectionViewCell
let url = NSURL(string: self.imgArray.objectAtIndex(indexPath.row) as! String)
iCell.imgView.sd_setImageWithURL(url)
return iCell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// Change code below to use SDWebImage
let url = NSURL(string: self.imgArray.objectAtIndex(indexPath.row) as! String)
let data = NSData(contentsOfURL: url!)
if data != nil {
UIPasteboard.generalPasteboard().setData(data!, forPasteboardType: kUTTypePNG as String)
} else {
// Do nothing
}
}
SDWebImage has a cache. You can access it with SDImageCache. There is customization allowed (different namespaces for caches).
let image = SDImageCache.sharedImageCache.imageFromDiskCacheForKey(url!)‌
if image != nil
{
let data = UIImagePNGRepresentation(image);
}
You have to check if image is not nil because the downloads are asynchrone. So user may trigger collectionView:didSelectItemAtIndexPath: whereas the image hasn't been downloaded yet, and per se not present in the cache.

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!