I have a problem with cellForRowAtIndexPath always returning nil when called from tableView:cellForRowAtIndexPath:
The thing is I am actually calling cellForRowAtIndexPath inside a block inside tableView:cellForRowAtIndexPath: and I guess that this might be the problem.
I am populating cells with remote images that load (and cache) ad hoc. When the image returns fully loaded in the completion handler block (inside tableView:cellForRowAtIndexPath:) I need to get the cell again because it might have scrolled out of view... So I call cellForRowAtIndexPath to get the cell again - cellForRowAtIndexPath would only return a cell if it's visible. In my case I never get it to return anything but nil. Even though I am scrolling very slowly (or fast, or whatever speed I tried...) all I get is nil.
I'll try to get some code in here soon but until then any suggestion why this would occur - I guess the block thing would be a hint.
Here is the code: https://dl.dropboxusercontent.com/u/147970342/stackoverflow/appsappsappsappsapps.zip
The -tableView:cellForRowAtIndexPath: implementation:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber* numberAppleid = self.appids[indexPath.row];
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"Mycell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:#"Appleid: %#", numberAppleid];
NSString* appleidasstring = [NSString stringWithFormat:#"%#", numberAppleid];
cell.imageView.hidden = YES; // Hide any previous until image loads
[self getAppIconForAppleid:appleidasstring handlerImage:^(UIImage *image) {
if (image == nil) {
return;
}
DLog(#"cellForRowAtIndexPath tableView: %#, indexPath: %#", tableView, indexPath);
UITableViewCell* localcell = [tableView cellForRowAtIndexPath:indexPath];
if (localcell != nil) {
localcell.imageView.image = image;
localcell.imageView.hidden = NO;
}
else {
DLog(#"Nah indexpath is not visible for: %# because localcell is nil.", appleidasstring);
}
}];
return cell;
}
Fixed version:
#define KEYAPPLEID #"keyappleid"
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber* numberAppleid = self.appids[indexPath.row];
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"Mycell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:#"Appleid: %#", numberAppleid];
NSString* appleidasstring = [NSString stringWithFormat:#"%#", numberAppleid];
[cell setAssociativeObject:appleidasstring forKey:KEYAPPLEID];
cell.imageView.hidden = YES; // Hide any previous until image loads
[self getAppIconForAppleid:appleidasstring handlerImage:^(UIImage *image) {
if (image == nil) {
return;
}
NSString* currentappleidofcell = [cell associativeObjectForKey:KEYAPPLEID];
if ([currentappleidofcell isEqualToString:appleidasstring]) {
cell.imageView.image = image;
cell.imageView.hidden = NO;
}
else {
DLog(#"Returning appleid: %# is not matching current appleid: %#", appleidasstring, currentappleidofcell);
}
}];
return cell;
}
And this category is needed: https://stackoverflow.com/a/10319083/129202
If the block is within the tableView:cellForRowAtIndexPath: callback, you can just reference the cell directly in the block. The block will automatically keep the cell around until the block goes away. However, be careful of retain cycles. If the block is owned by the cell, you will have to use a weak reference to the cell.
So your implementation would look like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSNumber* numberAppleid = self.appids[indexPath.row];
// Create your own UITableViewCell subclass that has an "appleidasstring" property
MyTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"Mycell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:#"Appleid: %#", numberAppleid];
NSString* appleidasstring = [NSString stringWithFormat:#"%#", numberAppleid];
cell.imageView.hidden = YES; // Hide any previous until image loads
// Set the appleidasstring on the cell to be checked later
cell. getAppIconForAppleid:appleidasstring = appleidasstring;
// Modify the handler callback to also callback with the appleidasstring
[self
getAppIconForAppleid:appleidasstring
handlerImage:^(UIImage *image, NSString *imageAppleIdaString)
{
if (image == nil) {
return;
}
// Ensure the cell hasn't been repurposed for a
// different imageAppleIdaString
if ([cell.appleidasstring isEqualToString:imageAppleIdaString]) {
cell.imageView.image = image;
cell.imageView.hidden = NO;
}
}
];
return cell;
}
Related
My Custom tableview cell content getting empty after scrolling.So pls help me with this.
My custom cell has 8 buttons and a label.First I'm showing only label that has title and on selection I'm expanding the cell and showing all buttons.So, when I select few buttons and I do scrolling,buttons that I selected getting refreshed or get back to normal state.Here is the code.Pls help me with this
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"newFilterCell";
newFilterCell *cell = (newFilterCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"newFilterCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
[cell.contentView.superview setClipsToBounds:YES];
}
cell.QuestionLabel.text=[orderArray objectAtIndex:indexPath.row];
NSArray * arr = [filterInfo objectForKey:[orderArray objectAtIndex:indexPath.row]];
int val = 0;
NSLog(#"%#",cell.subviews);
NSArray * cellViews = cell.contentView.subviews;
if (arr.count>0)
{
for (int i=1; i<=8; i++) {
if (i<=arr.count) {
UIButton * target = (UIButton*)[cell viewWithTag:i];;
[target setTitle:[arr objectAtIndex:i-1]forState:UIControlStateNormal];
[target addTarget:self action:#selector(selectButton:) forControlEvents:UIControlEventTouchUpInside];
}
else
{
[[cell viewWithTag:i] setHidden:YES];
}
}
}
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
return cell;
}
On selection of my cell I'm expanding and reloading the tableview cells.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath==_expandIndexPath) {
_expandIndexPath = nil;
NSMutableArray *modifiedRows = [NSMutableArray array];
[tableView reloadRowsAtIndexPaths:modifiedRows withRowAnimation:UITableViewRowAnimationLeft];
}
else
{
selectedRow=indexPath.row;
NSMutableArray *modifiedRows = [NSMutableArray array];
[tableView deselectRowAtIndexPath:indexPath animated:TRUE];
_expandIndexPath = indexPath;
[modifiedRows addObject:indexPath];
[tableView reloadRowsAtIndexPaths:modifiedRows withRowAnimation:UITableViewRowAnimationLeft];
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSArray * arr = [filterInfo objectForKey:[orderArray objectAtIndex:indexPath.row]];
if ([indexPath isEqual:_expandIndexPath])
{
if ([[orderArray objectAtIndex:indexPath.row]isEqualToString:#"Height"]||[[orderArray objectAtIndex:indexPath.row]isEqualToString:#"Age"])
{
return 275;
}
else
{
if(arr.count==3)
return 55*arr.count;
else
return 37*arr.count;
}
}
else
return 70;
}
UITableViewCells are getting recycled. That's why its not safe to do it your way. Your data model needs to remember your buttons and other stuff that changed. You need to apply the changes every time the cell gets created.
You need to check in the cellForRowAtIndexPath what button is pressed and then show the view correctly.
You need to remember what happend in the cells with an external data source to apply the changes you want.
In your cellForRowAtIndexPath should be something like a check for a boolean whether or not you show some stuff:
if(button_is_pressed_for_row_5 == true){
button_in_cell.hidden = true;
}else{
button_in_cell.hidden = true;
}
I am trying to keep the selected state of multiple cells on a didSelectRowAtIndexPath method. I have an edit button that I've set up that loops through every cell to select each field on my UITableView.
Here is the code for the edit button on tap that selects all my rows.
- (IBAction)editButtonTapped:(id)sender {
for (int i = 0; i < self.caseDataTableView.numberOfSections; i++) {
for (NSInteger r = 0; r < [self.caseDataTableView numberOfRowsInSection:i]; r++) {
[self tableView:caseDataTableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:r inSection:i]];
}
}
}
When calling the didSelectRowAtIndexPath method, it does the following code.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
OKOperatieNoteTableViewCell *cell = (OKOperatieNoteTableViewCell *)[self.caseDataTableView cellForRowAtIndexPath:indexPath];
cell.cellIndexPath = indexPath;
[cell hideLabelAndShowButtons];}
Incase you were wondering here is the hideLabelAndShowButtons method.
- (void)hideLabelAndShowButtons {
self.caseDataKeyLabel.hidden = NO;
if (!self.disabled) {
self.caseDataValueLabel.hidden = YES;
self.textField.hidden = NO;
if ([self.inputType isEqualToString:#"switcher"] || [self.inputType isEqualToString:#"multiselect"] || [self.inputType isEqualToString:#"picker"] || [self.inputType isEqualToString:#"DatePicker"] || [self.inputType isEqualToString:#"selectContact"]) {
self.button.hidden = NO;
}else {
self.button.hidden = YES;
}
}
self.caseDataDescriptionTextView.hidden = YES;}
Now at this point, I have all my rows selected. If I scroll down and then back up the selection of these rows is not there anymore. Now I'm aware when you go in and out of the view, the cellForRowAtIndexPath method recreates these cells. The following is my cellForRowAtIndexPath method.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"caseData";
OKOperatieNoteTableViewCell * cell = [[OKOperatieNoteTableViewCell alloc]init];
cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if (indexPath.row < _procedureVariables.count) {
if ([[[_caseDataArray objectAtIndex:indexPath.row] valueForKey:#"key"] isEqualToString:#"Procedure"]) {
[cell setLabelsWithKey:[[_caseDataArray objectAtIndex:indexPath.row] valueForKey:#"key"] AndValue:[self.model valueForKey:#"var_procedureName"]];
}else {
[cell setLabelsWithKey:[[_caseDataArray objectAtIndex:indexPath.row] valueForKey:#"key"] AndValue:[[_caseDataArray objectAtIndex:indexPath.row] valueForKey:#"value"]];
}
OKProcedureTemplateVariablesModel *variableModel = _procedureVariables[indexPath.row];
cell.variable = variableModel.value;
[cell showLabelAndHideButtons];
cell.delegate = self;
[cell setUpCellType];
} else if (indexPath.row == _procedureVariables.count) {
NSString *text = [NSString stringWithFormat:#"%# \n\n %#", [_templateDictionary objectForKey:#"indicationText"], [_templateDictionary objectForKey:#"procedureText"] ];
[cell showDescription:text];
NSLog(#"cell.caseDataDescriptionTextView.font.fontName = %#", cell.caseDataDescriptionTextView.font.fontName);
}
cell.procedureID = _procedureID;
[tableView setContentInset:UIEdgeInsetsMake(1.0, 0.0, 0.0, 0.0)];
return cell;
}
I'm just trying to figure out how to keep the selected state of these cells once the cellForRowAtIndexPath method is called. Any suggestions are welcomed.
i tried to simulate your situation, created a customCell and saved the indexpaths of selectedRows in my custom selectedPaths mutable array(initialized in viewDidLoad).
After every click i removed or added related indexpath to my array.
it worked for my case. Hope it helps.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"caseData";
NOTableViewCell *cell = (NOTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
NSLog(#"new cell created for row %d", (int)indexPath.row);
cell = [[[NSBundle mainBundle] loadNibNamed:#"NOTableViewCell" owner:self options:nil] objectAtIndex:0];
}
if ([selectedPaths indexOfObject:indexPath] != NSNotFound) // this cell is in selected state.
{
[cell.textLabel setText:#"This cell selected"];//selected state job.
return cell;
}
[cell.textLabel setText:[NSString stringWithFormat:#"%d", (int)indexPath.row]];
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([selectedPaths indexOfObject:indexPath] != NSNotFound) {
[selectedPaths removeObject:indexPath];
}
else{
[selectedPaths addObject:indexPath];
}
//[tableView reloadData];
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];//instead of reloading all just reload clicked cell.
}
You need to update the cell to selected and not selected explicitly in both directions in cellForRowAtIndexPath.
If not, the recycled cells will just show the value of the cell the cell was last used for until you change it.
While you are invoking the delegate method in order to call hideLabelAndShowButtons, you aren't telling the table view that you have selected the row;
- (IBAction)editButtonTapped:(id)sender {
for (int i = 0; i < self.caseDataTableView.numberOfSections; i++) {
for (NSInteger r = 0; r < [self.caseDataTableView numberOfRowsInSection:i]; r++) {
NSIndexPath *path=[NSIndexPath indexPathForRow:r inSection:i];
[caseDataTableView selectRowAtIndexPath:path animated:NO scrollPosition:UITableViewScrollPositionNone];
[self tableView:caseDataTableView didSelectRowAtIndexPath:path];
}
}
}
Also, you aren't using the cell selection state in cellForRowAtIndexPath, so you probably need to change some code there too, but I am not sure what the relationship is between selected state and how you want to render the cell.
I realize this is a common problem but I'm just not getting it. I have a button that when pressed on a cell, changes to a check mark. However, when pressed it changes other reused cell's image to a check mark as well. I'm using a cell's delegate to manipulate local data on the tableView's VC.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Configure the cell...
static NSString *ReusableIdentifier = #"Cell";
SetListTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReusableIdentifier forIndexPath:indexPath];
cell.delegate = self;
if ([self.selectedRows containsIndex:indexPath.row]) {
[cell.plusButton setBackgroundImage:checkImage forState:UIControlStateNormal];
}
else {
[cell.plusButton setBackgroundImage:plusImage forState:UIControlStateNormal];
}
cell.tag = indexPath.Row
return cell;
}
Method in custom cell's class.
- (IBAction)addSongButtonPressed:(UIButton *)sender
{
[self.delegate addSongButtonPressedOnCell:self];
UIImage *checkImage = [UIImage imageNamed:#"check.png"];
[self.plusButton setBackgroundImage:checkImage forState:UIControlStateNormal];
}
Method from the cell's delegate.
-(void)addSongButtonPressedOnCell:(id)sender
{
NSInteger index = ((UIButton *)sender).tag;
NSMutableDictionary *track = [self.searchTracks objectAtIndex:indexPath];
[self.selectedRows addIndex:indexPath];
}
I load a expanded cell XIB during didSelectRowAtIndexPath in my tableView.Now when every cell expands I call a webservice.Depending on the response of it, I load a childViewController in the expanded cell .Now the problem is that the data is visible in certain cells and it isn't in other cells? I am not entirely sure whether dequeueReusableCellWithIdentifier i e re-using of cells is causing such a problem.But if that is , How can I solve the problem?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"Cell";
static NSString *expandedCellIdentifier = #"ExpandedCell";
if (!isExpanded) {
ListCell *cell =(ListCell*) [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell==nil) {
NSArray *nibs = [[NSBundle mainBundle]loadNibNamed:#"ListCell" owner:self options:nil];
cell = nibs[0];
}
cell.Name.text = [[bArray objectAtIndex:indexPath.row]valueForKey:#"opName"];
return cell;
}
else{
expCell = (ExpandedCell*)[tableView dequeueReusableCellWithIdentifier:expandedCellIdentifier];
if (expCell==nil) {
NSArray *nibs = [[NSBundle mainBundle]loadNibNamed:#"ExpandedCell" owner:self options:nil];
expCell = nibs[0];
UILabel *end = [[UILabel alloc]initWithFrame:CGRectMake(90, 24, 70, 14)];
[end setTag:102];
[expCell.background_View addSubview:end];
}
UILabel *endLabel = (UILabel *)[expCell.background_View viewWithTag:102];
endLabel.text = [NSString stringWithFormat:#"%#",endStn.capitalizedString];
return expCell;
}
return nil;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if ([self.expandedCells containsObject:indexPath]) {
[self.expandedCells removeAllObjects];
}else{
isExpanded=YES;
if (self.expandedCells.count>0) {
[self.expandedCells removeAllObjects];
}
[self.expandedCells addObject:indexPath];
//Call webservice and populate the view controller to be loaded.
[self callWebservice completionBlock:^(BOOL finished){
if (finished) {
[self initialseChildViewController:^(BOOL finished){
if (finished) {
[self populateView:^(BOOL finished){
if (finished) {
if (expCell.expContainer.hidden==YES) {
expCell.expContainer.hidden=NO;
}
}else{
NSLog(#"Data not populated");
}
}];
}else{
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"" message:#"ChildViewController not initialised" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
}
}];
}
}];
[self.busTableView beginUpdates];
[self.busTableView reloadData];
[self.busTableView endUpdates];
}
Modify your code as below. You need to return..cell in first if condition..
You are returning nil at if (!isExpanded) condition. Fix as below
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"Cell";
static NSString *expandedCellIdentifier = #"ExpandedCell";
if (!isExpanded) {
ListCell *cell =(ListCell*) [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell==nil) {
NSArray *nibs = [[NSBundle mainBundle]loadNibNamed:#"ListCell" owner:self options:nil];
cell = nibs[0];
}
cell.Name.text = [[bArray objectAtIndex:indexPath.row]valueForKey:#"opName"];
return cell;
}
else
{
expCell = (ExpandedCell*)[tableView dequeueReusableCellWithIdentifier:expandedCellIdentifier];
if (expCell==nil)
{
NSArray *nibs = [[NSBundle mainBundle]loadNibNamed:#"ExpandedCell" owner:self options:nil];
expCell = nibs[0];
}
UILabel *endLabel = (UILabel *)[expCell.background_View viewWithTag:102];
endLabel.text = [NSString stringWithFormat:#"%#",endStn.capitalizedString];
return expCell;
}
return nil;
}
Hope it helps you..
Anything you do to the cells in didSelectRow is suspect, especially things you do asynchronously. Those cells come and go while the user scrolls. The state changes you make directly to cells will show up elsewhere when the cell is reused in another indexPath.
Instead, the pattern that you should follow is: (1) user takes action, (2) action changes model, (3) model change causes the table update, (4) table update reads the model.
In this case, that means that this code...
[self callWebservice completionBlock:^(BOOL finished){
if (finished) {
[self initialseChildViewController:^(BOOL finished){
if (finished) {
[self populateView:^(BOOL finished){
...should not refer to any table cells (neither in the methods, nor in the completion blocks). The only cell-modifying code you've posted is in the next lines:
if (expCell.expContainer.hidden==YES) {
expCell.expContainer.hidden=NO;
}
...and even those must change. Whatever it is you're doing to the cell right there must instead be done in cellForRowAtIndexPath. Make a note in your model that the hidden state of the cell's expContainer at this indexPath must change (not sure if that's equiv in your project to adding to the expandedCells collection), then reload the row at this indexPath.
To restate the rule: only change cells in cellForRowAtIndexPath. Change those cells according to the model (aka the datasource array). If there's insufficient info in the model to tell you how to configure a cell, then your model is missing something... add it to your model. Then, when you want to do anything to a table cell, don't do it. Do something to your model, and then that part of the table to reload.
I'm doing the following to set the cell image if a message is unread
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MessageCell"];
Message *m = [self.messages objectAtIndex:indexPath.row];
cell.textLabel.text = m.subject;
NSString *detail = [NSString stringWithFormat:#"%# - %#", m.callbackName, m.dateSent];
cell.detailTextLabel.text = detail;
if([m.messageRead isEqualToString:#"False"])
cell.imageView.image = [UIImage imageNamed:#"new.png"];
return cell;
}
This is correctly showing an image when it's supposed to. If I scroll down however and scroll back up, they all show the image whether it's supposed to or not
Cells are reused. So you need to set each property for every condition.
if([m.messageRead isEqualToString:#"False"])
cell.imageView.image = [UIImage imageNamed:#"new.png"];
else
cell.imageView.image = nil;
Since UITableViewCells are reused, you should set cell.imageView.image to nil in case you do not need image.
if([m.messageRead isEqualToString:#"False"]) {
cell.imageView.image = [UIImage imageNamed:#"new.png"];
} else {
cell.imageView.image = nil;
}