Custom UITableViewCell backgrounds, when to reload, etc - iphone

I've set up a custom grouped tableview style using the following code in cellForRowAtIndexPath. Each cell is given a new background view and it's corners are rounded according to its position in the section. I use this style throughout my app and in various views I use animations to add and delete rows. (Using [tableview insertRowsAtIndexPaths:], etc).
If the last row of a section is inserted or removed I need to be able to reload the cells background view corners. How can I do this without calling [tableview reloadData] or even reloadCellsAtIndexPaths? Both of those functions mess up the animations of inserting and deleting cells. Is there somewhere I can put this background corner definition that would get called at the appropriate time? How does the default grouped tableview do this?
Sorry if this is unclear. Ask for clarification and I will edit. Thanks! :)
UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:[self styleForCellAtIndexPath:indexPath]
reuseIdentifier:cellIdentifier] autorelease];
// Theme the cell
TableCellBackgroundView *backgroundView = [[TableCellBackgroundView alloc] initWithFrame:cell.frame];
TableCellBackgroundView *selectedBackgroundView = [[TableCellBackgroundView alloc] initWithFrame:cell.frame];
selectedBackgroundView.selectedStyle = YES;
// top row, without header
BOOL header = [self tableView:aTableView heightForHeaderInSection:indexPath.section]!=0;
if (indexPath.row == 0 && (!header || tableTheme==kTableThemeSimple)) {
backgroundView.topRadius = 6;
selectedBackgroundView.topRadius = 6;
}
// bottom row
if (indexPath.row == [self tableView:aTableView numberOfRowsInSection:indexPath.section]-1) {
backgroundView.bottomRadius = 6;
selectedBackgroundView.bottomRadius = 6;
}
cell.backgroundView = backgroundView;
cell.selectedBackgroundView = selectedBackgroundView;
cell.contentView.backgroundColor = [UIColor clearColor];
cell.backgroundColor = [UIColor clearColor];
}
}

If you have a method that you call for the custom animation, when the animation is completed, or rather prior to the display of the new cell, using a subclass of UITableViewCell make a call to a method named something like - (void) fixCornersWithIndexPath: (NSIndexPath *) indexPath.
- (void) customAnimationForCellAtIndexPath: (NSIndexPath *) path {
//your animation things
CustomCell *cell = [self.tableView cellForRowAtIndexPath: indexPath];
[cell fixCornersWithIndexPath: path andRowCount: [self tableView:aTableView numberOfRowsInSection:indexPath.section]];
}
and then fix corners should be something like this:
- (void) fixCornersWithIndexPath: (NSIndexPath *) indexPath andRowCount: (NSInteger *) rowCount {
if (indexPath.row == 0 && (!header || tableTheme==kTableThemeSimple)) {
self.backgroundView.topRadius = 6;
self.selectedBackgroundView.topRadius = 6;
}
// bottom row
if (indexPath.row == rowCount-1) {
self.backgroundView.bottomRadius = 6;
self.selectedBackgroundView.bottomRadius = 6;
}
}
Hope this helps!

I think this is what you're looking for. After you've altered your model, call:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
This will cause your cellAtIndexPath method to be invoked. Send an array with a single index path of the row that's out of date (I guess for the rounded corners problem, that's either the last row after a row was removed, or the row before a newly added last row).
You can call it along with your other update methods in between begin/end updates.

