UITableViewCell showing UIImageView in some cells and different cells when scrolling - in use with CoreData and NSFetchedResultsController

StackOverflow https://stackoverflow.com/questions/23362121

Question

I have a simple application whereby a UITableView is populated by a user tapping on an item in the UINavigationBar and then getting taken to another view to fill in UITextFields and UITextView. When the user clicks save, the information is saved to Core Data and then represented in this UITableView with the use of NSFetchedResultsController.

One of the attributes is a "Notes" attribute on a "Transaction" Entity. Filling in the Notes in the UITextView is completely optional, but if the user adds in a note, I want to show a custom image that I've created on the cell for the entry that has the note.

When the app is run in this version alone (so deleted and installed with this developer release), it works very well and the cells show the notes only for the cells that have the notes. However, when updating from a previous version of the app, this is where the problem occurs.

The previous version of the app didn't have a Notes attribute and so I used CoreData lightweight migration to set up a new model with a Notes attribute. That part works.

The Problem

Because of the reusing of cells, I'm noticing that when I've updated from an old version to this new version, none of the cells have the custom image and that's good because the notes doesn't exist. However, if I go in and add a new entry with a note and then scroll through my UITableView, I notice the cells start showing the custom image randomly, based on scrolling. So it disappears from one cell and shows up on another. This is a big mis-representation for my users and I'm not quite sure what to do to fix this.

In my cellForRow I have the following code:

self.pin = [[UIImageView alloc] initWithFrame:CGRectMake(13, 30, 24, 25)];
self.pin.image = [UIImage imageNamed:@"pin"];
self.pin.tag = 777;
if (!transaction.notes) {
    dispatch_async (dispatch_get_main_queue (), ^{
        [[customCell viewWithTag:777] removeFromSuperview];
    });
}


if ([transaction.notes isEqualToString:@""] || ([transaction.notes isEqualToString:@" "] || ([transaction.notes isEqualToString:@"  "] || ([transaction.notes isEqualToString:@"   "]))))
{
    [[customCell viewWithTag:777] removeFromSuperview];
}
else
{
    [customCell addSubview:self.pin];        
}

So the first if statement is to check whether the notes exist and that returns true when updating from an old version of an app to this version. The second if statement just checks if the value of the notes is equal to a few spaces and if so, then to remove the note.

I just can't figure out what's going on here.

Possible Solution

In the same UITableView cell, I also have a green/red dot UIImageView which is displayed depending on whether the user selected a Given or Received Status when adding a new entry. With this in mind, one image is ALWAYS displayed, whether it's the green or red dot. So what I'm thinking about here is creating a transparent square and just changing the if statement to say "If note, show pin image and if not, show transparent image".

That feels a bit like a hack though and I'd prefer a proper way to fix this.

Any thoughts on this would really be appreciated.

Was it helpful?

Solution

First of all, bad practice to allocate views in cellForRow. If you really need to allocate views in cellForRow do it just when it's needed, in your case in the else statement.

Second, do not use dispatch_async to dispatch on main thread if you are already on main thread (cellForRow it's on main thread).

The above points are just some suggestions for performance improvement.

As a solution of your problem, I would create a custom UITableViewCell and in it's method prepareForReuse I would remove the imageView.

EDIT

YourCustomCell.m

- (void)prepareForReuse {
   [super prepareForReuse];
   [[self viewWithTag:YOUR_TAG] removeFromSuperview];
}

This is a straightforward implementation, but you have to take in consideration that is more expensive to alloc/dealloc the UIImageView than keep a reference to the image and hide it when you don't need it. Something like:

YourCustomCell.h

@interface YourCustomCell : UITableViewCell {
  IBOutlet UIImageView *theImageView; // make sure you link the outlet properly ;)
}

YourCustomCell.m

 - (void)prepareForReuse {
       [super prepareForReuse];
       theImageView.hidden = YES;
    }

And in cellForRow you just have to check if you have notes and make the imageView visible (probably you will make theImageView a property)

OTHER TIPS

Because table view cells are reused you must have a default value for your image. So for example set your image to transparent by default and change it under some condition. This will stop your image being shown in reused cells.

Why do you have a dispatch_async here?

if (!transaction.notes) {
    dispatch_async (dispatch_get_main_queue (), ^{
        [[customCell viewWithTag:777] removeFromSuperview];
    });
}

Because you cannot be sure when the function inside it will execute. Suppose that transaction.notes is nil. All the isEqualToString functions will return false and the else condition of addSubView will be called. But sometime after this function is exited the code inside dispatch_async will be run and remove the pin view. I'm not whether this is the intended behavior.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top