In my iPhone app I have a table view where I add a tick image to a cell if that objects 'isConfirmed' value is true. When entering the detailed view I can edit the confirmed value, and upon popping back to the main table view I need to see the update and not only when I view the main table from a fresh.
So I am using this code in my tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method`:
UIImageView *tickImg = nil;
//If confirmed add tick to visually display this to the user
if ([foodInfo.isConfirmed boolValue])
{
tickImg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"ConfirmedTick.png"]];
[tickImg setFrame:CGRectMake(0, 0, 32, 44)];
[cell addSubview:tickImg];
}
else
{
[tickImg removeFromSuperview];
}
What this does it successfully add the tick image to my cells which have a true value for isConfirmed and when going into the detail view of an object and setting it to TRUE and retuning, the tick appears, however I can't get it to work the other, so if the tick is there and I go into the detail view to unconfirmed it, the tick doesn't disappear.
This is the code that is executed if [foodInfo.isConfirmed boolValue] is false:
UIImageView *tickImg = nil;
[tickImg removeFromSuperview];
Clearly this will not work -- tickImg is not pointing to the UIImageView. You need to somehow save the reference to the UIImageView. You could add the tickImg variable to the header of your class or make it a property or something.
Are you calling [self.tableView reloadData]; on the VC's viewWillAppear:?
Also, the approach you're using to configure the cell is error-prone. Since the tableView is reusing the cells, you can't be sure what state a cell is in when you dequeue it.
A better approach would be to build the cells consistently:
static NSString *CellIdentifier = #"MyCell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
// always create a tick mark
UIImageView *tickImg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"ConfirmedTick.png"]];
tickImg.tag = kTICK_IMAGE_TAG;
tickImg.frame = CGRectMake(0, 0, 32, 44);
[cell addSubview:tickImg];
}
// always find it
UIImageView *tickImg = (UIImageView *)[cell viewWithTag:kTICK_IMAGE_TAG];
// always show or hide it based on your model
tickImg.alpha = ([foodInfo.isConfirmed boolValue])? 1.0 : 0.0;
// now your cell is in a consistent state, fully initialized no matter what cell
// state you started with and what bool state you have
Related
I'm trying to add an activity indicator to certain cells in my UITableView. I do this successfully in the method didSelectRowAtIndexpath using
CGRect CellFrame = CGRectMake(260, 10, 20, 20);
actindicator = [[UIActivityIndicatorView alloc]initWithFrame:CellFrame];
[actindicator setHidesWhenStopped:NO];
[actindicator setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray];
actindicator.tag =1;
[[cell contentView] addSubview:actindicator];
The catch is I need to control these multiple activity indicators from ANOTHER METHOD. I figured a property was a way to do this however by initialising a new instance of actIndicator every time, I loose reference's to all but the 'latest' init of the activity indicator thus meaning I can only control one.
What do i need to do here (if even possible?) to maintain reference to all the actIndicators so i can begin animating ALL of them?
Or Can I somehow use the actindicator.tag to control some form of reference.
Many thanks for any help.
EDIT: (Derived from answer) to access all Instances of Activity indicator with a tag of 1 in tableView (visible cells only) can use below from another method:
for (UITableViewCell *cell in [self.tableView visibleCells]) {
UIActivityIndicatorView *actView = (UIActivityIndicatorView *)[cell.contentView
viewWithTag:1];
[actView startAnimating];
activityFlag = 1;
}
The method above will cycle through all visible cells and start animating the activity indicator.
To handle the case of the tableview being scrolled, I re-animate the indicators using the method below which is in cellForRowAtIndexPath. cellStateKey simply indicates if the cell has a checkmark next to it, if it does have a checkmark and my activityflag (async webserver call in progress)is set..then i want to continue animating.(technically re-start animation, as scrolling tableview stops it)
if ([[rowData objectForKey:cellStateKey] boolValue]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
if(cell.accessoryType == UITableViewCellAccessoryCheckmark &&activityFlag ==1){
for (UIView *sub in [[cell contentView] subviews]) {
if (sub.tag == 1) {
UIActivityIndicatorView *acView = (UIActivityIndicatorView *)
[cell.contentView viewWithTag:1];
[acView startAnimating];
}
}
As mentioned in origional question I initialise my activity indicators (and remove activity indicators aswell if required) in the method didSelectRowAtIndexPath
You can add UIActivityIndicatorView as cell's accessoryView.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryView = spinner;
[spinner startAnimating];
[spinner release];
}
A simple way to do this (assuming you're adding the indicator as in your code) is to first get a collection of the visible cells (or rows) in your table, by calling [tableView visibleCells]. Then iterate through the cells like this:
for (UITableViewCell *cell in [tableView visibleCells]) {
for (UIView *sub in [cell subViews]) {
if (sub.tag == 1) { // "1" is not a good choice for a tag here
UIActivityIndicatorView *act = (UIActivityIndicatorView *)sub;
[act startAnimating]; // or whatever the command to start animating is
break;
}
}
}
There's more complexity for you to deal with: in your original code, you need to make sure you're not adding an additional activity indicator to a pre-existing cell each time cellForRowAtIndexPath is called, and you need to account for the situation where the user might scroll the table at a later point, exposing cells that do not have their activity indicator turned on.
You need to make a custom UITableViewCell by extending it. Then have a UIActivityIndicatorView as a member of that Cell. Then you can access it and control it on a cell by cell basis.
Assuming that activity view indicator tag is unique in the cell.contentView you can try something like:
UITableViewCell *cell = [tableview cellForRowAtIndexPath:indexpath];
UIActivityIndicatorView *acView = (UIActivityIndicatorView *)[cell.contentView viewWithTag:1];
//acView will be your activitivyIndicator
I have created a grouped table view like in the image below. but data in the table row , appears and disappears when we scroll up (or) scroll down the page , when we scroll up first 2 rows will go up (beyond the view) then if we scroll down both rows will take same value,, y its so? can any one help me
Thanks in advance.
like following code i am designing the table cell where CustomCellStudentData is class there i am creating like lable frames, text field frames
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
CustomCellStudentData *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[CustomCellStudentData alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
}
switch(indexPath.section)
{
case 0:
switch (indexPath.row)
{
case 0:
{
tfText[0] = [[UITextField alloc] initWithFrame:CGRectMake(100,10, 200, 40)];
cell.accessoryView = tfText[0];
tfText[0].delegate=self;
tfText[0].placeholder =#"<Student Name>";
cell.primaryLabel.text =#"Student Name: ";
}
break;
case 1:
{
tfText[1] = [[UITextField alloc] initWithFrame:CGRectMake(100,10, 200, 40)];
cell.accessoryView = tfText[1];
tfText[1].delegate=self;
tfText[1].placeholder =#"<Student Age>";
cell.primaryLabel.text =#"Student Age: ";
}
}
}
UITableView reuses the cells so as to not bog down the memory. As soon as the two cells go up, they enter the pool of reusable cells. If the pool is not empty, dequeueReusableCellWithIdentifier: returns a cell from that pool (there is a dependency on the identifier here). As the table view needs cells for the new rows being scrolled in, it gets the cells from the pool and provides it to you for reconfiguration. So, the text field that you attached to the cell when configuring it for row 0 and 1 still remain unless you explicitly remove. Since we don't which cells we are going to get, we should reset all customizations that we do and configure it as a new cell. In this case, irrespective of the cell coming in, you should remove the text field as the accessory view first.
cell = [[[CustomCellStudentData alloc] initWithFrame:CGRectZero reuseIdentifier:nil] autorelease];
What Deepak said is correct.
Adding to his answer, in future, if you add any subView to cell view. Remove from the cell before you add anything on it. Use
for(UIView * view in cell.contentView.subviews){
[view removeFromSuperview]; view = nil;
}
before customizing the cell.
And yes, please increase your acceptance rate. Going back to the questions you asked before and accepting some answers should help.
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...
}
We couldn't find a way to animate UIViews inside a UITableCell as an action from a callback.
Suppose in a scenario where you click on a button on the UITableViewCell and it fires off an asynchronous action to download a picture. Suppose further that when the picture is downloaded we want a UIView in the cell to animate the picture to give user a visual feedback that something new is about to be presented.
We couldn't find a way to track down the UIVIew to invoke beginAnimation on because the original cell that the user clicked on might now be used for another row due to the nature of cells being reused when you scroll up and down in the table. In other words we can't keep a pointer to that UITableViewCell. We need to find another way to target the cell and animate it if that row is visible and don't animate if the row is scrolled out of range.
Keep the cell object different from the object being animated so the cell holds a UIView. When the animation callback occurs check to make sure that the UIView still exists and, if it does, animate the changes.
When the cell object gets bumped off the screen and recycled, release the UIView that would have been animated and create a new one. When the animation callback occurs it will have nothing to do because the UIView no longer exists.
A modification of the above is to keep some sort of object in the UIView that your callback can check to see if the animation is still appropriate. This could be some sort of unique identifier for the picture being downloaded. If the identifier changes, no animation is needed. If it matches, do the animation.
EDIT:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyTableCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:MyIdentifier] autorelease];
} else {
UIView *oldViewToAnimate = [cell.contentView viewWithTag:1];
[oldViewToAnimate removeFromSuperview];
}
UIView *viewToAnimate = [[UIView alloc] initWithFrame:CGRectZero]; //replace with appropriate frame
viewToAnimate.tag = 1;
[cell.contentView addSubview:viewToAnimate];
return cell;
}
When you spawn your download process you pass in [cell.contentView viewWithTag:1]. When the download is done, it will update the appropriate view. If the table cell was reused the view will no longer have a superview and will not update the wrong cell.
There are things you can do to make this more efficient but this is the basic idea. If you have a custom UITableViewCell than this will probably look a bit different.
EDIT 2:
To reuse the viewToAnimate objects to make sure that they get updated if their parent cells were recycled, do something like the following:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyTableCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:MyIdentifier] autorelease];
} else {
UIView *oldViewToAnimate = [cell.contentView viewWithTag:1];
[oldViewToAnimate removeFromSuperview];
}
UIView *viewToAnimate = [self viewToAnimateForIndexPath:indexPath];
viewToAnimate.tag = 1;
[cell.contentView addSubview:viewToAnimate];
return cell;
}
viewToAnimateForIndexPath will need to:
Check to see if a viewToAnimate has been created for this indexPath
Create a viewToAnimate if there isn't one
Save a reference to the view that can be looked up by indexPath
Return the viewToAnimate so the table cell can use it
I don't know enough about your data structure to do this for you. Once the download process completes it can call this same method to get the view and animate it.
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.