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!
Related
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.
I have some troubles, I have a table view with a custom cell, the cell has a image view, the image is downloaded from internet, when the data loaded the first time everything is fine, but if scroll quickly the screen, the images will mix.
this is my code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell:FlinkerTableViewCell = tableView.dequeueReusableCell(withIdentifier: "flinkerCell") as! FlinkerTableViewCell
cell.imgeCell.image = nil
let row = indexPath.row
let flinkerAux: Flinker = flinkers[row]
if let stringURL = flinkerAux.image{
if stringURL != "" {
DispatchQueue.main.async {
cell.imgeCell.downloadedFrom(link: stringURL)
cell.imgeCell.contentMode = .scaleAspectFill
}
cell.imgeCell.setNeedsDisplay()
cell.lblTitle.text = flinkerAux.fullName
cell.lblSubtitle.text = flinkerAux.phone
}else{
cell.imgeCell.image = UIImage(named: "user_placeholder")
cell.lblTitle.text = flinkerAux.fullName
cell.lblSubtitle.text = flinkerAux.phone
}
}else{
cell.imgeCell.image = UIImage(named: "user_placeholder")
cell.lblTitle.text = flinkerAux.fullName
cell.lblSubtitle.text = flinkerAux.phone
}
cell.imgeCell.layer.cornerRadius = 20
cell.imgeCell?.clipsToBounds = true
cell.selectionStyle = .none
return cell
}
I'm using swift 4.2, I tried with DispathQueue load the images, but when the table view has so much images, the problem is back.
how I should do?
This is commonly known issue. I'm sure you can find a lot with google
The easiest option here is to use any library to download images, like AlamofireImage, Nuke, Kingfisher, SDWebImage
UITableViewCell asynchronously loading images issue - 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.
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
}
}
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.