UITableViewCell continues to change its image when scrolling - swift

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()
}
}

Related

How to make consistent display of image in cell in tableView?

I have an image in a custom cell (called "StoryCell"), the reference to which is in a Realm database and which I am loading up in the CellForRowAt with the following code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
....
let story = stories?[indexPath.row]
if story?.storyImage.isEmpty == true {
print("Do nothing")
} else {
let path = getDocumentDirectory().appendingPathComponent(story!.storyImage)
do {
let imageData = try Data(contentsOf: path)
let retrievedImage = UIImage(data: imageData)
cell.storyImage.image = retrievedImage
} catch {
print("Error retrieving image: \(error)")
}
}
cell.delegate = self
return cell
}
However, whenever I am adding a new table item, after the eighth time the display of the images becomes inconsistent i.e. the first picture repeats on the eighth line. I know this is connected with the 'reuse' nature of cells in tableviews and have tried to resolve it using the 'if' statement in my code above, and also by 'reloading' the tableview data when the new item is added:
#IBAction func addStoryButton(_ sender: UIBarButtonItem) {
let newStory = Story()
newStory.dateCreated = Date()
self.save(story: newStory)
self.storiesTableView.reloadData()
}
However, I still get the same behaviour. Thanks in advance for any thoughts on this.
A full explanation of this issue is here: https://fluffy.es/solve-duplicated-cells/. I finally resolved with the following code in the cellForRowAt although the 'reset' code can also be placed in the 'prepareForReuse' function as indicated by Rocky. Setting the 'defaultImage' as seen in code below also helped:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "storyCell", for: indexPath) as! StoryCell
let story = stories?[indexPath.row]
//reset to default image
let defaultURL = Bundle.main.url(forResource: "test", withExtension: "jpg")
do {
let imageData1 = try Data(contentsOf: defaultURL!)
let retrievedImage = UIImage(data: imageData1)
cell.storyImage.image = retrievedImage
} catch {
print("Error retrieving defaultImage: \(error)")
}
//place saved image in storyImage
if story?.storyImage.isEmpty == true {
print("Do nothing")
} else {
let path = getDocumentDirectory().appendingPathComponent(story!.storyImage)
do {
let imageData = try Data(contentsOf: path)
let retrievedImage = UIImage(data: imageData)
cell.storyImage.image = retrievedImage
} catch {
print("Error retrieving image: \(error)")
}
}
cell.delegate = self
return cell
}
Thanks #Rocky for putting me on the right path.
prepareForReuse is the method which you are looking for.
override func prepareForReuse() {
super.prepareForReuse()
// Do your stuff here, like reset image or content of cell for reusing
self.storyImage.image = nil
}

Can not display image in table view cell using firebase

I can not display image using firebase in table view cell, I don't know why because my code seems to work, but not there, may anyone help me?
Note: The label Works. I created a custom cell using a cocoa touch class, then I have linked that using the notation to cell as you can see.
import UIKit
import Firebase
class PlacesTableViewController: UIViewController,
UITableViewDataSource, UITableViewDelegate{
let ref = Database.database().reference()
var PlacesRef:DatabaseReference! = nil
var SelectedRef:DatabaseReference! = nil
let storage = Storage.storage()
var postdata:[String] = [String]()
#IBOutlet weak var tableview: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
print("Here Variables")
print("Here translated Names")
PlacesRef = ref.child("Places")
let storageRef = self.storage.reference()
PlacesRef.observe(.value) { (snapshot) in
let postDict = snapshot.value as? [String : AnyObject] ?? [:]
if snapshot.exists() {
for a in ((snapshot.value as AnyObject).allKeys)!{
var now:String = ""
self.postdata.append(a as! String)
DispatchQueue.main.async{
self.tableview.reloadData()
}
}
} else {
print("we don't have that, add it to the DB now")
}
print(self.postdata) //add key to array
}
tableview.delegate = self
tableview.dataSource = self
}
func tableView(_ _tableView: UITableView, numberOfRowsInSection selection: Int) ->Int{
print(postdata)
return postdata.count
}
func tableView( _ tableview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableview.dequeueReusableCell(withIdentifier: "cell") as! CustomTableViewCell
var image=UIImage()
let StorageRef = storage.reference()
let NationRef = StorageRef.child("Nations/\(postdata[indexPath.row]).jpg")
print("\(postdata[indexPath.row]).jpg")
NationRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error)
} else {
// Data for "images/island.jpg" is returned
image = UIImage(data: data!)!
print("image downloaded!")
}
}
cell.DetailLabel.text = postdata[indexPath.row]
cell.DetailImage.image = image
return cell
}
}
Let's see what happens in
func tableView( _ tableview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableview.dequeueReusableCell(withIdentifier: "cell") as! CustomTableViewCell
var image=UIImage()
let StorageRef = storage.reference()
let NationRef = StorageRef.child("Nations/\(postdata[indexPath.row]).jpg")
// HERE we starting async web request for image data (tag 1)
NationRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error)
} else {
// HERE we getting image data from async request and storing it in image var (tag 3)
image = UIImage(data: data!)!
print("image downloaded!")
}
}
cell.DetailLabel.text = postdata[indexPath.row]
// HERE we setting UIImage to cell's imageView (tag 2)
cell.DetailImage.image = image
return cell
}
if printing tags we'll get tag 1, tag 2 and tag 3. It means that firstly you create empty uiimage, then set this image to cell's imageView and at last you change image to image from request (image, not cell's imageView)
I think the answer can be - replace image = UIImage(data: data!)! with cell.DetailImage.image = UIImage(data: data!)
and there are many ways to improve this method

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!

UICollectionView image flickering

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.