Images in UITableViewCell change after scrolling down and back up - ios

I have a picture and some labels inside my cells. If I have more cells than what can fit on the page, scrolling down then back up loads a different image momentarily then loads the original photo. I have read around StackOverflow to see what would work in my case, but so far I can't find anything since my UITableView is inside a ViewController.
Here is how I load my content into my cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! PostTableViewCell
// Configure the cell...
let post = self.posts[indexPath.row] as! [String: AnyObject]
cell.titleLabel.text = post["title"] as? String
cell.priceLabel.text = post["price"] as? String
if let imageName = post["image"] as? String {
let imageRef = FIRStorage.storage().reference().child("images/\(imageName)")
imageRef.data(withMaxSize: 25 * 1024 * 1024, completion: { (data, error) -> Void in if error == nil {
let image = UIImage(data: data!)
UIView.animate(withDuration: 0.4, animations: {
cell.titleLabel.alpha = 1
cell.postImageView.alpha = 1
cell.priceLabel.alpha = 1
cell.postImageView.image = image
})
} else {
print("Error occured during image download: \(error?.localizedDescription)")
}
})
}
return cell
}
Is there any way I could change tableView.dequeueReusableCell to something different so this doesn't happen?

In your table view cell PostTableViewCell you need to implement the method
override func prepareForReuse() {
super.prepareForReuse()
self.postImageView.image = nil
// Set cell to initial state here, reset or set values
}
The cells are holding on to their old content

