Tableview reusing the cells and showing wrong data first - ios

Hello I’ve been having this problem for awhile. I want to stop the tableview from reusing the cell. It keeps displaying the wrong information when i scroll then it shows the right thing like a few milliseconds. How can i stop the tableview from reusing the cell or how can i reuse the cell and make it not do that.
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cats.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "CategoryTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! CategoryTableViewCell
cell.nameLabel.text = cats[indexPath.row].categoryName
cell.subNameLabel.text = cats[indexPath.row].appShortDesc
let catImageUrl = cats[indexPath.row].imageUrl
let url = NSURL(string: "https:\(catImageUrl)")
let urlRequest = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response, data, error) -> Void in
if error != nil {
print(error)
} else {
if let ass = UIImage(data: data!) {
cell.photoImageView.image = ass
}
self.loading.stopAnimating()
}
}
return cell
}

The problem is that you are seeing an image from a previous cell. Simply initialize the image to nil when you dequeue the reused cell:
cell.photoImageView.image = nil
or set it to a default image of your choosing.
Note, the way you are updating the image after it loads has issues.
The row may no longer be on screen when the image finally loads, so you will be updating a cell that has been reused itself.
The update should be done on the main thread.
A better way to do this would be to have an array that caches the images for the cells. Load the image into the array, and then tell the tableView to reload that row.
Something like this:
dispatch_async(dispatch_get_main_queue()) {
self.imageCache[row] = ass
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: row, inSection: 0)],
withRowAnimation: .None)
}

override prepareForReuse() method in your cell class and reset your values
override func prepareForReuse() {
super.prepareForReuse()
nameLabel.text = ""
}
This method is called every time the UITableView before reuses this cell

Related

After tableView scrolled data puts in cells in wrong order

in my View:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TransactionTableCell", for: indexPath) as! TransactionTableCell
let newItem = getTransactionsInSection(section: sectionHeader[indexPath.section])[indexPath.row]
cell.configure(item: newItem)
}
in my TransactionTableCell
func configure(item: TransactionModel) {
guard let withdrawalBonuses = item.withdrawalBonuses,
withdrawalBonuses < 0,
let accruedBonuses = item.accruedBonuses,
accruedBonuses > 0 else {
configureWithOneOperation(item)//shows one line of operation
return
}
//show 2 lines of operations
firstOperationAmountLabel.text = "+\(Int(accruedBonuses))"
secondOperationAmountLabel.text = "\(Int(withdrawalBonuses))"
}
When I scroll the cell , second operation line is appears in wrong cells where its shouldn't be, even If I reload my table , that also has this problem.
You should use prepareForReuse() method
Simply just clear data of your labels:
override func prepareForReuse() {
super.prepareForReuse()
firstOperationAmountLabel.text = nil
secondOperationAmountLabel.text = nil
}
There are few things to check here.
Make sure you reset all fields before configure a new cell.
If you have created a cell using xib or storyboard, make sure you haven't filled labels with static text.
Is your guard statements passing for every item?
Else block for guard configures cell with a single operation, Is it handling all ui elements in cell?

UITableView resets to start position before the user has released it

