Proper place to update UITableViewCell content [duplicate] - ios

This question already has answers here:
iOS UITableView: what's the different between "cellForRowAtIndexPath" and "willDisplayCell: forRowAtIndexPath:"
(6 answers)
Closed 6 years ago.
I've always updated the UITableViewCell content in tableView:cellForRowAtIndexPath: like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.resultsTableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as UITableViewCell!
// update cell content
cell.backgroundColor = UIColor.clearColor()
cell.textLabel?.text = myTextString
return cell
}
The documentation also shows content being updated in tableView:cellForRowAtIndexPath:. However, today I read
The content of the cell has to be set right before it is displayed. We don’t care which cell object is being used. All we care about is that it has the right stuff to display when it is passed on to the renderer. So, the right place to set the cell’s content is tableView:willDisplayCell:forRowAtIndexPath:
The reason for this is mentioned in the same post. It talks about the
...problem where the custom cell appears to ‘randomly change content’ when the table is scrolled. The reason why this happens is because the content of the cell is set in the tableView:cellForRowAtIndexPath: method, which is not the right place. In this method, the system expects only to obtain the cell object. Once a cell is displayed, the system will not ask for the cell object until it loses it (i.e. when the cell object is destroyed) and cannot find a replacement to reuse. If, however, a replacement is found, it is reused, and you will end up seeing the contents of that reused cell in place of the original cell.
Is that correct? I've never noticed a problem before, but should I be updating my cell content in tableView:willDisplayCell:forRowAtIndexPath:? Have I been misled by the documentation?

The statement is wrong.
custom cell appears to ‘randomly change content’ when the table is scrolled
occurs only if
dequeueReusableCellWithIdentifier is (mis)used outside cellForRowAtIndexPath.
The entire content of the cell is not set reliably to a defined state inside cellForRowAtIndexPath.
The content of the cell is manipulated outside cellForRowAtIndexPath.
The developer is strongly discouraged from using any of the three cases.
So it's perfectly fine to both supply the cell and update the content in cellForRowAtIndexPath.
PS: I guess willDisplayCell:forRowAtIndexPath: is a legacy method coming from the cell based NSTableView in macOS where the data source delegate methods don't support to change the cell's appearance.

Related

How can the content of a UITableViewCell be updated properly?