I think you run into problems because you update the cell inside the completion block. If the cell scrolls out of view before the completion block is run, it'll be reused for a different row, but you're still setting the image for the previous row.
Try this:
imageRef.data(withMaxSize: 25 * 1024 * 1024, completion: { (data, error) -> Void in if error == nil {
if let cell = self.tableView.cellForRow(at: indexPath) as? PostTableViewCell {
let image = UIImage(data: data!)
UIView.animate(withDuration: 0.4, animations: {
cell.titleLabel.alpha = 1
cell.postImageView.alpha = 1
cell.priceLabel.alpha = 1
cell.postImageView.image = image
}
})
Instead of relying on the cell still being visible, this will try to get it from the table view based on indexPath. If it's not visible any more, cellForRow(at:) will return nil.

Related

Jerky scrolling with Images in UITableView

So I have been reading lots of solutions to this problem and it seems no matter what I do, I am still getting jerky scrolling in my UITableView when there are images present in my cells.
Here is a little info on how I am generating my cells.
I am calculating heights for the cells and cacheing the heights in height for row at index path
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cachedHeights[indexPath.row] {
return height
} else {
let post = dataSource.items[indexPath.row]
var CellClass = FeedTableViewCell.self
if let RegisteredCellClass = cells[post.reusableIdentifier] {
CellClass = RegisteredCellClass
}
cachedHeights[indexPath.row] = CellClass.height(post)
return CellClass.height(post)
}
}
I have verified that the actual and calculated sizes are the same.
When configuring the cell in cell for row at indexpath, I setup all the elements and load the images asynchronously with SDWebImage
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 1 {
let post = dataSource.items[indexPath.row]
var wallCell: FeedTableViewCell?
if let registeredCell = tableView.dequeueReusableCell(withIdentifier: post.reusableIdentifier) as? FeedTableViewCell {
wallCell = registeredCell
if let postCell = wallCell as? FeedTableViewPostCell {
postCell.informationDelegete = self
postCell.actionDelegate = self
}
}
guard let cell = wallCell else { return FeedTableViewPostCell() }
cell.configureCell(post)
cell.delegate = self
return cell
} else {
return UITableViewCell()
}
}
Configure cell calls the method on the cell that populates the element with the post data. There is a sub view called mediaview that handles the images. If there are images int he post they are configured in that view like so.
for (index, element) in media.enumerated() where index < 3 {
addSubview(viewsArray[index])
viewsArray[index].setImage(with: element.source, placeholderImage: nil)
}
I read something about SDWebImage causing issues in it's default UIImageView extension so I wrote my own and this is the code for that.
func setImage(with url: URL?, placeholderImage: UIImage?){
if let placeholder = placeholderImage{
DispatchQueue.main.async(execute: {
self.image = placeholder
})
}
SDWebImageManager.shared().loadImage(with: url, options: [SDWebImageOptions.cacheMemoryOnly, SDWebImageOptions.scaleDownLargeImages], progress: nil, completed: {(image, data, error, cacheType, finished, url) in
if finished {
DispatchQueue.main.async(execute: {
self.alpha = 0
UIView.transition(with: self, duration: 0.1, options: UIViewAnimationOptions.transitionCrossDissolve, animations: { () -> Void in
self.image = image
self.alpha = 1
}, completion: nil)
})
}
})
}
If I comment out the block in mediaview that sets the image, my scrolling is perfectly smooth so I know it's not another portion of the cell generation. My understanding was that the asynchronous loading should alleviate the scrolling lag but I have attempted just about everything to no avail. Any help or insights on this would be greatly appreciated.

Images not show in sequence in UITableViewCell

I'm trying to show images from XML enclosure to tableViewCell image. Images are show but not in sequence, due to dequeueReusableCellWithIdentifier because when i scroll tableViewCell up and down it change images and not show in sequence according to array index. I've tried different ways but did't get success'
Can anyone please tell me how can show images in sequence, or is there any way that first download all images and then show in cell image??
Or any other quick or easy method instead using dispatch_async.
Thanks
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
downloadFileFromURL(NSURL(string: self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String)!, completionHandler:{(img) in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.sideImageView.image = img
})
})
return cell
}
UPDATE
Now i tried this
let picURL = self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String
let url = NSURL(string: picURL)
let data = NSData(contentsOfURL: url!)
cell.sideImageView?.image = UIImage(data: data!)
It show images in sequence but make scrolling hard?
Update2
Now i've tried this
var check = true
var imageArrayNsData : [NSData] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
if check == true{
var indeX = 0
for i in posts.valueForKey("enclosure") as! [NSString]{
let picURL = self.posts.objectAtIndex(indeX).valueForKey("enclosure") as! String
let url = NSURL(string: picURL)
let data = NSData(contentsOfURL: url!)
print("download")
imageArrayNsData.append(data!)
indeX++
print(indeX)
}
check = false
}
if check == false{
cell.sideImageView.image = UIImage(data: imageArrayNsData[indexPath.row])
}
return cell
}
This method only download images one time. And after downloading images it appends in array and next time it show images from array without downloading again. But this method is little bit hard for scrolling. Any one have idea why?
The problem is that the cell object may have been already reused by the time you set the image. You need to add a check to make sure the cell still represents the content you want. That could be as simple as:
if tableView.indexPathForCell(cell) == indexPath {
cell.sideImageView.image = img
}
But might need to be more complex if the index path for a specific item might change in that time (for example, if the user can insert/delete rows).
You could also use a library like AlamofireImage which handles this work (in a different way) for you. With AlamofireImage, your code would look like:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
let URL = NSURL(string: self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String)!
cell.sideImageView.af_setImageWithURL(URL)
return cell
}
To download asynchronously images and set to UIImageView of your UITableViewCell, you can add an extension to your UIImageView.
extension UIImageView {
func downloadImageFrom(link link:String, contentMode: UIViewContentMode) {
//in my methods, I have a cache to avoid re-downloading my images. Images in cache are identified by its URL
if let _imageData = ImageCache.shareCache.getImageData(link) {
self.image = UIImage(data: _imageData)
return
}
//else, download image
NSURLSession.sharedSession().dataTaskWithURL( NSURL(string:link)!, completionHandler: {
(data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue()) {
self.contentMode = contentMode
if let data = data {
ImageCache.shareCache.cacheImageData(data, imageId: link)
self.image = UIImage(data: data)
}
}
}).resume()
}
}
then, from your call-back cellforrow,
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
cell.imageView.downloadImageFrom(yourImageUrl)
return cell