The solution that worked for me is this:
Set the background in tableView:willDisplayCell:forRowAtIndexPath:
Create a convenience method called reloadBackgroundForRowAtIndexPath that calls willDisplayCell manually
When I add a row, call the convenience method after a delay to allow the table animations to finish
- (void)reloadBackgroundForRowAtIndexPath:(NSIndexPath*)indexPathToReload {
[self tableView:self.tableView willDisplayCell:[self.tableView cellForRowAtIndexPath:indexPathToReload] forRowAtIndexPath:indexPathToReload];
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Logic to determine the background image
}
// elsewhere, update the table, then
NSIndexPath *indexPathToFix = [NSIndexPath indexPathForRow:count-1 inSection:0];
// If the new row is coming out, we want to update the background right away. If the new row is going away, we want to wait to update until the animation is done.
// I don't like hard-coding the animation length here, but I don't think I have a choice.
[self performSelector:#selector(reloadBackgroundForRowAtIndexPath:) withObject:indexPathToFix afterDelay:(self.tableView.isEditing ? 0.0 : 0.2)];

Related

What is wrong with my UITableView cellForRowAtIndex for Single Selection?

Below is code for UITableView, But when i scroll its behaves weirdly (too annoying)... This problem is due to reuseIdentifier.... but dont know how to solve..
- (UITableViewCell *)tableView:(UITableView *)tableView1 cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView1 dequeueReusableCellWithIdentifier:CellIdentifier];
NSInteger imgTag = 1;
NSInteger lblTag = 2;
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(2, 2, 52, 52)];
// Image:[UIImage imageNamed:[self.glassType objectAtIndex:indexPath.row]]];
imgView.tag = imgTag;
[cell.contentView addSubview:imgView];
[imgView release];
UILabel *lblName = [[UILabel alloc] initWithFrame:CGRectMake(60, cell.frame.size.height/4, 200, 21)];
// lblName.text = [self.glassName objectAtIndex:indexPath.row];
lblName.tag = lblTag;
[cell addSubview:lblName];
[lblName release];
}
NSInteger imgIndex = 2;
NSInteger lblIndex = 3;
((UIImageView *)[cell viewWithTag:imgTag]).image = [[self.glassType objectAtIndex:indexPath.row] objectAtIndex:imgIndex];
((UILabel *)[cell viewWithTag:lblTag]).text = [[self.glassName objectAtIndex:indexPath.row] objectAtIndex:lblIndex];
return cell;
}
- (void)tableView:(UITableView *)tableView1 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView1 cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
How to make Cell for row at index so that it remains constant even when scrolled??? Also how to make single selection in UITableView??
The answer is that you should not add subviews to your table cells outside of the "if (cell == nil) { ..." clause or they get added over and over again to the same cell when it gets re-used.
See my answer to this question for a more detailed explanation, including code for how to fix it:
cellForRowAtIndexPath memory management
You also cannot store state in table cells because as soon as they scroll offscreen they are recycled and re-appear at a different index in your table. You need to set up an array of model objects to store state for your table cells (such as what their accessory type should be). A more detailed explanation can be found in my answer to this question:
Looping through UITableViewCells of a UITableView
If you fix how you are adding subviews to the cells, and store your "ticked" state in an array of model objects as well as setting the cell.accessoryType (so that it can be restored when the cell is dequeued), then your approach to row selection is otherwise correct.
So put this in your tableView:cellForRowAtIndexPath: method, just before the return cell;:
MyModelObject *object = [self.arrayOfModelObjects objectAtIndex:indexPath.row];
BOOL isChecked = object.checked;
cell.accessoryType = isChecked? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
And in your tableView: didSelectRowAtIndexPath: method, get rid of the current logic and replace it with:
- (void)tableView:(UITableView *)tableView1 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
for (int i = 0; i < [self.arrayOfModelObjects count]; i++)
{
MyModelObject *object = [self.arrayOfModelObjects objectAtIndex:i];
object.checked = (i == indexPath.row); // only check the one we just tapped
}
//refresh table to update the accessory views for all rows
[tableView1 reloadData];
}
Obviously replace the arrayOfModelObjects with your own model implementation. You could just use an array of NSNumber objects containing bools if you don't want to create a custom class for this purpose.
The recycling queue is like a pool where previously created Cells are stored before to reuse them. For example when you scrolls up, at the moment the cell disappears above, it is stored in the queue and becomes available for the cell that will appear at the bottom. Ok ?
Actually the number of cells really created is exactly the max simultaneous cell you can display in your table (in most cases from 3 to 8). In other words your if (cell == nil) code is executed (more or less from 3 to 8 times) at the first reloadData to create the pool of cells your table needs.
Then all you make on a cell is kept as it and appears again when you dequeue it. It's now easy to understand that, in your code, you have to make all strictly row-dependant settings outside the if (cell == nil) block. The same way, do not add subViews outside the if (cell == nil) block, you can imagine the thousands of subview you will add each time you reset a dequeued cell !
Tip: if you need some custom cleanup before reusing a cell (like to set an image to blank), you can create a custom UITableviewCell class and implements the prepareForReuse method.
Is it clear ?
Always reload your tableView in viewWillAppear method instead of viewDidLoad.
(void) viewWillAppear:(BOOL)animated{
[self.tableView reloadData];
}
This avoids most of all unexpected and annoying problems. :)

