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.
Related
hello friends I have created custom table and add all needed delegate and datasource for table then this is working good but when we scroll my table then content change like this is code...
Table_worklist=[[UITableView alloc]initWithFrame:CGRectMake(10,480,750,130)style:UITableViewStylePlain];
Table_worklist.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
Table_worklist.delegate=self;
Table_worklist.dataSource=self;
Table_worklist.layer.borderWidth = 2.0;
Table_worklist.layer.borderColor = [UIColor grayColor].CGColor;
[ScrollView addSubview:Table_worklist];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell== nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] ;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
UITextField *resultval=[[UITextField alloc]initWithFrame:CGRectMake(510,10,120,30)];
resultval.tag=1;
resultval.font = [UIFont boldSystemFontOfSize:12.0];
//the horizontal alignment of the text
resultval.textAlignment = NSTextAlignmentCenter;
resultval.contentVerticalAlignment = UIControlContentHorizontalAlignmentCenter;
resultval.clearButtonMode = UITextFieldViewModeWhileEditing; // has a clear 'x' button to the right
resultval.delegate =self;
if([[NSString stringWithFormat:#"%#",[[TestSample objectAtIndex:indexPath.row]valueForKey:#"ResultType"]]isEqualToString:#"Numerical"])
{
imageLayer = field.layer;
[imageLayer setCornerRadius:02];
[imageLayer setBorderWidth:1];
imageLayer.borderColor=[[UIColor lightGrayColor]CGColor];
}
else if([[NSString stringWithFormat:#"%#",[[TestSample objectAtIndex:indexPath.row]valueForKey:#"ResultType"]]isEqualToString:#"Words"]||[[NSString stringWithFormat:#"%#",[[TestSample objectAtIndex:indexPath.row]valueForKey:#"ResultType"]]isEqualToString:#"Comment"])
{
imageLayer = field.layer;
[imageLayer setCornerRadius:06];
[imageLayer setBorderWidth:1];
imageLayer.borderColor=[[UIColor blackColor]CGColor];
UIButton *CheckMark=[[UIButton alloc]initWithFrame:CGRectMake(540,10,30,30)];
CheckMark.tag=1;
[CheckMark setImage:[UIImage imageNamed:#"DownTriangle1"]forState:UIControlStateNormal];
imageLayer = CheckMark.layer;
[imageLayer setCornerRadius:02];
[imageLayer setBorderWidth:1];
imageLayer.borderColor=[[UIColor blackColor] CGColor];
[cell.contentView addSubview:CheckMark];
}
[cell.contentView addSubview:resultval];
}
return cell;
}
this show right data for first 3or4 cell(because table height 130 so show only 3 cell at a time )for these 3 cell my table show right data ...for example
if([[NSString stringWithFormat:#"%#",[[TestSample objectAtIndex:indexPath.row]valueForKey:#"ResultType"]]isEqualToString:#"Numerical"])
{ }
then data is numerical go this if condition other wise go else condition......
but when we have for more cell like as 7-8 cell then when we scroll these cell so what happened suppose at cell-4 my controller go else if condition forComment and word data .........so at last cell -8 got numerical data so controller go in if condition but check mark button which was created in if else condition which show on my if condition data ......so i don't know what happened
when we scroll how to change my condition ......
Actually i thing when last created else if condition (which create uibutton checkmark)on cell-4 which are apply on cell-8...........but controller go in right place in if condition at cell-8 .....but last time created checkmark button generated on cell-8 ......
how to solve this problem....
Before You follow my Answer i want to tell you that following code is bad for memory management because it will create new cell for each rows of UITableView, so be careful for it.
But it is better to use, When UITableView Have Limited rows (about 50-100 may be ) then following code is helpful in your case, use it if is it suitable for you.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *CellIdentifier = [NSString stringWithFormat:#"S%1dR%1d",indexPath.section,indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
/// Put your code here
}
return cell;
}
If you have limited rows then this is best code for you.
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
When I scroll down and up again my text in tableView will disappear.
And my code is:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [screenDefBuild.elementsToTableView count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier];
}
ScreenListElements *currentScreenElement = [screenDefBuild.elementsToTableView objectAtIndex:indexPath.row];
cell.textLabel.text = currentScreenElement.objectName;
currentRow++;
return cell;
}
- (void)viewDidLoad
{
[super viewDidLoad];
tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
[tableView setDataSource:self];
[self.view addSubview:tableView];
}
I also want to fill my table view to entire screen. (grey strap on the top).
I don't know what you're doing with this variable
currentRow++;
But whatever you use that for, i'd wager its breaking your code.
the UITableView will call cellForRowAtIndexPath every time a cell is about to appear on screen regardless of whether it has been on screen before or not. When you scroll down and then scroll back up this variable will have increased beyond the bounds of your data, hence you get empty cells.
You need to design this method in such a way that it can create any cell in the table view at any time. You can't rely on the order that the cells will be made and with scrolling you will have to make the same cell over and over again. Only use indexPath to figure out which cell you are currently supposed to be making.
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UITableView_Class/Reference/Reference.html
Answer for the second part of the question- grey strap. You are adding the table view to current view, so you should use the size property of self.view.frame but not the origin. You want this to be set to 0,0.
Change
tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
to
CGRect viewFrame=self.view.frame;
viewFrame.origin=CGPointZero;
tableView = [[UITableView alloc] initWithFrame:viewFrame];
As for the first part of your question- it's strange, as you seem to do everything properly. One thing i may suggest is to add [tableView reloadData]; in the end of viewDidLoad.
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. :)
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...
}