Problem updating UITableViewCells when rotating UITableView - iphone

I have a UILabel in a custom UITableViewCell that gets resized when the device is rotated. The text in this label needs to be recalculated after the rotation because I am cutting it down to size and appending some text at the end.
E.g. the datamodel has: "This is a run-on sentence that needs to stop."
In portrait mode it becomes "This is a run-on sent... more"
In landscape mode it becomes "This is a run-on sentence that... more"
From (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
I am able to access the visible UITableViewCells and update the descriptions.
The problem seems to be that there are UITableViewCells that are cached but I can't get to. When I scroll the UITableView after a rotation, one or two cells that are below the visible area after the rotation don't have the correct text in the label. So they haven't been rendered via (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath - but they weren't returned by [tableView visibleCells] (or via looping through all views returned via [tableView subViews]).
I've tried to access the "extra" cells via this method:
for (int index=max + 1; index < max + 3 && index < [cellTypes count]; index++) {
NSIndexPath *updatedPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:updatedPath];
if (cell == nil) { continue; }
[self updateCellForRotate:cell forRow:index];
}
(where max is the biggest row returned from visibleCells) but cell is always nil.
Is there anyway to flush the cache of UITableViewCells so that they don't get re-used? Or to access them so I can update them?
Thanks!

Two things.
First. In your didRotateFromInterfaceOrientation method you can simply reload the visible rows like so:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation) fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
NSLog(#"didRotateFromInterfaceOrientation:%d",fromInterfaceOrientation);
[tableView reloadRowsAtIndexPaths:[tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationNone];
}
Then I would recommend you add either the interfaceOrientation number or simply the table width to the dequeue cell name that way the tableView knows that cells in one rotation are different from those in another. Like so:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath withType:(NSString *)s_type
{
UITableViewCell *cell = nil;
// add width of table to the name so that rotations will change the cell dequeue names
s_cell = [s_cell stringByAppendingString:[NSString stringWithFormat:#"%#%d",#"Width",(int)tv.bounds.size.width]];
NSLog(#"%#",s_cell);
cell = [tv dequeueReusableCellWithIdentifier:s_cell];
if( cell == nil ) {
cell = [[[UITableViewCell alloc];
initWithFrame:CGRectZero reuseIdentifier:s_cell] autorelease];
}
}

Firstly, to reload all of your table cells use [self.tableView reloadData]
Secondly, add the line of code that is responsible for the shrinking inside the (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method.
Example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//Some identifier and recycling stuff
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
//Make labels smaller
}
else {
//Make them bigger
}
}
Or you can just call your updateCellForRotate:forRow: method when making them. But I'm not sure how that function works, so I can't be too specific.

When you create the cell in cellForRowAtIndexPath:, add it to an array. Then, loop through the array, updating the text as necessary.
Hope this helps,
jrtc27
EDIT:
You say they are custom cells - could you not update your text in your UITableViewCell subclass?

So, I was having (what I think was) a very similar problem recently, and none of the posted answers helped me, I'm sorry to say.
My issue was that I deliberately resized and repositioned the UITableView upon rotation, and I did that programatically. The table cells in portrait took up the width of the view, and in Landscape were made somewhat higher but less wide. I then repositioned the elements of the cell depending on the orientation we'd come to.
Upon application start, the first viewing of the table was fine. Then I rotated and found that I appeared to have two instances of some elements, and these appeared to be where the cells had been visible in the first table. Rotating back then corrupted the initial orientation table with elements from the previous table.
I tried all of the applicable answers above, until I looked closer at the cellForRowAtIndexPath code:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
I understand cell re-use is a great idea and all, but I really didn't need to retain (as in preserve) any cells and wanted them all bright, spangly and new after each rotation.
EDIT: In my own app I'll have maybe 20-30 rows maximum, as I personally don't like hugely long tables. If there were going to be lots of rows returned for a particular query I'd have some filters available to the user to help them sort out which rows they wanted. If you're going to have loads of rows displayed, then dequeuing them may cause you a performance impact that you don't want.
All I did was comment out the if and the following bracket, and my table cells renewed exactly as I wanted them to:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
//}
Apologies for the waffle, and the late answer to an old question.
Ben.
Waffles and cream, or syrup.

You can use this simple line on the shouldAutorotateToInterfaceOrientation method :
self.view.autoresizesSubviews = YES;
For me it works always successfully

Related

UITableview strange behaviour

I have a UITableView with a top navigation bar. The data for the UITableView comes from an array which contains more than 20 objects.
Everything is fine so far. However sometimes when i do repeated scrolling (fast), I find last row being repeated, sometimes gets cut into half, overwritten.
I am new to iPhone development and have no clue why this happens.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return [appDelegate.preList count];
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
Item *i=[appDelegate.preList objectAtIndex:indexPath.row];
cell.textLabel.text=i.iName;
cell.accessoryType=UITableViewCellAccessoryNone;
return cell;
}
I have attached a screenshot of my problem as well. Notice how the navigation bar and the last cell are.
Help would be greatly appreciated
This looks like you have added two identical table views to your view. If you place a break point on numberOfSections… and then inspect the tableView object, I bet you will find two different table views. Start by looking for places in your code where you [self.view addSubview:tableView] or something similar. Remember, if you are using IB to setup your table, you do not need to programmatically add it.
I think you are seeing this when scrolling fast because only then the tableView bounces leaving some content offset. You have probably added 2 tableViews back to back.

Deleting a cell in tableview, the cell also can be reused,why?

at first,i delete one cell in tableView,then i want to add another new cell in the tableView,but the new cell is not new,it is just the cell which i delete. i know what dequeueReusableCellWithIdentifier means.i think when delete one cell,the cache in dequeueReusableCellWithIdentifier is also deleted.why not?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"table reload");
PDFModel* tempmodel=[PDFArray objectAtIndex:indexPath.row];
NSString * CellIdentifier=tempmodel.PDFName;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSLog(#"cell %x",cell);
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
//custom the cell
cell.text=...
}
return cell;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle==UITableViewCellEditingStyleDelete)
{
[PDFArray removeObjectAtIndex:indexPath.row];
NSMutableArray* array=[[NSMutableArray alloc] init];
[array addObject:indexPath];
[self.tableView deleteRowsAtIndexPaths:array withRowAnimation:UITableViewRowAnimationRight];
}
}
There's no reason to delete the UITableViewCell since it can be reused. That cell won't be displayed until it is processed again by cellForRowAtIndexPath, so it won't show up with the old data. The laid-out cell, though -- the view and all of its subviews -- can be reused after new data replaces the old data.
Think of it like each cell is a whiteboard (dry erase board) where it's cut down to a certain size and all of the basic information areas are etched into the surface: a place for some text, another area for a number, a special space for drawing a picture, etc. In tableView:cellForRowAtIndexPath: when you check whether the cell == nil, you're trying to find out if there are any already-etched whiteboards available that you can erase and write the new information on with your dry erase marker. If there aren't then it cuts out and etches a brand new board to write information on, but if there is one that can be erased then it just wipes off the old information with an eraser and writes in the new information. As you can imagine, this saves a lot of time.
When a user is scrolling the table what's actually happening is that when one cell (whiteboard) scrolls off the top and is no longer visible, the system erases the data and moves it to the bottom of the stack; it will be the next one to scroll into view. As a result the system only has to create as many whiteboards as can be visible at any one time -- the ones off-screen don't actually exist.
When you delete a cell all you're really doing is telling the table to (a) stop displaying that cell, and (b) setting a flag so the table can use the eraser to wipe off the old data but still reuse the whiteboard.
Creating cells can be very costly in terms of computing power. If a UITableView had to create a new cell every time one came into view while the user was scrolling -- in the analogy, if it had to cut out a new whiteboard and etch all of the areas into it every time one came into view -- the scrolling would be very jerky and look terrible. By reusing cells, replacing just the changing contents, the table can move smoothly as the user interacts with it.
Your question is a bit weird, but I guess you're wondering what the following code does?
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
What the code does, is that it tries to retrieve a cached UITableView cell. This is to reduce memory usage on the iPhone.
Once you delete a cell, you should also remove the data from the datasource (usually an array), e.g.:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle==UITableViewCellEditingStyleDelete)
{
// remove object from array ...
[dataArray removeObjectAtIndex:indexPath.row];
}
}
(Remember: you can only remove items from an NSMutableArray at runtime, not from a NSArray)
If the cell gets redrawn, you will first check which cell needs to be redrawn, then you retrieve the appropriate data from the datasource, this is all done by the UITableViewDataSource delegate methods. You only have to make sure to retrieve the right content from the array and update the cell's content on redraw, e.g. like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"CellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
// no cached cell is found, create a new cell ...
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
// add some defaults for cells in the initializer,
// stuff that's done here should be important for
// all displayed cells, for example background image.
}
// update the cell's content, e.g.: - it will only display content that's currently in the array, deleted objects shouldn't exist in the array at this point
NSString *title = [dataArray objectAtIndex:indexPath.row];
[cell.textLabel setText:title];
return cell;
}
I think You can use
NSString *CellIdentifier = [NSString stringWithFormat:#"Cell%d",indexPath.row];
instead of
NSString * CellIdentifier=tempmodel.PDFName;
please replace it & check it. I think it's helpful to you.
You seem confused by the difference between deleting a cell (an objective-c UI object) and the data it displays.
When you allow a user to delete a cell in the user interface you need to delete the corresponding data from your data model
The implementation of UITableView is highly optimized in iOS and it caches cells (UI objects) to improve performance. You don't really need to be concerned about this as your table will be optimally loaded and displayed based on your data.
In tableView:cellForRowAtIndexPath: you do the following:
Attempt to dequeue a cell instance. This takes advantage of caching done by UITableView
If dequeue does not return a cell, create a new one.
Once you have a cell, configure it with the appropriate data from your data model
When a user chooses a delete action from a cell, delete the data from your data model. optionally, you can animate removing the cell from the table or simply reload the whole table.
The specifics of #4 depend on the user interface you want and how you've configured your data model.
The Apple docs are pretty good about explaining this.
http://developer.apple.com/library/ios/#documentation/userexperience/conceptual/TableView_iPhone/AboutTableViewsiPhone/AboutTableViewsiPhone.html

UITableViewDelegate methods

Here is an xcode project that I just did to ask this question:
http://www.mediafire.com/?z26ufsfhby62br9
Open the log and run it. You will find that it outputs display: x and create: x where x is a number 0 to 49 and corresponds to the cell of the same number. It should only output to 22 before any scrolling is performed as Apple are always boasting that their tableviews are loaded as needed.
It basically shows that tableView:willDisplayCell:forRowAtIndexPath: and tableView:cellForRowAtIndexPath: are fired for each cell pretty much as soon as the tableview appears, why is this?
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"create: %i", indexPath.row);
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
cell.textLabel.text = [NSString stringWithFormat:#"%i", indexPath.row];
return cell;
}
- (void) tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"display: %i", indexPath.row);
}
Why are the above meothds called just after the tableView loads (and just before each cell appears)? Surely they should be called just before each cell appears only?
These are the default delegate methods.. This will be be called for each cell everytime.
You use willDisplayCell:forRowAtIndexPath: to configure things like font and text color. In the newer version of the iPhone, with certain table configurations, if you configure things like the label text color in the tableView:cellForRowAtIndexPath: method, your changes will be lost at some point before the cell is actually displayed. Here you can do things like change the label's color, adjust background highlighting, such things as these.
If your table view is reloading before it actually fully displays, that could cause the behavior you're seeing. Then the cells would get initialized, prepared for display, but then all of that would be lost (before even shown on screen) as the table is reloaded.