How can I change the background color of a UITableViewCell dynamically?

I have a variable that is going to keep track of how many cells need to be colored. So if that variable is 3, then the top three cells backgroundcolor will change. How can I do this?
I know I need to update this in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
But how can I ensure the top cells have a different background color based on my variable?
The indexPath parameter is your starting point. If coloredCells is an integer that holds the number of cells you are coloring, your method would include something like
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
// fetch or create the cell, first
UITableViewCell *cell = // ...
// then set it up
if(indexPath.row < self.coloredCells) {
cell.contentView.backgroundColor = [UIColor redColor];
} else {
cell.contentView.backgroundColor = [UIColor whiteColor];
}
// perform rest of cell setup
// ...
return cell;
}
Now, if you adjust the value of coloredCells, you'll need to inform the table view that some of its views have changed. The laziest way to do that is to reload the whole table:
// elsewhere...
self.coloredCells = 4;
[self.tableView reloadData];
Or you can take a little more effort to reload just the cells that have colored backgrounds:
self.coloredCells = newColoredCount;
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:newColoredCount];
for(int i = 0; i < newColoredCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
You would test for the row number and change the color accordingly. In cellForRowAtIndexPath use:
//having allocated or recycled a cell into myCell pointer above
//test for row and assign background color like so
if (indexPath.row < 3) {
myCell.contentView.backgroundColor = [UIColor greenColor];
} else {
myCell.contentView.backgroundColor = [UIColor redColor];
}
//continue configuring your cell
You can use the tableView:willDisplayCell:forRowAtIndexPath: of your UITableViewDelegate to change things like the background colour of the cell. Changes made in cellForRowAtIndexPath may be lost before the cell is actually displayed, so it's usually better to do it in this method.
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.row < numberOfColoredRows) {
cell.backgroundColor = [UIColor redColor];
}
}
From the discussion of this method in the reference:
A table view sends this message to its delegate just before it uses
cell to draw a row, thereby permitting the delegate to customize the
cell object before it is displayed. This method gives the delegate a
chance to override state-based properties set earlier by the table
view, such as selection and background color. After the delegate
returns, the table view sets only the alpha and frame properties, and
then only when animating rows as they slide in or out.
No, you don't have to update the tableView:cellForRowRowAtIndexPath: delegate method. All you have to do is this:
[self.tableView cellForRowAtIndexPath:indexPath].backgroundColor = desiredUIColor;
Note that calling tableView:cellForRowAtIndexPath: from type id<UITableViewDataSource> is different from calling cellForRowAtIndexPath: from type UITableView. The former calls the delegate method (this should never be directly called) and the latter returns the current cell for an index path without recalculating the cell.
If you only have one section in your table view, the math to calculate the top n cells is easy. If your "variable that is going to keep track of how many cells need to be colored" is (NSUInteger)numberOfHighlightedCells, this is some simple loop code your could run:
NSUInteger i;
for (i = 0; i < numberOfHighlightedCells; i++) {
[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]].backgroundColor = desiredUIColor;
}
However, if you have more than one section in your table, some very complicated calculating of index paths may be necessary.

Custom editingAccessoryView behaving unpredictable

