UIImageView Animation Stopping in UITableView - iphone

I have a UIImageView inside of a UITableViewCell. The UIImageView animates. For some odd reason, when the cell goes out of view, the animation stops. The UIImageView is never initialized again and the UIImageView is never explicitly told to - (void)stopAnimating; so I'm not sure why it's stopping.
Here's the interesting parts of my cellforRowAtIndexPath:. As you can
cell = (BOAudioCell *)[tableView dequeueReusableCellWithIdentifier:AudioCellIdentifier];
if (!cell) {
cell = [[[BOAudioCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:AudioCellIdentifier] autorelease];
BOPlayControl *playControl = [(BOAudioCell *)cell playControl];
[[playControl playbackButton] addTarget:self action:#selector(playbackButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
}
int index = [indexPath row];
BOModel *model = [models objectAtIndex:index];
[[(BOAudioCell *)cell titleLabel] setText:[model title]];
[[(BOAudioCell *)cell favoriteButton] addTarget:self action:#selector(favoriteButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
As you can tell, I'm not setting the appropriate animation begin point for the cell because there is not a built in way with a UIImageView.

You should have exposed the UIImageView on your cell and started the animation in the cellForRowAtIndexPath method or do an override of the prepareForReuse method of the UITableViewCell and started the animation in that method.
prepareForReuse is called every time your reusable cell is dequeued from the reusable queue. Just to note, if you were to use this, do not forget to invoke the superclass implementation.

When the cell goes off-screen, it will be released, and the subview (the image view) will be released as well, causing the animation to be stopped.
Keep a -retain-ed instance of that image view somewhere to avoid this.

I wrote a workaround. I'm just using a NSTimer to coordinate manually animating the UIImageView. It works great.

Related

Accessing -willDisplayCell method of moreNavigationController's UITableView

After setting the backgroundView of UITableView in moreNavigationController like so...
UITableView *moreTableView = (UITableView *)tabBarController.moreNavigationController.topViewController.view;
[moreTableView setBackgroundView:[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"bg.png"]]];
...the cells of that UITableView changed their backgroundColor from white to clear (transparent). I've tried setting it back to white with the following lines of code...
for (UITableViewCell *cell in [moreTableView visibleCells]) {
[cell setBackgroundColor:[UIColor whiteColor]];
}
...but unfortunately this does not work, so I guess the only solution would be to set that backgroundColor in the -willDisplayCell: method. Can anyone tell me how to access that method? Any help would be greatly appreciated.
If you have a custom cell then the back ground colour of the cell can be set in a subclass of UITabeViewCell, otherwise you should try to add this logic to the tableView:cellForRowAtIndexPath: method of the UITableView's datasource.
What you are doing in your loop will only effect the visible cells, since they will be reused and if the background is being set to clear dynamically then you'll always have your background overridden.

UITableViewCell from contentView subview

I have created the cells with labels and using checkaMarksAccessory. The few last cells have UITextFields which can user modifi, and those have selector on UIControlEventEditingDidEnd where i want change the state of the cell to checked.
How can i get the cell in the selector? Doesn't have the object some parentView?
The way i inserting the object to cell.
UITextField *textfield = [[UITextField alloc] initWithFrame:CGRectMake(10, 25, 200, 30)];
[textfield setBorderStyle:UITextBorderStyleRoundedRect];
[textfield addTarget:self action:#selector(vybavaDidFinishEdit:) forControlEvents:UIControlEventEditingDidEnd];
[cell.contentView addSubview:textfield];
I'm not sure if it's safe to assume cell.contentView.superview == cell. Might Apple change this? I doubt it. But, I don't see anywhere in the documentation that says a cell's content view is a direct subview of the cell.
If you've added a UIGestureRecognizer to one of your subviews of the cell's content view, then you can get a reference to the cell with:
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:[gestureRecognizer locationInView:self.tableView]];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
Table View Animations and Gestures sample code uses indexPathForRowAtPoint: this way.
If you must traverse superviews, I think using a function like the one below is a bit safer.
UITableViewCell *ACMContentViewGetCell(UIView *view)
{
while ((view = view.superview)) {
if ([view isKindOfClass:[UITableViewCell class]]) {
return (UITableViewCell *)view;
}
}
return nil;
}
But that function still assumes contentView is within its cell, which I also didn't see anywhere in the documentation.
So perhaps, the best solution is to rearchitect your code so that you don't need to get cell from contentView, or if you must, then add an instance variable from the subview of contentView to cell.
ok so the way is to use superview. The superview is component which own the object. If i want get the UITableViewCell from UITextField i used [[UITextField superview] superview].

Removing target-actions for UIButton

I have a UIButton that is added to each UITableViewCell (except for 2 cells) in a tableview.
The button's target is the UITableViewController.
I noticed that the app has crashed when the action has been sent to the wrong target. I'm assuming that this is because the target has somehow been deallocated (even though, if the UITableViewController has been deallocated, the buttons should not be visible, and not pressable (and should be deallocated themselves)).
I'm guessing I need to balance the addTarget method, with the removeTarget. Like KVO and retain/release.
But I'm not sure where to do this, because I only have a reference to the button when it is being created and added to the cells, in cellForRowAtIndexPath:?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = (UITableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
UIButton *extraButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[extraButton setFrame:CGRectMake(0, 0, 60, 30)];
[extraButton setTitle:#"Meta" forState:UIControlStateNormal];
[extraButton addTarget:self action:#selector(extraButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
cell.accessoryView = extraButton;
}
if (indexPath.row == kNoExtraButtonRow) {
cell.accessoryView.hidden = YES;
} else {
cell.accessoryView.hidden = NO;
}
//set textlabels etc...
return cell;
}
It is very unusual to need to do this. What is more likely is that you're incorrectly managing memory some other way. In particular, the target of a UIButton should generally be the UIViewController that owns that button. In most good designs, the button always has a shorter lifespan than the controller. Are you retaining the UIButton elsewhere? Are you using a nib file to manage your button, or generating it programmatically? It is somewhat common for people to accidentally create multiple instances of UI elements when they create them programmatically (one of several reasons that nib files are preferred).
Are you sure to use accessors for all your ivars (particularly the button in this case)? Direct access to ivars is the most common way developers create duplicate UI elements. Always use accessors (except in init and dealloc).
I think it will help
[someControl removeTarget:nil
action:NULL
forControlEvents:UIControlEventAllEvents];
If you have subclassed UIButton for this or have subclassed UITableViewCell then you can put the code in there to remove the target action for the button when the cell or button is deallocated. In the UITableViewCell's dealloc you can call removeTarget on the its button, or in the button's dealloc it could call removeTarget for itself.
You do not need to removeTarget and please make sure that you are setting Target as self. Your code works fine here for me.
It's assumed that the table view cells enter the reuse queue and come back off of it untouched, but as a sanity check, maybe what you want to do is in the case where ( cell != nil ) coming off the dequeue, you want to removeTarget and re-establish the target anyway.
Basically do your own inline -prepareForReuse in the ( cell != nil ) case and set the whole thing up (except for the actual table cell alloc) in every case.
[extraButton addTarget:nil action:#selector(extraButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
Try This.
if (cell == nil){
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]retain];
UIButton *extraButton = [[UIButton buttonWithType:UIButtonTypeRoundedRect]retain];
[extraButton setFrame:CGRectMake(0, 0, 60, 30)];
[extraButton setTitle:#"Meta" forState:UIControlStateNormal];
[extraButton addTarget:self action:#selector(extraButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
cell.accessoryView = extraButton;
}
remove autorelease and add retain in Tableviewcell allocation and in buton allocation.....

How to set the table view cell accessory view to retain a previously initialized UIImageView?

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:
Only one cell's accessory view shows the image
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.
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.
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.
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];
}
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.

What is a good way to animate UIViews inside UITableViewCell after a callback?

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.