Images from CKAsset Load Out of Order in Swift

I am loading a tableview of images that are being fetched from a public CloudKit database as CKAssets. However, the images are loading out of order about two seconds until the correct image is loaded into the UIImageView of a custom UITableview cell. I know that the issue is that since the cell is reusable the image is still downloaded from CloudKit and displayed in any visible cell while a user is scrolling through the TableView before the correct image is shown in the image view. I am wondering if there is a fix to this in swift so that the image downloaded is only for that of a visible cell and not any previous cells.
Here is the code for cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! PostsTableViewCell
cell.userInteractionEnabled = false
photoRecord = sharedRecords.fetchedRecords[indexPath.row]
cell.photoTitle.text = photoRecord.objectForKey("photoTitle") as? String
cell.photoImage.backgroundColor = UIColor.blackColor()
cell.photoImage.image = UIImage(named: "stock_image.png")
if let imageFileURL = imageCache.objectForKey(self.photoRecord.recordID) as? NSURL {
cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageFileURL)!)
cell.userInteractionEnabled = true
print("Image Cached: \(indexPath.row)")
} else {
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let fetchRecordsImageOperation = CKFetchRecordsOperation(recordIDs:[self.photoRecord.recordID])
fetchRecordsImageOperation.desiredKeys = ["photoImage"]
fetchRecordsImageOperation.queuePriority = .VeryHigh
fetchRecordsImageOperation.perRecordCompletionBlock = {(record:CKRecord?, recordID:CKRecordID?, error:NSError?) -> Void in
if let imageRecord = record {
NSOperationQueue.mainQueue().addOperationWithBlock() {
if let imageAsset = imageRecord.objectForKey("photoImage") as? CKAsset{
cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageAsset.fileURL)!)
self.imageCache.setObject(imageAsset.fileURL, forKey:self.photoRecord.recordID)
cell.userInteractionEnabled = true
}
}
}
}
publicDatabase.addOperation(fetchRecordsImageOperation)
}
return cell
}
Thanks in advance!
There is latency between when your table view appears and when fetchRecordsImageOperation.perRecordCompletionBlock is called. Within that time the user may scroll the table view causing the table view cell to dequeue and requeue with a different indexPath and different data associated with it, if you do not check that the cell's index path is the same as when you constructed fetchRecordsImageOperation.perRecordCompletionBlock, this line: cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageAsset.fileURL)!) will cause the image to be placed in the cell that is already displaying different data. You can modify your completion block like so to avoid this.
if let imageRecord = record {
NSOperationQueue.mainQueue().addOperationWithBlock() {
if let imageAsset = imageRecord.objectForKey("photoImage") as? CKAsset{
if indexPath == tableView.indexPathForCell(cell){
cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageAsset.fileURL)!)
}
self.imageCache.setObject(imageAsset.fileURL, forKey:self.photoRecord.recordID)
cell.userInteractionEnabled = true
}
}
}
You find the answer in here I believe, I bias of course cause I wrote it.
How to determine when all images have been downloaded from a set in Swift?
You should setup an image to display while its loading an image and show that so that the user understands what is happening?

Failure in loading image to cell in swift