Okay, first off - how I want my cells to look in my UItableView in editing mode (with some nicer buttons):
However - this is how it looks right now:
My problem is that my custom EditingAccessoryView only appears on the cell that I first created, and that those circle-thingies (what are those called?) appears. Which doesn't do much good.
Now, my code looks like this (which seems like the common way of doing this, seen at this question for example: How to add Custom EditingAccessoryView for UITableView?)
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
cell.editingAccessoryView = AccessoryView;
AccessoryView.backgroundColor = [UIColor clearColor];
}
I have read that you are supposed to be able to call your custom editingAccessoryView by swiping, even if - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath returns "NO". This however I have not been able to achieve.
So, to sum it upp; I want my editingAccessoryView to be displayed at all cells - and, if possible, remove the red circle. Or, alternatively, call my editingAccessoryView when I swipe - what method gets called when the user does this?
Any help would be much appreciated.
I suggest you subclass table cell and override following method
- (void)layoutSubviews
{
if([self isEditing])
{
// your custom stuff
[self.contentView addSubview:myButton];
}
}
You need to make a new accessoryView for every cell.
Instead of setting all this in the cellForRowAtIndexPath.... do this.
In cellForRowAtIndexPath
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
And set your accessory view in
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
NSLog(#"Am I Editing");
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
selectedCell.editingAccessoryView = AccessoryView;
AccessoryView.backgroundColor = [UIColor clearColor]
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class,
// insert it into the array, and add a new row to the table view
}
}
To remove red circles you can return UITableViewCellEditingStyleNone in
- (UITableViewCellEditingStyle)tableView:(UITableView*)tableView editingStyleForRowAtIndexPath:(NSIndexPath*)indexPath;
But in this case the empty space for that circle is still there. I'm almost sure, it can be removed too.
My problem is that my custom EditingAccessoryView only appears on the cell that I first created
Try to instantiate new AccessoryView for each cell instead of using the same one.

Force UITableView to dump all reusable cells