UITableViewCell: how to verify the kind of accessoryType in all cells?

I have a UITableView in that some cells are marked with UITableViewCellAccessoryCheckmark at the initialization of the view.
When the user selects another row, I have to check if the maximum number of selected rows was achieved before. To do that, I used the code bellow:
- (NSInteger)tableView:(UITableView *)tableView numberOfSelectedRowsInSection:(NSInteger)section{
NSInteger numberOfRows = [self tableView:tableView numberOfRowsInSection:section];
NSInteger numberOfSelectedRows = 0;
for (int i = 0; i < numberOfRows; i++) {
UITableViewCell *otherCell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:section]];
if (otherCell.accessoryType == UITableViewCellAccessoryCheckmark) {
numberOfSelectedRows++;
}
}
return numberOfSelectedRows;
}
If my number of rows is, as example, 20, the variable numberOfRows is setted correctly with 20. Lets say that 13 rows already are marked with UITableViewCellAccessoryCheckmark. So, numberOfSelectedRows should be 13 after the loop, but only the marked and VISIBLE cells are considered. So, if I have 9 cells showed and 7 are marked, the numberOfSelectedRows returns 7 instead of 13 (but the for iterate 20 times, as expected).
Is this a correct behavior of UITableView or it is a bug of iPhone simulator?
Thanks in advance.
Yes, it works as designed. You should never store model data in your views. UITableView knows nothing about the data, it only displays cells (and throws them aways as soon as they scroll off the screen). You need to store the checkmark state of each cell in a model object (e.g. an array) that you then access from your view controller.
This is correct behavior.
The UITableView is not a list. The system caches cell that are off screen to save memory and CPU and they can not be iterated over in a manner that makes sense.
Ok, you should keep track of the model/data and the tableView will keep track of displaying it. I have had some problems with this until I accepted that uitableView is not a list:)
So, have an array of objects that each corresponds to the data in the a cell. When building the individual cells like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"categoryCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
Item *item = [self.itemList objectAtIndex:indexPath.row];
[cell.textLabel setText:[item itemBrand]]; //notice that here we set the cell values
return cell;
}
The when a user clicks you change you model like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"IndexPat.row%i", indexPath.row);
Item item = (Item*) [self.itemList objectAtIndex:indexPath.row];
//change the state of item
}
This way the tableView will update to resemble the model/data, you just managed the model.