I have an app with an UITableView and its data gets updated regularly. If the data receives new element, the table view is reloaded. Let’s say the table view has place for 10 visible cells, but data for only 2 of them. The user has scrolled in either direction and not released the table view from touch. If the user has scrolled up, they may have hidden the first cell and only the second one would be visible. Then a new element is received and reloadData is called. Instead of waiting for releasing the table view to update, the tableview gets updated right away and the contentOffset is reset to 0. The tableView just resets to start position while the user has scrolled and not released.
I tried similar setup in separate Xcode project and the issue does not appear there. I wonder what the difference could be.
This is some of the code:
For the ViewController that is the dataSource:
func viewDidLoad() {
super.viewDidLoad()
//some other code
DataManager.shared.onElementReceival = { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return DataManager.shared.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
cell.textLabel?.textColor = .white
cell.backgroundColor = .clear
if let name = DataManager.shared.data[indexPath.row].name {
cell.textLabel?.text = name
} else {
cell.textLabel?.text = "Unnamed"
}
return cell
}
From the DataManager
func didReceive(_ element: Element) {
data.append(element)
onElementReceival()
}
Try to reload only cell using:
tableview.insertRowsAtIndexPaths([IndexPath(forRow: Yourarray.count-1, inSection: 0)], withRowAnimation: .Automatic)
In your example:
let onElementReceival:(IndexPath) -> Void = { [weak self] inx in
guard let self = self else { return }
DispatchQueue.main.async {
tablview.beginUpdates()
tablview.insertRowsAtIndexPaths(at: [inx], with: .automatic)
tablview.endUpdates()
}
}
From DataManager
func didReceive(_ element: Element) {
data.append(element)
onElementReceival(IndexPath(row:data.count, section: 0))
}

When calling prepareForReuse() in CustomTableViewCell.swift, UITableViewCells become unresponsive after several deletions from UITableView (Swift 3)

I have an app that pulls objects from Firebase, then displays them in a table. I've noticed that if I delete 5 entries (this is about when I get to the reused cells that were deleted), I can't delete any more (red delete button is unresponsive) & can't even select the cells. This behavior stops when I comment out override func prepareForReuse() in the TableViewCell.swift controller. Why???
The rest of the app functions normally while the cells are just unresponsive. Weirdly, if I hold one finger on a cell and tap the cell with another finger, I can select the cell. Then, if I hold a finger on the cell and tap the delete button, that cell starts acting normally again. What is happening here??? Here is my code for the table & cells:
In CustomTableViewCell.swift >>
override func prepareForReuse() {
// CELLS STILL FREEZE EVEN WHEN THE FOLLOWING LINE IS COMMENTED OUT?!?!
cellImage.image = nil
}
In ViewController.swift >>
override func viewDidLoad() {
super.viewDidLoad()
loadUserThings()
}
func loadUserThings() {
ref.child("xxx").child(user!.uid).child("yyy").queryOrdered(byChild: "aaa").observe(.value, with: { (snapshot) in
// A CHANGE WAS DETECTED. RELOAD DATA.
self.arr = []
for tempThing in snapshot.children {
let thing = Thing(snapshot: tempThing as! DataSnapshot)
self.arr.append(thing)
}
self.tableView.reloadData()
}) { (error) in
print(error)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomTableViewCell
let cellData = arr[indexPath.row]
...
// SET TEXT VALUES OF LABELS IN THE CELL
...
// Setting image to nil in CustomTableViewCell
let imgRef = storageRef.child(cellData.imgPath)
let activityIndicator = MDCActivityIndicator()
// Set up activity indicator
cell.cellImage.sd_setImage(with: imgRef, placeholderImage: nil, completion: { (image, error, cacheType, ref) in
activityIndicator.stopAnimating()
delay(time: 0.2, function: {
UIView.animate(withDuration: 0.3, animations: {
cell.cellImage.alpha = 1
})
})
})
if cell.cellImage.image == nil {
cell.cellImage.alpha = 0
}
// Seems like sd_setImage doesn't always call completion block if the image is loaded quickly, so we need to stop the loader before a bunch of activity indicators build up
delay(time: 0.2) {
if cell.cellImage.image != nil {
activityIndicator.stopAnimating()
cell.cellImage.alpha = 1
}
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// instantly deselect row to allow normal selection of other rows
tableView.deselectRow(at: tableView.indexPathForSelectedRow!, animated: false)
selectedObjectIndex = indexPath.row
self.performSegue(withIdentifier: "customSegue", sender: self)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
print("should delete")
let row = indexPath.row
let objectToDelete = userObjects[row]
userObjects.remove(at: row)
ref.child("users/\(user!.uid)/objects/\(objectToDelete.nickname!)").removeValue()
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
if (self.tableView.isEditing) {
return UITableViewCellEditingStyle.delete
}
return UITableViewCellEditingStyle.none
}
A few things. For performance reasons, you should only use prepareForReuse to reset attributes that are related to the appearance of the cell and not content (like images and text). Set content like text and images in cellForRowAtIndexPath delegate of your tableView and reset cell appearance attributes like alpha, editing, and selection state in prepareForReuse. I am not sure why it continues to behave badly when you comment out that line and leave prepareForReuse empty because so long as you are using a custom table view cell an empty prepareForReuse should not affect performance. I can only assume it has something to do with you not invoking the superclass implementation of prepareForReuse, which is required by Apple according to the docs:
override func prepareForReuse() {
// CELLS STILL FREEZE EVEN WHEN THE FOLLOWING LINE IS COMMENTED OUT?!?!
super.prepareForReuse()
}
The prepareForReuse method is only ever intended to do minor cleanup for your custom cell.

Reloading table causes flickering

I have a search bar and a table view under it. When I search for something a network call is made and 10 items are added to an array to populate the table. When I scroll to the bottom of the table, another network call is made for another 10 items, so now there is 20 items in the array... this could go on because it's an infinite scroll similar to Facebook's news feed.
Every time I make a network call, I also call self.tableView.reloadData() on the main thread. Since each cell has an image, you can see flickering - the cell images flash white.
I tried implementing this solution but I don't know where to put it in my code or how to. My code is Swift and that is Objective-C.
Any thoughts?
Update To Question 1
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(R.reuseIdentifier.searchCell.identifier, forIndexPath: indexPath) as! CustomTableViewCell
let book = booksArrayFromNetworkCall[indexPath.row]
// Set dynamic text
cell.titleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.authorsLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)
// Update title
cell.titleLabel.text = book.title
// Update authors
cell.authorsLabel.text = book.authors
/*
- Getting the CoverImage is done asynchronously to stop choppiness of tableview.
- I also added the Title and Author inside of this call, even though it is not
necessary because there was a problem if it was outside: the first time a user
presses Search, the call for the CoverImage was too slow and only the Title
and Author were displaying.
*/
Book.convertURLToImagesAsynchronouslyAndUpdateCells(book, cell: cell, task: task)
return cell
}
cellForRowAtIndexPath uses this method inside it:
class func convertURLToImagesAsynchronouslyAndUpdateCells(bookObject: Book, cell: CustomTableViewCell, var task: NSURLSessionDataTask?) {
guard let coverImageURLString = bookObject.coverImageURLString, url = NSURL(string: coverImageURLString) else {
return
}
// Asynchronous work being done here.
task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue(), {
// Update cover image with data
guard let data = data else {
return
}
// Create an image object from our data
let coverImage = UIImage(data: data)
cell.coverImageView.image = coverImage
})
})
task?.resume()
}
When I scroll to the bottom of the table, I detect if I reach the bottom with willDisplayCell. If it is the bottom, then I make the same network call again.
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == booksArrayFromNetworkCall.count {
// Make network calls when we scroll to the bottom of the table.
refreshItems(currentIndexCount)
}
}
This is the network call code. It is called for the first time when I press Enter on the search bar, then it is called everytime I reach the bottom of the cell as you can see in willDisplayCell.
func refreshItems(index: Int) {
// Make to network call to Google Books
GoogleBooksClient.getBooksFromGoogleBooks(self.searchBar.text!, startIndex: index) { (books, error) -> Void in
guard let books = books else {
return
}
self.footerView.hidden = false
self.currentIndexCount += 10
self.booksArrayFromNetworkCall += books
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
If only the image flash white, and the text next to it doesn't, maybe when you call reloadData() the image is downloaded again from the source, which causes the flash. In this case you may need to save the images in cache.
I would recommend to use SDWebImage to cache images and download asynchronously. It is very simple and I use it in most of my projects. To confirm that this is the case, just add a static image from your assets to the cell instead of calling convertURLToImagesAsynchronouslyAndUpdateCells, and you will see that it will not flash again.
I dont' program in Swift but I see it is as simple as cell.imageView.sd_setImageWithURL(myImageURL). And it's done!
Here's an example of infinite scroll using insertRowsAtIndexPaths(_:withRowAnimation:)
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var dataSource = [String]()
var currentStartIndex = 0
// We use this to only fire one fetch request (not multiple) when we scroll to the bottom.
var isLoading = false
override func viewDidLoad() {
super.viewDidLoad()
// Load the first batch of items.
loadNextItems()
}
// Loads the next 20 items using the current start index to know from where to start the next fetch.
func loadNextItems() {
MyFakeDataSource().fetchItems(currentStartIndex, callback: { fetchedItems in
self.dataSource += fetchedItems // Append the fetched items to the existing items.
self.tableView.beginUpdates()
var indexPathsToInsert = [NSIndexPath]()
for i in self.currentStartIndex..<self.currentStartIndex + 20 {
indexPathsToInsert.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .Bottom)
self.tableView.endUpdates()
self.isLoading = false
// The currentStartIndex must point to next index.
self.currentStartIndex = self.dataSource.count
})
}
// #MARK: - Table View Data Source Methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = dataSource[indexPath.row]
return cell
}
// #MARK: - Table View Delegate Methods
func scrollViewDidScroll(scrollView: UIScrollView) {
if isLoading == false && scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
isLoading = true
loadNextItems()
}
}
}
MyFakeDataSource is irrelevant, it's could be your GoogleBooksClient.getBooksFromGoogleBooks, or whatever data source you're using.
Try to change table alpha value before and after calling [tableView reloadData] method..Like
dispatch_async(dispatch_get_main_queue()) {
self.aTable.alpha = 0.4f;
self.tableView.reloadData()
[self.aTable.alpha = 1.0f;
}
I have used same approach in UIWebView reloading..its worked for me.

UITableViewCell backgroundView image is not downloading & showing right images

I have a customized UITableViewCell with some attributes. The goal is to update each cell with a different background image (downloaded from the web asynchronously). I have ten cells to show, but the problem is that the bottom 5 cells always show the first five images downloaded (thats downloaded for the top 5 cells).
To avoid downloading the images again when a cell reloads when user scrolls, I use a table view cell attribute called isImageLoaded. See the snippet below. Tried multiple solutions to fix this without avail. I do show text attributes for these cells as well (not fetched through HTTP requests) and they show correct values for all the 10 cells scrolling up and down.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:ImageTableCell = self.tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! ImageTableCell
cell.tag = indexPath.section
// cell.imageLoaded is initialized as false in the custom cell settings
if !(cell.imageLoaded) {
var photourl:String = "http://someurl/image" + String(indexPath.section)
downloadImage(cell, url: NSURL(string: photourl)!)
}
}
func downloadImage(cell:HomeSummaryTableCell, url:NSURL){
cell.activityindicator.startAnimating()
println("Started downloading \"\(url.lastPathComponent!.stringByDeletingPathExtension)\".")
getDataFromUrl(url) { data in
dispatch_async(dispatch_get_main_queue()) {
println("Finished downloading \"\(url.lastPathComponent!.stringByDeletingPathExtension)\".")
var imageView:UIImageView = UIImageView(image: UIImage(data: data!))
imageView.contentMode = UIViewContentMode.ScaleAspectFill
imageView.clipsToBounds = true
cell.backgroundView = imageView
cell.activityindicator.stopAnimating()
cell.imageLoaded = true
}
}
}
func getDataFromUrl(urL:NSURL, completion: ((data: NSData?) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(urL) { (data, response, error) in
completion(data: data)
}.resume()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 10
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
This is happening because dequed cell retains the values of set properties, so you need to reset your imageLoaded parameter when preparing for reuse.
In your custom cell, override prepareForReuse and set imageLoaded to false.
override func prepareForReuse() {
super.prepareForReuse()
imageLoaded = false
}

Resources