At my experiment I need to have reference to first UITableViewCell in tableView. By some action I need to set image and some other cell properties and to keep this state of this only cell even if the tableView will be scrolled. All of this properties can be potentially nulled via scrolling (and they actually are) because of reusing. For set this properties every time cell appears on screen, inside of `-cellForRowAtIndexpath' I tried to catch first cell using:
UITableViewCell *firstCell = (UITableViewCell *)[atableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
but looks like this way I can only catch every next first cell on next scrollable "screen".
So, how can I get ref to first UITableView cell?
If I understand you correctly, you are trying to do something special if the cell at (0, 0) is about to be displayed, right? If that's the case, you can easily implement UITableViewDelegate's tableView:willDisplayCell:forRowAtIndexPath: method as follows:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath) {
// Do something special
}
}
There is also a corresponding tableView:didEndDisplayingCell:forRowAtIndexPath: method if you need to undo things.
Hope it helps!
There is no "first" table view cell. The entire table view typically uses a single cell to improve performance.
You can change that, by implementing your own cell reuse system (search for reuse in the documentation). But generally the cell is the wrong place to store any data related to a specific index in the table view.
Related
I have a list of custom cells in my tableview and as I scroll everything appears to fine and the cells appear to be in the same order. I have some functionality with my cells - as i select a cell (and it dynamically expands)the background color changes and a few other custom cell properties. Once I do this and then I start scrolling, different cells that i haven't even touched before show up, selected(expanded) and the cell only updates when I select it manually to the correct data. I seem to see duplicates and all kinds of craziness.
I know there are LOTS of posts about this on here so far but for me, so far nothing has worked. Would like some input on what I could do to stop this ridiculous behavior.
I have posted some code to give you a better idea of what I am doing. I know that 'dequeueReusableCellWithIdentifier' is the culprit but don't know of an alternative.
As side notes, this is a tableview(its own xib) that is a child view of a large view (also a xib). I have also already registered the nib for the tableview.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCustomCell *cell = [tableView dequeueReusableCellWithIdentifier:myIndentifier forIndexPath:indexPath];
if(self.currentSelectedIndex){
if(self.previousSelectedIndex){
//collapse cell
//configure cell in method(change background color etc)
}
else{
//expand cell
//configure cell in method(change background color etc)
}
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.currentSelectedIndex = indexPath;
[tableView beginUpdates];
if(self.currentSelectedIndex){
if(self.previousSelectedIndex && (self.previousSelectedIndex != self.currentSelectedIndex)){
[tableView reloadRowsAtIndexPaths:#[self.currentSelectedIndex, self.previousSelectedIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else{
[tableView reloadRowsAtIndexPaths:#[self.currentSelectedIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
[tableView endUpdates];
if(self.previousSelectedIndex == self.currentSelectedIndex){
self.previousSelectedIndex = nil;
}
else{
self.previousSelectedIndex = self.currentSelectedIndex;
}
}
What can I do or how would i make sure that nothing else in the list 'seems' to be selected(expanded) or prevent from appearing to see duplicates as i scroll? I already keep track of my current and last selected index(as shown in the code) so I suppose that I could use that somehow?
Dequeued Cells are Reused
Know that cells are re-used, so that the appearance of a UITableViewCell is persistent for the entire life of that cell.
This means that if you do not explicitly reset all the presentation view of your cell, and just returning it unchanged in cellForRowAtIndexPath, what you are returning may be a currently selected (or deselected) cached cell.
A possible location to reset a table cell is prepareForReuse.
Design note:
How are you maintaining self.currentSelectedIndex and self.previousSelectedIndex? This is typically quite dangerous, since you are attempting to replicate the UITableView behavior. It is for example, unlikely to work with multiple selection. Setting an active selection is unlikely handle situations when the OS didDeselectRowAtIndexPath, as a result of a keyboard dismissal for example.
I have TableViewController and ViewController. In 3 cells change pictures depending on the index and intForString in ViewController.
In the first session I work with all cells. In the second I work with one cell. When I go to the third session image stay only to the cell in which I worked the second session. And the other two images disappear. How to make that all the images were displayed?
I don't understand what you are asking when you say "How to make that all the images were displayed?"
However, this bit is very wrong:
(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell1 = [tableView cellForRowAtIndexPath:indexPath];
The table view data source method tableView:cellForRowAtIndexPath: is supposed to configure and return a new (or recycled) table view cell.
The method you're calling in the first line is the table view instance method cellForRowAtIndexPath. That method only returns a cell if there is a cell on-screen for the specified indexPath.
That won't work.
You want to use code that calls dequeueReusableCellWithIdentifier:forIndexPath. If you've defined a class for your identifier, that method will always return a valid cell. If not, you need to add code that checks for nil and creates a new cell using the UITableViewCell method initWithStyle:reuseIdentifier:
I have a custom UITableViewCell. When a cell gets selected, a UILabel gets added to it. I had to use prepareForReuse for it not to get messy, like so:
- (void)prepareForReuse {
NSArray *viewsToRemove = [self.view subviews];
for (UILablel *label in viewsToRemove) {
[label removeFromSuperview];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CategorieCell *customCell = [tableView dequeueReusableCellWithIdentifier:#"cellID" forIndexPath:indexPath];
return customCell;
}
The problem is when I scroll down enough that the label is out of view, and then I scroll back up, the label isn't there anymore. The reason is obviously because when the cells get reused, I removed all the labels.
So is there a way to disable prepareForReuse (or just the code in the method) for the selected row, and how?
Cells that are scrolled away will be reused, and there's no way around it. Even if you avoid the removeFromSuperview logic, that cell will reappear at a different index path, probably not where you want it.
The way to conditionally configure cells is in cellForRowAtIndexPath. There, you can ask if the indexPath is among the table view's indexPathsOfSelectedCells. If it is, then configure it with the extra labels, or not, if not.
One way to reduce the messiness is to have those labels remain in the cell unconditionally, just setting their alphas to 0 or 1, depending on the selection state.
For example, in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
// if you know the table has singular selection
NSIndexPath *selectedIndexPath = [tableView indexPathForSelectedRow];
BOOL rowIsSelected = [indexPath isEqual:selectedIndexPath];
// OR, for multiple select...
NSArray *selection = [tableView indexPathsForSelectedRows];
BOOL rowIsSelected = [selection containsObject:indexPath];
// now either conditionally create/destroy or show/hide the subviews
// that appear on selection (I prefer show/hide for simpler cells)...
[cell configAsSelected:rowIsSelected]; // have the custom cell do it
// in that method, or here, if you're less OO-inclined...
cell.subviewThatAppearsOnSelected.alpha = (rowIsSelected)? 1.0 : 0.0;
The larger point is, this is the suggested place to reliably configure a cell based on the model and its current position in the table
Think of table cells as dumb containers that get reused to hold different things (labels, images, buttons, etc.).
You fill the cells in cellForRowAtIndexPath.
You empty them in prepareForReuse so they can be filled again and reused.
Don't confuse these two actions.
When you fill the cell, you should be filling it from data that you have stored somewhere else - i.e. not from other cells. If you are relying on indexPathsOfSelectedCells to help you when filling your cell, you are going to have problems. Don't do this.
Typically you would have an array of objects, where each object corresponds to a cell. You have as many cells in your table as objects in the array. You might initialize the objects in your array in viewDidLoad, or pass them in from a previous view controller.
This process doesn't have to be complicated. Most cells display only a few bits of data, so your object (often called a model) doesn't have to have many properties to hold this data.
When the user selects a cell, set a "selected" property in its corresponding object to indicate this. This value stays around in the object even when the cell is scrolled off the screen and reused. That's good.
Now when the user scrolls back to the cell, you fill the cell with data from the corresponding object. Since that object has its "selected" property set, you "fill" the cell by adding the label that you want there in this case. Or if it isn't set, you don't add the label.
In prepareForReuse, always remove the label to put the cell in its empty state, ready to be refilled.
I am working on an ios application,
I have a normal table view. When calling heightForRowAtIndexPath I am doing the folowing
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellId = [self getCellIds][indexPath.row];
BaseTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
return [cell calculateHeigh];
}
Basically I am dequeueing the cell because I have a function calculateHeigh inside every cell that will do the height calculation. this is working fine as intended however I have a concern:
Is it safe to call dequeueReusableCellWithIdentifier: inside the heightForRowAtIndexPath ? will it cause any issue?
EDIT:
Just to clarify why I did this, I have a big amount of custom cells with different identifier that needs to be loaded. and to avoid having a huge if-else statement in my heightForRowAtIndexPath I placed the getter of the cell height in the custom cell that way I just ask it to return it (no calculation is made there), I can't do it as a class method as I don't know which class, I can get the object from the identifier and not the class. And I want to avoid a big if-else just for code readability.
So my concern was with the dequeueReusableCellWithIdentifier: is it heavy to call it when getting the height? will it cause memory issues or lags? or is it worth to just do a bug if-else of use a dictionary?
First of all you should avoid any calculations in table drawing methods(such as heightForRow, cellForRow, etc). These methods are called a lot and although your table may be short and/or not complicated(with custom cells with a lot of labels, buttons and images) you should always try to optimize this drawing process or otherwise user will experience some nasty lag when scrolling.
So you should call some method to prepare data before calling 'reloadTable'
-(void)prepareMethod
{
//get only one cell to calculate all row heights
BaseTableViewCell *cell = [_myTableView dequeueReusableCellWithIdentifier:cellId];
for (NSDictionary* dataObj in _dataArray)
{
//loop through all rows data and set new property for row height
dataObj[#"rowHeight"] = [cell calculateHeigh];
}
}
And then when calling heightForRow just pass this value without any expensive operation(such as probably string calculations):
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//always make sure you don't access unexisting array index
return ( indexPath.row < _dataArray.count ) ? _dataArray[indexPath.row][indexPath.row][#"rowHeight"] : 1.0;
}
Of course you don't need separate method just to populate row heights in your data array - you can populate this value when populating(formatting) your data array to avoid second array iteration. It all depends on your current implementation.
Just remember that expensive drawing methods(not only for table though) should always be as short as possible and just get data needed for drawing and draw. It's really so simple. If you need to make some complicated calculations do it before that(maybe in view init) so your data is prepared before actual drawing. This way your application will be working smoothly even with bigger tables(because no matter how big the table is, UITableViewController draws only visible cells).
Regards,
hris.to
I don't like to have big if statements in heightForRowAtIndexPath and accessing a cell using dequeueReusableCellWithIdentifier. Your approach getting cell height from each cell quite is reasonable. I believe your calculateHeigh return value depends on the table data you pass into the cell.
In BaseTableViewCell.h
+ (CGFloat)heightWithData:(id)data;
In BaseTableViewCell.m
+ (CGFloat)heightWithData:(id)data
{
//put your calculateHeigh logic here. I believe your calculateHeigh depends on the data each cell has.
}
Then you can do
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [BaseTableViewCell heightWithData:[self.tableData objectAtIndex:indexPath.row]];
}
If you do this, you don't need to access each cell object to get cell height.
You should not use this method to provide the calculation. Based on what I can see on your setup, you are calculating the height based on the values already on your cell. What happens is that the cell dequeue system will give you a cell to reuse, but because it's sharing cells from multiple index paths, that cell probably has data that belongs to a record of an index path different from the current one. Get the calculate height code and try to reproduce it inside the datasource callback you are using.
I have a UITableViewController with UITextfield inside the tableview cells. If I scroll the table view, the user entered data in the textfields disappears. I tried to add the textfield data to a NSMutableArray but it still didn't work. Any help please.
When cellForRowAtIndexPath: is called, the cell you return has to be completely filled in with whatever data you want to show. So, if the cell includes a UITextfield, you'll need to set it's text property to the right value for that row in your data.
When a table cell disappears off the top or bottom of the screen, the UITableViewCell itself becomes available for re-use. (As you scroll, cells disappear, and new cells appear, but the UITableView class is re-using the UITableViewCell objects.) In cellForRowAtIndexPath: when you get a cached cell to use, you have to be sure to setup everything you want it to show for the row in question, otherwise you might see some odd behavior in your table.
Does this help?
EDIT:
Here's an example of the typical pattern used in cellForRowAtIndexPath:. Notice the use of dequeueReusableCellWithIdentifier:. That method returns a previously allocated but not in use UITableViewCell, if there is one. Notice further that if no cached cell is returned, the code creates a new one, and sets it up (with stuff that is independent of anything that might be row specific). Following that, you'd setup the cell as you need it for the row in question.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *SearchResultsCellIdentifier = #"SearchResultsCellIdentifer";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:SearchResultsCellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:SearchResultsCellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Your row-specific setup of the cell here
// ...
return cell;
}
Check the docs for specifics about these methods. There are LOTS of examples from Apple and elsewhere about how to implement tableViews.