I have a TableView whose cells content need to be updated when it's TableViewRowAction is excecuted. More precisely I want to update a UILabel inside the cell. Setting a new text to this label inside the action is no problem but when scrolling down/up and the cell gets reloaded, then this label inside the cell has the same text as before changing it.
I know that the cells get reused and that's why I want to know how to "avoid" this effect and how to update the content of the cells properly. I have already tried to call TableView.ReloadData() which seems to solve the problem but the cells appear in a completly different order which doesn't look very nice.
The most straightforward approach is to update the data that your UITableView is drawing from (i.e. the array of data that you're populating each cell from has been updated to reflect your text change), then reload the specific cell of the UITableView:
let indexPath = IndexPath(row: 0, section: 0)
tableView.reloadRows(at: [indexPath], with: .automatic)
As you said table cells are reused , this means you have to keep a model array for the whole table and make all the changes to it , then reload the table more importantly to set the content of the cell index in cellForRowAt
To achieve this you must create an Array that acts as a dataSource for your labels. When you are changing the text for a specific label, you must update the array. You can get the exact update location from the property indexPath of the method cellForRowAt. Every single time you scroll up and down, cellForRowAt method is called. Just use something like
label.text = array[indexPath.row]

Should I use dequeReusableCellWithIdentifier?

I know this question is dumb. But suddenly got stuck up with this question.
When I use dequeReusableCellWithIdentifier, the cells are reused. To be more specific, first 'n' set of cells is reused - along with their references
As the same references are reused, I can't actually store local variables to the cells. I actually need to assign them everytime in cellForRowAtIndexPath
Assume I'm using a custom complex UITableviewcell. (I know we should reduce complexity. But still...)
Some views are to be added to the cell.
Best example i could think of is an image slider.
So the number of images in the image slider differs based on the datasource. So i need to add the views in cellForRowAtIndexPath. Can't avoid it.
Since the same reference of cells are reused, I need to add these views everytime cellForRowAtIndexPath is called. I think that is a bit of heavy load.
I thought of using drawRect method, but it does not work with UITableViewAutomaticDimension
If I don't use dequeReusableCell, I will have individual referenced cells, but it will cost the memory and performance.
So what is the best solution?
Can I use dequeReusableCell and still need not rewrite its content?
Edit 1:
By mentioning dequeReusableCell, I did mention dequeueReusableCellWithIdentifier - forIndexPath. I'm sorry for the confusion.
Edit 2:
I feel, I'm not so clear in my question.
Lets consider I have a array in my viewcontroller.
I'm configuring my cell in cellForRowAtIndexPath with dequeueReusableCellWithIdentifier - forIndexPath
So what happens is, everytime i scroll, when the invisible rows become visible, cellForRowAtIndexPath is called.
Lets say I have a image slider with n imageviews. For each cell, this 'n' differs. So I'm forced to draw the cell's view based on its dataSource.
So everytime the tableView calls cellForRowAtIndexPath, the cell is configured again and again.
I want to avoid this to improve performance.
what I do in this case is the following:
I always use dequeReusableCell:, for reasons you already said
I write a cleanCell method in the custom UITableViewCell class in order to clean everything has been already set on the cell (just to make it ready for reuse)
then in the cellForRowAtIndexPath: I configure my cell as desired
That's what I would do: I would move the logic of adding those heavy views inside the cell's class itself. So that in cellForRowAtIndexPath I would just set an object that has to be displayed inside the cell, and cell would create all the necessary views on its own (it's a view layer, after all). So inside cellForRowAtIndexPath it would look something like this:
cell = [tableView dequeueReusableCellWithIdentifier...forIndexPath...];
cell.modelObject = myModelObject;
and that's it.
Then inside the cell class I would implement optimizations for switching between different model objects. Like, you could defer releasing the heavy views, and reuse them, when the cell is reused.
To be honest, I don't get your example with the slider, but let's suppose you have a star rating with 5 stars, which can be visible or not. There are various way to do it, but let's assume you're just adding / removing a UIImageView for each star. The way you do it now, you would have an empty cell and create / add views in cellForRow. What I'm suggesting is to have 5 UIImageViews as part of your cell, and inside the cell set their visibility based on the modelObject.rating. Something like that. I hope, it helps to illustrate my point :)
UPDATE: when your cell can have an arbitary number of images inside of it, I would probably create them inside the cell's class. So, for instance, for the first model object we need 3 image views. So we create them. But then we don't release them in prepareForReuse, we wait for the next model object to come. And if it has, say, 1 image, we release 2 image views (or we don't, so that we didn't have to recreate them later, it depends on what is more critical: performance, or memory usage), and if it needs 5, we create two more. And if it needs 3, we're all set already.
In your case you should use dequeueReusableCellWithIdentifier(_ identifier: String, forIndexPath indexPath: NSIndexPath) method. This method automatically decides whether the cell needs to be created or dequeued.
I need to add these views everytime cellForRowAtIndexPath is called. I think that is a bit of heavy load.
You don't need to add those views every time cellForRowAtIndexPath gets called. Those views should be already added as a cell is instantiated. In cellForRowAtIndexPath you only assign images and other values to those views.
In case if you use storyboards and prototype cells your code should look like this:
var items = [UIImage]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ImageCell", forIndexPath: indexPath)
let item = items[indexPath.row]
let recipient = item.recipients.first
// I have given my UIImageView tag = 101 to easily access it from the view controller.
// This allows me to avoid defining a custom UITableViewCell subclass for simple cells.
if let imageView = cell.viewWithTag(101) as? UIImageView {
imageView.image = item
}
return cell
}
1)I think you need to use "lazy load" of cells.
http://www.theappguruz.com/blog/ios-lazy-loading-images (for example)
2) You can create a property in your viewController, and create an NSMutableDictionary property, which will store the some data by the some key,
if your model have things like "ID" of the cells or you can use indexPath.row for it, then you can load it only one time, and next time when -cellForRow will be called, you can retrieve data from your dictionary by the
[self.dataDictionary objectForKey:#"cellID"];
And with this you, solve your repeating load.
In -cellForRow you can set the check,like.
if ([self.dataDictionary objectForKey:#"cellID"]) {
cell.cellImage = [self.dataDictionary objectForKey:#"cellID"];
} else {
UIImage *img = [uiimage load_image];
cell.cellImage = img;
[self.dataDictionary setObject:img forKey:#"cellID"];
}
And so on. Its like example, the dataType's you can choose by yourself, it's example.
Thanks for everyone helping me out. I think the better answer was from #Eiko.
When you use reusable cells, it must be completely reusable. If you are going to change its views based on the data, you either have to use a new identifier or avoid using reusable cells.
But avoiding reusable cells, will take a load. So you should either use different identifiers for similar cells or as #FreeNickName told - you can use an intermediate configuration and alter it on your need.

UICollectionView: Get indexPath of Cell that is not visible

I have a UICollectionView with several cells – there are always some of them outside the viewport, so there are cells being reused.
When an event occurs I want to take a specific cell and move it to the front of the UICollectionView. In order to make moveItemAtIndexPath() work I need to know the current indexPath of the Cell that I want to move.
When I initialize the Cells in the CollectionViewDelegate with the cellForItemAtIndexPath method I save its indexPath into a variable of the Object that is the model of the Cell (not it’s actual Object, just the model). Then I look at this variable when I want to move the cell.
This is working fine as long as the cell was visible once – it seems like it only initiated then, even though it is part of the CollectionViewData all the time.
How can I make sure my CollectionViewCell has an indexPath, even when it is not visible or has not been visible yet?
If you know what data (cell) you want to present at the front (I assume top) of your UICollectionView, why don't you just update your dataSource and reload your UICollectionView?

Multiple cells got affected after calling cellForRowAtIndexPath

I have a UITableView, and I want to change text color of the selected row. But I see every other 15 row got affected along with the one I clicked. This is tested under the master-detail sample project.
if let indexPath = self.tableView.indexPathForSelectedRow(){
self.tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.textColor = UIColor.redColor()
}
I checked cellForRowAtIndexPath value in debug session, it seems returning only one object, how come other cells got affected too?
Cells are reused - almost certainly you are not resetting your cells to a base state in prepareForReuse and configuring them correctly on each call to cellForRowAtIndexPath.
When you look at a table view that can display some number of cells at once, typically there will only exist one more cell than can be shown. When a cell is moved off the screen it is placed in a pool of cells for reuse. Just before a cell moves onto the screen it is configured by you, in cellForRowAtIndexPath. If you have configured something in the cell and you do not configure that explicitly every time you return a cell from cellForRowAtIndexPath then that configuration persists in the cell that is in the reuse pool. The function prepareForReuse is also called before each cell is reused - if you have subclassed UITableViewCell then you can implement this function to return the cell to a base configuration, so that settings like text color do not unexpectedly affect multiple cells.
This approach makes it possible to scroll through an entire table view smoothly with a minimum amount of memory used. It is an expensive operation to create and destroy cells every time one disappears and a new one is required.
The simplest fix is to always set the text color in cellForRowAtIndexPath - either to the base color or to the special color you want in some cells.

Can you store state in a custom UICollectionViewCell? Or can the state get erased (like when the cell scrolls off the screen)?

If I create a UICollectionViewCell subclass like:
class StudentCell: UICollectionViewCell {
var student: Student?
}
And in my controller I implement UICollectionView's didSelectItemAtIndexPath and set the variable:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if let studentCell = collectionView.cellForItemAtIndexPath(indexPath) as? StudentCell {
studentCell.student = self.someStudent
}
}
When I click on the cell it should set student, but if the cell is scrolled off screen it seems like the whole cell might get wiped and when it comes back on screen it would need to rebuild itself in cellForItemAtIndexPath.
The problem I have is that I have a UICollectionView with cells that do not scroll off the screen that I'm storing data in. Because they don't leave the screen I assume they should not get wiped, but one of the cells does not seem to keep it's variable, which makes me think maybe it's getting wiped and I may need to move the state outside of the cells to a dictionary or something and in didSelectItemAtIndexPath instead of setting the variable in the cell I'd set the dictionary. Whenever I need to access the data instead of asking the cell for it I'd look it up in the dictionary.
But either way, I was wondering if it's possible (or a bad idea) to set it in the cell or not.
Yes, cells in both UICollectionView and UITableView can (will) be reused at the systems discretion and should not be used to store state information, only display information. Specifically, both views will reuse cells when they are scrolled off-screen, but there's no guarantee this is the only time they'll be reused. The usual way to handle this is to define some kind of cell data object which stores the data for each cell (visible and not) and refresh the cell view from that as needed/requested.
Tables display their data in cells. A cell is related to a row but it’s not exactly the same. A cell is a view that shows a row of data that happens to be visible at that moment. If your table can show 10 rows at a time on the screen, then it only has10 cells, even though there may be hundreds of rows with actual data. Whenever a row scrolls off the screen and becomes invisible, its cell will be re-used for a new row that scrolls into the screen.

Resources