UITableView with a single custom Cell? (iPhone)

I have run into a bit of a problem. Usually when dealing with UITableView I will build a special
method, configureCell, that builds the cells as the tableView needs them. So cellForRowAtIndexPath queries the configureCell to get a new cell if none is found in the cache.
In the part of cellForRowAtIndexPath that gets run every time a user scrolls:
- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [theTableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
[self configureCell:cell atIndexPath:indexPath];
}
//this part is always executed!
}
I set the label values etc. from my model. This works fine and this, I believe, is how it should be to work properly and be the least strain on the CPU. From what I can read in the TableView Guide.
My problem is now that the first Cell in my tableView is different than the others. I has a special icon and other things that separates it from the other cells.
In my configure cell I then tried asking:
if (indexPath.row == 0) {
//do special setup!
}
This is a bad idea as the tableView lazy loads the cells and therefore the first cell that is off-screen, when being scrolled on-screen, will also get indexPath.row = 0 so now I have a "special cell" for every 7 cells.
How do I get around this one?
A second problem that also originates from the above: I have a custom UIView placed in all the cells accessoryView. When the user taps an "Edit" button, all the cells accessoryViews should change to an icon indicating that we are in "Edit mode". Here, again, this only happens for the cells on screen, when scrolling "old" cells are either fetched from cache or new cells are build that doesn't know we are in edit mode.
When You tap a Cell, there is never any doubt about the indexPath.row of the cell tapped, this index you need to pair with your model array to figure out what the user tapped. But it seems different rules apply when the TableView is manipulating the cells (pulling them on-screen, off-screen, deleting, adding).
I guess, I am asking; to follow best practice and not fight the SDK, what should I do to obtain the desired functionality and at which stage in the Cell life-cycle should I apply this logic?
Hope someone can guide me in the right direction:) thank You in advance:)
I think the problem is not that the first off-screen cell is at indexPath.row = 0.
The problem is that you are using the same CellIdentifier for regular and custom cells. So when the TableView goes to dequeue a cached cell, it is sometimes grabbing your custom cell that has the icon in it.
Since you gave it the same CellIdentifier as your regular cells, it doesn't know that they are not the same.
Something like this should fix it:
static NSString *CellIdentifier;
if([indexPath row] == 0){
CellIdentifier = #"CustomCell";
} else {
CellIdentifier = #"Cell";
}
I'm not sure about your edit button problem, but it could be related.