views:

130

answers:

4

Let's say I have a property in my view controller, defined as follows:

@property (nonatomic, retain) UIImageView *checkmarkOffAccessoryView;

I @synthesize this in the implementation, release it in -dealloc and initialize it in -viewDidLoad as follows:

self.checkmarkOffAccessoryView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"checkmarkOff.png"]] autorelease];

So far so good.

When I use it in my table view delegate as an accessory view for multiple cells, two things happen:

  1. Only one cell's accessory view shows the image
  2. The application UI freezes.

The app doesn't crash, as near as I can tell, the UI simply becomes unresponsive. This is both in the simulator and on the device.

Here is how I use the initialized property with my cell:

- (UITableViewCell *) tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    // initialize or dequeue cell...

    if (condition)
        cell.accessoryView = self.checkmarkOffAccessoryView;
    else
        cell.accessoryView = nil;
}

With the aforementioned code, only one cell shows the accessory view and the UI freezes.

If I initialize the UIImageView instance directly in the delegate method I get all condition-satisfying cells showing the accessory view and I do not experience the UI freeze:

- (UITableViewCell *) tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    // initialize or dequeue cell...

    if (condition)
        cell.accessoryView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"checkmarkOff.png"]] autorelease];
    else
        cell.accessoryView = nil;
}

My goal is to initialize as few objects as possible and reuse one UIImageView. I'm curious why the first chunk of code is problematic and what I could do to fix this.

It seems like the cell's accessoryView property should just increment the retain count of self.checkmarkOffAccessoryView but it appears I am missing some detail.

What have I overlooked? Thanks for your advice.

EDIT

I think that:

self.checkmarkOffAccessoryView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"checkmarkOff.png"]] autorelease];

is the same as:

UIImageView *uncheckedView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"checkmarkOff.png"]];
self.checkmarkOffAccessoryView = uncheckedView;
[uncheckedView release];

Either way, I experience the same freeze symptom.

A: 

Try it without the autorelease in the initializer. I suspect you're over-releasing.

By the way, your console probably is showing a BAD_ACCESS error when it freezes. If you turn on NSZombieEnabled, my guess is you'll see it's making a call to a deallocated UIImage.

Dan Ray
he's calling initWithImage, if he doesn't release it via autorelease (or explicitly) he'd be leaking the object... (unless I'm missing something).
MarkPowell
There's no exception thrown. I'm fairly sure I'm not over-releasing.
Alex Reynolds
I say that because you're assigning it to a property synthesized with `retain` setter semantics, and then you're manually releasing it at `-dealloc` time. Seems to me like at best, the autorelease is unnecessary and at worst, it would cause occasional access of a null pointer. If I were you, just for fun, I'd take the autorelease off and see if it clears things up. Might not, but it's where I'd attack it first.
Dan Ray
@Dan Ray: Please see the edit where I show an alternative instantiation that follows the same memory management rules.
Alex Reynolds
A: 

maybe this will help

- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    static NSString *CellIdentifier = @"ShoppingListCell";

    HSShoppingListCell *cell = (HSShoppingListCell *)[aTableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        [[NSBundle mainBundle] loadNibNamed:@"ShoppingListCell" 
                                                                            owner:self 
                                                                        options:nil];
        cell = shoppingListCell;
    }

    ShoppingListItem *theItem = nil;
    theItem = [self.fetchedResultsController objectAtIndexPath:indexPath];

    UIImage *selected         = [UIImage imageNamed:@"listBullet_checked.png"];
    UIImage *notSelected    = [UIImage imageNamed:@"listBullet.png"];

    cell.imageView.image = ([theItem.checkedOff boolValue] ? selected : notSelected); 

    cell.shoppingListLabel.text = theItem.productName;
    [cell.shoppingListLabel setFont:[UIFont fontWithName:@"Marker Felt" size:26.0]];
    return cell;
}

- (void)toggleCellImage:(NSIndexPath *)indexPath
{
    ShoppingListItem *item  = [self.fetchedResultsController objectAtIndexPath:indexPath];

    item.checkedOff = ([item.checkedOff boolValue] ? [NSNumber numberWithBool:NO] : [NSNumber numberWithBool:YES]);

    [HSCoreDataUtilities saveContext:item.managedObjectContext];
    [self.tableView reloadData];
}

#pragma mark -
#pragma mark Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 
{
    [self toggleCellImage:indexPath];
    [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
iAm
+5  A: 

You cannot add the same view multiple times. The UI handler will go bonkers. To make sure of this, I tried doing what you said above and I got the same issue. The UI freezes up, the image only appears for one of the cells.

The best thing you can do is to store your image as a UIImage allocated, and to have a helper function which returns a new UIImageView per cell.

Using your current method (without a stored UIImage) you might do:

-(UIImageView *) makeCheckmarkOffAccessoryView
{
    return [[[UIImageView alloc] initWithImage:
        [UIImage imageNamed:@"checkmarkOff.png"]] autorelease];
}

And then do

cell.accessoryView = [self makeCheckmarkOffAccessoryView];

As you may be aware, UIImages on the other hand may be used any number of times. a UIImageView doesn't take up a lot of space, so you can easily have a bunch of those without worrying.

To expand on the one place only deal, imagine that you add a UIView to two places at the same time.

What will [ob removeFromSuperview] do for this object? Will it remove the view from both places? From one of them only? Which value will be returned when you request [ob superview]? Clearly the UI is not made to handle what you're asking for.

Kalle
A: 

Reducing your case to the bare essentials (I was going to suggest to put two 'thin' UIView objects around the UIImageView...), I found that it is most probably impossible.

Create 2 empty UIView objects in IB, hook them up to bareView1 and bareView2. Then

UIImageView *imageView = [[UIImageView alloc]
                      initWithImage:[UIImage imageNamed:@"test.png"]];
[bareView1 addSubview:imageView]; // it shows either here ...
[bareView2 addSubview:imageView]; // ... or here

You can never get the image on sceen more than once like this. As a rule of thumb, I think the first object in line which does not inherit from UIView can be used multiple times, i.e. the UIImage. Like Kalle stated, a UIView can only have one parent in the view hierarchy.

Postponing the second addSubview only makes the UIImageView jump from bareView1 to bareView2.

The freeze happens maybe because the event handling gets mixed up: the accessory can be interactive, how would you know which one was tapped if they are one and the same object? So the code assumes objects are unique, and you manage to violate that assumption.

mvds