I have just started using swift. I am using blocks and NSOperationQueue to download the image in tableViewCell and in the completion handler I am returning the downloaded image. I am trying to update the cell as below.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("itemCell", forIndexPath: indexPath) as! UITableViewCell
var itemImage = cell.viewWithTag(1000) as! UIImageView
var itemName = cell.viewWithTag(1001) as! UILabel
if let item = self.itemArray?[indexPath.row] {
itemImage.image = UIImage(named: "Placeholder.jpg")
getImageForItem(item, withCompletion: { (image) -> () in
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
var imageViewToUpdate = cellToUpdate.viewWithTag(1000) as! UIImageView
imageViewToUpdate.image = image
}
})
itemName.text = item.itemName
}
return cell
}
func getImageForItem(item: item, withCompletion completion:((image: UIImage) -> ())) {
if let image = self.imageCache.objectForKey(item.itemID) as? UIImage {
completion(image: image)
} else {
let request = item.getItemImage(ItemImageSize(rawValue: 2)!, withWidth: 100, shouldFetch: false, block: { (image, tempID) -> Void in
if image != nil {
self.imageCache.setObject(image, forKey: item.itemID)
if item.itemID == tempID {
completion(image: image)
}
}
})
if request != nil {
imageQueue.addOperation(request)
}
}
}
The problem I face is, I am getting the image successfully in the completion block of cellForRowAtIndexPath(), but, I fail to update the cell. For the above code, the downloaded image is applied to all the visible cells in the tableView, but, as I scroll down, I see only the placeholder image. Even I loose the loaded images to placeholder image on scrolling back.
On debugging, I found that
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
var imageViewToUpdate = cellToUpdate.viewWithTag(1000) as! UIImageView
imageViewToUpdate.image = image
}
loop is called for the visible cells only first time. But not called again on scrolling. What am I missing?
I sorted out myself. I added one more argument, indexPath to track it.
getImageForItem(item, indexPath: indexPath, withCompletion: { (image, imageIndexPath) -> () in
if Set<NSIndexPath>(tableView.indexPathsForVisibleRows() as! [NSIndexPath]).contains(imageIndexPath) {
itemImage.image=image
}
})
That gave me the perfect solution

Autolayout, async load image into uiimageview (Swift, xCode6)

Good day! I have 2 problems and I hope you will help me.
1) I have news feed in my application, that contains images.
I am using Autolayout for dynamic cells:
and I want the image to keep its ratio and to completely fill the width of the cell (with margins = 12).
I set constrains, cell is autoresizable, but image didn't save its ratio:
.
What I am doing wrong?
2) The second problem, i load images asynchronously, here is my code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: EventCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as EventCell
var rowData: NSDictionary = tableData[indexPath.row] as NSDictionary
cell.titleButton.setTitle(rowData["title"] as? String, forState: UIControlState.Normal)
cell.titleButton.addTarget(self, action: "openWebSite:", forControlEvents: UIControlEvents.TouchUpInside)
cell.titleButton.tag = indexPath.row
cell.descriprionLabel.text = rowData["description"] as? String
var urlString = rowData["image"] as String
var image = imageCache[urlString]
if( image == nil ) {
var imgURL = NSURL(string: urlString)
// Download an NSData representation of the image at the URL
var request: NSURLRequest = NSURLRequest(URL: imgURL!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
if error == nil {
image = UIImage(data: data) // data -> image
// Store the image in to our cache
self.imageCache[urlString] = image // save in our dictionary
if let cellToUpdate : EventCell = tableView.cellForRowAtIndexPath(indexPath) as? EventCell {
self.table.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
else {
println("Error: \(error.localizedDescription)")
}
})
} else {
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate : EventCell = tableView.cellForRowAtIndexPath(indexPath) as? EventCell {
cellToUpdate.img.image = image
}
})
}
cell.selectionStyle = UITableViewCellSelectionStyle.None;
cell.contentView.setNeedsLayout(); // autolayout bug solution
cell.contentView.layoutIfNeeded(); // autolayout bug solution
return cell
}
All seems okay, but UITableViewCell don't resize when image is loaded and I am trying to reload cell at index path.
Interesting moment, that it will work if I scroll down and then come back to cell.
I have similar error before and I fixed it reading this article UITableView layout messing up on push segue and return. (iOS 8, Xcode beta 5, Swift) , third answer. But it didn't help me now. Looks like I need to call some method to recalculate UITableViewCell, but I don't understand what.
First question : Change UIImageView view mode from Scale to Fill to Aspect Fit (in storyboad)
Second question : Remove dispatch async if image is not nil and make you code look similar like this:
if( image == nil ) {
...
}
else {
cell.img.image = image
}
For first one in the storyboard select the image view and click on the pin icon for setting auto layout constraint and check width and height.
it will remain the width and height constant.

Resources