I have a UITableView where I have the backgroud color set via
UIView *myView = [[UIView alloc] init];
if ((indexPath.row % 2) == 0)
myView.backgroundColor = [UIColor greenColor];
else
myView.backgroundColor = [UIColor whiteColor];
cell.backgroundView = myView;
[myView release];
The problem I find is that when I edit a table (via setEditing:YES...) some cells of the same color invariable are next to each other. How do I force UITableView to fully redraw. reloadData is not doing a great job.
Is there are deep-cleaning redraw?
I had this issue before so I'll share with you how I solved it:
You can use a boolean flag (say it's called needsRefresh) to control the behavior of cell creation in -cellForRowAtIndexPath:
An example:
- (UITableViewCell*) tableView:(UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath {
UITableViewCell *cell = [tableView dequeueResuableCellWithIdentifier:SOME_ID];
if(!cell || needsRefresh) {
cell = [[[UITableViewCell alloc] init....] autorelease];
}
//.....
return cell;
}
So, when you need a hard reload, set the needsRefresh flag to YES. Simple as a pimple.
For me the accepted answer didn't really work since I had no idea when to set the needsRefresh back to YES.
What worked for me was:
- (UITableViewCell*) tableView:(UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath {
UITableViewCell *cell = [tableView dequeueResuableCellWithIdentifier:customCellIdentifier];
if(nil == cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:customCellIdentifier];
}
//.....
return cell;
}
And then you change the customCellIdentifier value whenever you need to. This way the cells are also still reusable if you switch back to the original cell identifier.
The accepted method seems dirty, it just makes a bunch of new cells that are stored along with the bad ones. Here are a couple of solutions depending on your situation:
1.
first, for the situation described in the question you should not dump your cells and create new views on every cycle. You need to tag your view and then get it back when from the cell when you get a reuse cell:
- (UITableViewCell*) tableView:(UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath {
UITableViewCell *cell = [tableView dequeueResuableCellWithIdentifier:SOME_ID];
if(!cell) {
cell = [[UITableViewCell alloc] init];
UIView *myView = [[UIView alloc] init];
cell.backgroundView = myView;
[myView setTag:5]; //<------
}
UIView *myView = [cell viewWithTag:5]; //<------
if ((indexPath.row % 2) == 0)
myView.backgroundColor = [UIColor greenColor];
else
myView.backgroundColor = [UIColor whiteColor];
return cell;
}
//then just reload the tableview.
2.
...or even better, why not just use the cell backgrouncolor and update that without creating a view.
3.
A sure way to really clear out old cached cells it to simply recreate the UITableView object.
4.
In most cases you dont need to destroy these cells, just keep track of your elements and update them after getting the reusable cell.You can tag all your elements, keep a array reference to them, find them thought the view hierarchy... Im sure theres a bunch of other ways.
5.
heres a one liner to directly purge all cells, although not best practice to mess with the internals of objects like this as they might change in future versions:
[(NSMutableDictionary*)[tableview valueForKey:#"_reusableTableCells" ] removeAllObjects];
I was able to solve this by adding a refresh variable to the table datasource. I used a dictionary for each cell, but there's an extra key called #"refresh":#"1", indicating the cell needs refreshing. Once it's updated, I set that key's value to #"0". So whenever the table is reloaded, make sure the key goes back to #"0" again.
#define TABLE_VIEW_CELL_DEFAULT_ID #"cellIdentifier"
#property (nonatomic, strong) NSString *tableViewCellIdentifier;
#property (nonatomic) NSUInteger tableViewCellIdentifierCount;
// By using a different cell identifier, this effectively flushes the cell
// cache because the old cells will no longer be used.
- (void) flushTableViewCellCache
{
self.tableViewCellIdentifierCount++;
self.tableViewCellIdentifier = [NSString stringWithFormat:#"%#%i", TABLE_VIEW_CELL_DEFAULT_ID, self.tableViewCellIdentifierCount];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.tableViewCellIdentifier];
if (cell == nil) {
cell = [[MyTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:self.tableViewCellIdentifier];
}
// rest of method...
}

Why doesn't UITableViewCell selection not draw if I tap very quickly?

I have a a UITableView with cells that are partially custom using addSubview. I am using a different cell ID for the last cell, whose purpose is to load more data from a server which will make new cells appear. (Think the "Load more messages from server" cell in Mail.app)
E.g.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// <... code for normal cells omitted...>
static NSString *LoadMoreCellIdentifier = #"LoadMoreCellIdentifier";
UILabel *loadMoreLabel;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LoadMoreCellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:LoadMoreCellIdentifier] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleGray;
loadMoreLabel = [[[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, cell.frame.size.width, self.tableView.rowHeight - 1)] autorelease];
loadMoreLabel.tag = LOAD_MORE_TAG;
loadMoreLabel.font = [UIFont boldSystemFontOfSize:16.0];
loadMoreLabel.textColor = [UIColor colorWithRed:0.153 green:0.337 blue:0.714 alpha:1.0]; // Apple's "Load More Messages" font color in Mail.app
loadMoreLabel.textAlignment = UITextAlignmentCenter;
[cell.contentView addSubview:loadMoreLabel];
}
else
{
loadMoreLabel = (UILabel *)[cell.contentView viewWithTag:LOAD_MORE_TAG];
}
loadMoreLabel.text = [NSString stringWithFormat:#"Load Next %d Hours...", _defaultHoursQuery];
return cell;
}
As you can see above, I set cell.selectionStyle = UITableViewCellSelectionStyleGray;
When you tap on a cell, I clear the selection like so:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (_showLoadMoreEntriesButton && indexPath.row == [_sourceArray count])
{
// <... omitted...>
// Do a potentially blocking 1 second operation here to obtain data for more
// cells. This will potentially change the size of the _sourceArray
// NSArray that acts as the source for my UITableCell
[tableView deselectRowAtIndexPath:indexPath animated:NO];
[self.tableView reloadData];
return;
}
[tableView deselectRowAtIndexPath:indexPath animated:NO];
[self _loadDataSetAtIndex:indexPath.row];
}
The problem I'm seeing is that I have to tap and hold my finger down to have the gray highlight appear. If I tap very quickly, it doesn't display draw a highlight at all. The problem being that sometimes I perform an blocking operation that will take a second or so. I want some simple UI feedback that something is happening, instead of the UI just locking up. I think this may be related to me conditionally checking for if the indexPath is the last row of the table or not.
Any idea how to get it to draw the highlight every time?
If possible, move your blocking operation to another thread. NSOperation is probably a good system to use.
Apple's documentation
A random tutorial I found
There's no clean way to tell the UI thread to process anything while you're in the middle of a blocking operation.