I'm trying to animate an image in a UITableViewCell subclass. It works when the tap on the cell is about 1/2 second or more in duration. For shorter taps, the cell gets selected, but my animation doesn't run.
In my view controller, I have:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
ImageCell *cell = (ImageCell*)[tableView cellForRowAtIndexPath:indexPath];
cell.imageFlashDuration = 5.0;
cell.imageFlashNumberOfFrames = 25;
NSLog(#"Flash image...");
[cell flashImage];
[self performSelector:#selector(doSomething:) withObject:video afterDelay:5.0];
}
In my ImageCell:
-(void)flashImage {
UIImage *image = imageView.image;
if(imageView.isAnimating) {
NSLog(#"Stop animating");
[imageView stopAnimating];
}
NSMutableArray *animationArray = [NSMutableArray arrayWithCapacity:imageFlashNumberOfFrames];
for(int i=0; i<imageFlashNumberOfFrames; i++) {
[animationArray addObject:(i % 2 == 0? black60x60Image : image)];
}
imageView.animationImages = animationArray;
imageView.animationDuration = imageFlashDuration;
NSLog(#"> Start animating");
[imageView startAnimating];
}
In my log, I see
2009-07-02 22:02:55.907 MyProg[1797:20b] Flash image...
2009-07-02 22:02:55.912 MyProg[1797:20b] > Start animating
2009-07-02 22:02:59.455 MyProg[1797:20b] Flash image...
2009-07-02 22:02:59.460 MyProg[1797:20b] > Start animating
2009-07-02 22:03:02.463 MyProg[1797:20b] Flash image...
2009-07-02 22:03:02.468 MyProg[1797:20b] > Start animating
2009-07-02 22:03:05.009 MyProg[1797:20b] Flash image...
2009-07-02 22:03:05.014 MyProg[1797:20b] > Start animating
The above was from a mixture of 'short' and 'long' touches. The long touches resulted in cell selection and image animation and the short ones resulted in cell selection without animation.
Additionally, if a short tap is followed by another short tap on the cell, the animation begins.
I think the problem lies in the didSelectRowAtIndexPath method. There you call the cellForRowAtIndexPath method, which will probably NOT give back the same cell that was clicked! The cell returned probably is a new cell, not visible on screen, with an animating image-thing on it. But... It's not visible.
I assume you handle the longer clicks in a different method (not didSelectRowAtIndexPath)?
You should probably keep a reference to the cell at the point the cell gets requested by the tableview itself (or assign a tag to it or something) in the cellForRowAtIndexPath method. Then when the didSelectRowAtIndexPath method, look up that specific instance of the cell (by the reference, or by the tag, ..)
Hope this helps :) Cheers
Related
I'm trying to mimic the iMessage bubble text behaviour with an UITableView. In order to always scroll to the bottom I'm using scrollToRowAtIndexPath when viewDidLoad and viewDidAppear. This is because when the viewDidLoad method is called, the table has not been completely loaded, so I need that extra scroll in viewDidAppear. This code makes the trick. However, what I want is not an animated scroll (setting animated to NO does not solve this), I want the table to be displayed always from the bottom, not load the table and then go to the last row.
Is this possible? I can't find any solution that fits completely with the desired behaviour.
This is the best solution!
Just reverse everything!
tableView.transform = CGAffineTransformMakeRotation(-M_PI);
cell.transform = CGAffineTransformMakeRotation(M_PI);
Swift 4.0:
tableView.transform = CGAffineTransform(rotationAngle: -CGFloat.pi)
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
Be careful though, because now the headerView and footerView positions are reversed as well.
You can avoid the call from viewDidLoad because scrolling from within viewDidAppear makes that first call redundant. viewDidAppear is called every time you navigate back to the view but viewDidLoad is only called once when the view is initialized.
I would agree with earlier suggestions of hiding the scroll from the user instead of changing the way a UITableView is loading data. My suggestion would be to use the scrollToRowAtIndexPath method in the viewWillAppear method with animation set to NO. After that if you have to add a new row while the table is visible to the user, use insertRowsAtIndexPaths:withRowAnimation: to add a row at the bottom of the table view. Be sure to take care of adding the data at the end of your data model so that when the user navigates away and comes back, s/he comes back to the same layout.
Hope this helps.
edit:
Just saw your reason for not accepting the previous answers and thought I'd elaborate a little more. The solution I propose would require minimum effort, avoid calling reloadData time and again and thus avoid calling the scrollToRowAtIndexPath method again and again. You only need to make one call to scrollToRowAtIndexPath in viewWillAppear to scroll to the bottom of the table view (hiding the transition from the user when doing so) and you wouldn't need to do that again.
I do something similar in an RPN calculator I've built. I have a table view with all the numbers in it and when a number is added to the stack, everything pops up one cell. When I load the view I call:
[self.myTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:NumOfStackItems - 1 inSection:0]
atScrollPosition:UITableViewScrollPositionTop animated:NO];
In my viewWillAppear. This way my table view starts shown at the bottom of the stack and no animation is seen. By putting this in the viewWillAppear, every time I navigate to the view, it shows up at the bottom of the table.
When I add numbers to the stack, I just add it in an array that holds all the numbers and then put the text in the proper row like this:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Cell initialization here...
NSUInteger row_num = [indexPath row];
cell.rowNumber.text = [NSString stringWithFormat:#"%g", [DataArray objectAtIndex:NumberOfStackItems-row_num-1];// subtract the row number off to get the correct array index
return cell
}
I also make sure that whenever I update the tableview with a new value i first call the reloadData function, and then call the scrollToRowAtIndexPath function I cited above, this way I stay at the bottom of the table.
You can have your UITableView hidden on viewDidLoad, and then change it to visible on viewDidAppear right after you scroll the table to the bottom. This way the user won't see the scrolling animation.
The solution is to override viewWillAppear and let it scroll (non-animated) to the bottom:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self goToBottom];
}
-(void)goToBottom
{
NSIndexPath *lastIndexPath = [self lastIndexPath];
[self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
}
-(NSIndexPath *)lastIndexPath
{
NSInteger lastSectionIndex = MAX(0, [self.tableView numberOfSections] - 1);
NSInteger lastRowIndex = MAX(0, [self.tableView numberOfRowsInSection:lastSectionIndex] - 1);
return [NSIndexPath indexPathForRow:lastRowIndex inSection:lastSectionIndex];
}
By performing this at viewWillAppear it will be done before the user sees the table.
You can fix it by making an invisible footer and do the calculations in there. When the footer is loaded the contentSize is updated. To make it scroll I check set the contentOffset of the tableview.
I have commented out the animation part, since you wanted it without, but it also works.
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
return 1;
}
-(UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
if( tableView.contentOffset.y != tableView.contentSize.height - tableView.frame.size.height && automaticScroll ){
//[UIView animateWithDuration:0.0 animations:^{
self.contentTableView.contentOffset = CGPointMake(0, tableView.contentSize.height - self.contentTableView.frame.size.height);
//} completion:^(BOOL finished) {
[tableView reloadData];
//}];
automaticScroll = NO;
}
UIView *emptyFooter = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 1)];
emptyFooter.backgroundColor = [UIColor clearColor];
return emptyFooter;
}
I created a BOOL automaticScroll to trigger the scroll to the bottom. This should be set in the viewWillAppear method, or whenever you load the data and reload the tableView.
If you want to add rows, you also need to set the BOOL, like:
-(void)addItemButtonClicked:(id)sender
{
automaticScroll = YES;
//Add object to data
[self.contentTableView reloadData];
}
If you need more help, please let me know.
scrollToRowAtIndexPath
use to scroll the row in tableview to particular position
just change content inset after load data to move content view of table view if height is less than parent view.
[self.tableView reloadData];
[self.tableView setContentInset:UIEdgeInsetsMake(self.view.frame.size.height - self.tableView.contentSize.height < 0 ? 0 : self.view.frame.size.height - self.tableView.contentSize.height, 0, 0, 0)];
Swift 3.1
tableView.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
Credits: #Christos Hadjikyriacou
I have a UITableView populated with custom UITableViewCells. Within those custom cells, I have a UITextField and a "See More" UIButton. The purpose of the UIButton is to dynamically expand that particular UITableCell when the user wishes to read more of the text. In the same way, when the user wishes to return to the original size, the user clicks the Button again, and the UITableViewCell will shrink to the original size.
Since the cell isn't being selected, I setup an IBAction within the Custom Cell like such:
//Within CustomCell.m
- (IBAction)showMoreText:(id)sender
{
//instance bool variable to flag whether the cell has been resized
self.hasBeenResized = YES;
//turn off mask to bounds, otherwise cell doesnt seem to resize
[[self.cellView layer] setMasksToBounds:NO];
// Calculate the new sizes and positions for the textView and the button
CGRect newTextViewFrame = self.textView.frame;
newTextViewFrame.size.height = self.textView.contentSize.height;
self.textView.frame = newTextViewFrame;
CGFloat bottomYPos = self.textView.frame.origin.y + self.textView.frame.size.height;
CGRect buttonFrame = self.showMoreButton.frame;
buttonFrame.origin.y = bottomYPos;
self.showMoreButton.frame = buttonFrame;
// Call begin and end updates
[(UITableView*) self.superview beginUpdates];
[(UITableView*) self.superview endUpdates];
// Set mask and put rounded corners on the cell
[[self.cellView layer] setMasksToBounds:YES];
[[self.cellView layer] setCornerRadius:10.0];
}
Following this, I have this in my ViewController class:
// Within ViewController.m
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"heightForRowAtIndexPath");
CustomCell *cell = (CustomCell*)[self tableView:tableView cellForRowAtIndexPath:indexPath];
if([cell hasBeenResized] == NO)
{
return cell.frame.size.height + 20;
}
else
{
return cell.frame.size.height + cell.textView.frame.origin.y + cell.textView.frame.size.height + cell.showMoreButton.frame.size.height + 20;
}
}
What happens now is I can see the custom cell change the size of its textview, however, the table does not update the row height for that particular cell. Checking on the If-else statement there, it appears that hasBeenResized is always false, even though I set it to YES within the IBACtion of the CustomCell.
I have looked at other solutions here, but they all seem to involve didSelectRowAtIndexPath, which I cannot use in this instance (I have another behavior for the cell when it is selected).
Am I doing this completely wrong? Ideally, what I would like to do is to have the "Show More" button animate downwards as the textview is expanded and vice versa when it's collapsed.
Thank you!
Method beginUpdates won't call reloadData for you - you have to do it manually.
For your case it would be best to call:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
And place your showMoreText code in tableView:cellForRowAtIndexPath: method (for selected cell only)
To Update your tableView, you have to reload it.
[tableView reloadData];
I'm having some trouble getting my buttons to work below a certain y value in my table cells. I'm using a custom UITableViewCell class named "RowWhiskyContent". The default height is 44px and it's below that point my events don't seem to trigger anymore. The button displays just fine and so does everything else below that point, the event however don't seem to trigger. If i place my button half way (like at y=35) only the top part of the button triggers the event and the bottom part doesn't do a thing.
Here's the code trimmed down to the esentials:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
if(![self createView:&cell])
{
UIImage *bottle = [UIImage imageNamed:#"icon_add.png"]; //image size: 22x22
UIButton *bottleButton = [[UIButton alloc] initWithFrame:CGRectMake(60, 70, bottle.size.width, bottle.size.height)];
[bottleButton setImage:bottle forState:UIControlStateNormal];
[cell.contentView addSubview:bottleButton];
[bottleButton addTarget:self action:#selector(addToCollection:) forControlEvents:UIControlEventTouchUpInside];
cell.contentView.frame = CGRectMake(cell.contentView.frame.origin.x, cell.contentView.frame.origin.y, cell.contentView.frame.size.width, 160);
//cell.frame = cell.contentView.frame; // Tried this, didn't work.
//[tableView reloadData]; // Tried this too, didn't work either.
}
return cell;
}
// Check if cell exists and create the cell if it doesn't.
-(BOOL) createView: (UITableViewCell**) cell
{
BOOL cellExists = YES;
*cell = (RowWhiskyContent *) [myTableView dequeueReusableCellWithIdentifier:#"ContentIdentifier"];
if(*cell == nil)
{
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"RowWhiskyContent" owner:self options:nil];
*cell = [topLevelObjects objectAtIndex:0];
cellExists = NO;
}
return cellExists;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
{
return 160;
}
Since I'm setting the height of the cell and the contentView both at 160 I'm not sure what's going wrong here. Reloading the data didn't work and neither did setting the cell.frame.
Could anybody please tell me what I'm doing wrong?
Thanks in advance.
EDIT:
Added a screenshot:
Red button works fine but if I place it at the position of the green button it stops working. The contentview's background is set to purple so that explains the purple area. When clicking the cell it triggers the didSelectRowAtIndexPath so I'm guessing the the cell itself is also big enough.
This is definitely a content size issue. Your button will display outside of the frame similar to overflow in CSS, however they will not respond to events. So whatever UIView is containing your UIButton you need to make sure that it's content/frame/bounds are all set tall enough. You can also use [cell.contentView sizeToFit] to adjust it automatically to it's content.
You should definitely NOT reload data inside of the protocol methods for your UITableView.
I'm currently working on an iPhone app that's doing some strange things with a UIScrollView inside a UITableView. This is my first foray into iPhone dev, so I'm sure it's something silly I'm missing.
In each UITableViewCell I am putting in a basic UITableViewCell. In that UITableViewCell is a Label and a UIScrollView.
The label and scrollview is setup and working properly, but when it first displays it is offset about 30 pixels down on the y axis than it should be, or is positioned by the XIB/NIB. I am not moving it around manually. The label shows up in the right spot. at 0,0. The UIScrollView should be showing up at 0,22 but is showing up closer to 0,40.
When I swipe to scroll the containing UITableView, then all the UIScrollViews will show up in the right spot assuming that when the UITableView scrolled that UITableViewCell went offscreen.
Here is the code for the UITableView.cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"GalleryRowCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
[cell.layer setMasksToBounds:TRUE];
[cell.layer setCornerRadius:10.0];
Contagion *c = (Contagion *)[self.dataSet objectAtIndex:indexPath.row];
GalleryRowViewController *subView = [[GalleryRowViewController alloc] initWithContagion:c];
[cell.contentView addSubview:subView.view];
subView.contagionName.text = c.name;
subView.contagion = c;
return cell;
}
Here is the code for my GalleryRowViewController.viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
self.imageScroll.delegate = self;
[self.imageScroll setBackgroundColor:[UIColor blackColor]];
[self.imageScroll setCanCancelContentTouches:NO];
self.imageScroll.indicatorStyle = UIScrollViewIndicatorStyleWhite;
self.imageScroll.clipsToBounds = NO;
self.imageScroll.scrollEnabled = YES;
self.imageScroll.pagingEnabled = NO;
NSInteger x = 0;
CGFloat xPos = 0;
for (x=0;x<=10;x++) {
UIImage *image = [UIImage imageNamed:#"57-icon.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[imageView setBackgroundColor:[UIColor yellowColor]];
CGRect rect = imageView.frame;
rect.size.height = 70;
rect.size.width = image.size.width;
rect.origin.x = xPos;
rect.origin.y = 5;
imageView.frame = rect;
[self.imageScroll addSubview:imageView];
xPos += imageView.frame.size.width+5;
}
[self.imageScroll setContentSize:CGSizeMake(xPos, [self.imageScroll bounds].size.height)];
}
--- EDIT FOR IMAGES ---
After App Loads: http://img809.imageshack.us/img809/4576/screenshot20110927at427.png
After Scrolling the rows offscreen and back: http://img690.imageshack.us/img690/9461/screenshot20110927at428.png
Well, as my previous response was at too low a level, let me take another shot at it.
First, I just noticed the core problem that you're using a viewcontroller for each cell. To quote Apple, " "A single view controller typically manages the views associated with a single screen’s worth of content." That would also get rid of your XIB (just manually configuring your scrollview), which I bet will get rid of your problem.
To proceed, your main choice is whether to create a ContagionTableViewCell class or not as suggested by Scott.
If so, following the Elements example, create a subclass of UITableViewCell ContagionTableViewCell with properties of a scrollView, a labelview and a contagion. Like they use a custom setter for the element, use one for the contagion, so that whenever it is assigned, it also updates the cells label (and associated pictures).
Move your imageScroll code from GalleryRowViewController.viewDidLoad into the ContagionTableViewCell init code. Put the image code into a new routine, which will be called from the contagion setter.
If NOT, then move the GalleryRowView Controller code into your UITableView. I suggest you take a look at cellForRowAtIndexPath in Apple's tableViewSuite, the fourth example on subviews. In particular, it shows this pattern of separating the creation of a cell (when you need a brand new one) vs configuring the cell (when reusing it). As you have 10 imageViews inside your scrollView, you'll have to decide whether to delete all those (and/or the scrollview), or just reach inside and update their images when a cell is reused.
Can you post a screenshot. Its a bit hard to visualize what you are describing. I'm not sure how you are computing y origin to be 22.
As a side note I believe its cleaner to do this by creatint your own TableViewCell subclass and use that instead of the default UITableViewCell. There is an example called Elements which shows how to do this properly: http://developer.apple.com/library/ios/#samplecode/TheElements/Introduction/Intro.html
Well, I don't know it's the cause of your problem, but you've definitely got an issue. Note that every time you are asked for a cell, you're adding the galleryRow subview. When a cell goes off-screen, it's put on the reusableCell queue. Then you're asked for another cell; you get it from the queue, it still has the old galleryRow subview, and now you add another one; so that's not good. You should either reuse or delete the old one.
Finally, why are you using UITableViewCellStyleSubtitle, and then not using any of the default fields in that UITableView?
I've been working on a new app and was really hoping to implement a swipe to reveal more options menu inside my application. I've searched and searched, but it seems no one else has successfully made it work (aside from Loren). What I'm trying to do is swipe the cell, and simultaneously use CABasicAnimation to push it to x: 320, and add a subview below this that would have the buttons etc.. I'm using willBeginEditing to detect the swipe to avoid subclassing. Here's the code:
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
theAnimation.duration=0.0;
theAnimation.repeatCount=0;
theAnimation.autoreverses=NO;
theAnimation.removedOnCompletion = NO;
theAnimation.fillMode = kCAFillModeForwards;
theAnimation.fromValue=[NSNumber numberWithFloat:0];
theAnimation.toValue=[NSNumber numberWithFloat:+320];
[cell.layer addAnimation:theAnimation forKey:#"animateLayer"];
CGRect frame = CGRectMake(0, 59 * indexPath.row, 320, 59);
UIView *menu = [[[UIView alloc] initWithFrame:frame] autorelease];
NSString *path = [NSString stringWithFormat:#"%#%#",
[[NSBundle mainBundle] resourcePath],
#"/flick.wav"];
SystemSoundID soundID;
NSURL *filePath = [NSURL fileURLWithPath:path isDirectory:NO];
AudioServicesCreateSystemSoundID((CFURLRef)filePath, &soundID);
AudioServicesPlaySystemSound(soundID);
self.menu.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"dots.png"]];
[self.view addSubview:menu];
[self.view sendSubviewToBack:menu];
}
- (void)animationDidStop:(NSString*)animationID finished:(BOOL)finished context:(void *)context
{
// Release
[self release];
}
#pragma mark - Swipe Menu II
- (void)scrollViewDidScroll:(UIScrollView *)tableView {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:nil];
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
theAnimation.duration=0.2;
theAnimation.repeatCount=0;
theAnimation.autoreverses=NO;
theAnimation.fromValue=[NSNumber numberWithFloat:320];
theAnimation.toValue=[NSNumber numberWithFloat:0]
;
[cell.layer addAnimation:theAnimation forKey:#"animateLayer"];
}
The problem is the sliding back of the cell - I want to do it when any touch outside the menu view is received, but because it is a scrollView, I can't. The ScrollViewdidScroll Method only animates the cell back to its normal place once it is scrolled of the viewport. (As in, under the NavigationBar or object that obscures it) The last key issue is the ability to detect if a menu is already visible or active and a cell is already off of the screen, slide the cell back to its original position, remove the menu view, and then slide the other cell out and add the menu.
I would like to be the first beside Loren to implement this as so many others have tried, especially on StackOverflow.
I apologize for the poor formatting in the code.
Thanks in advance,
Kolin
I've actually made this work quite nicely, you can check out the project here: http://www.thermoglobalnuclearwar.com/opensource/
It also overcomes the problem in Tweetie where you cannot swipe the same cell twice without first swiping something else.
If you make any constructive changes, or need any help, let me know!
Loren's implementation has a fatal flaw, which is if you swipe on a cell, then scroll the table, you cannot re-swipe the same cell until you swipe a different cell. I believe this is because the tableview believes the cell is still being edited until you try and "edit" another cell.
You may want to investigate doing something else instead, like using a UISwipeGestureRecognizer attached to the cell to detect the swipe.
I can think of 2 approaches to try and detect taps outside of the cell as well. The first is to subclass UIApplication and override -sendEvent:. You can use this to detect the tap event and dismiss the cell "editing". The main problem here is giving the UIApplication subclass knowledge about your cell. The second approach is to slap a custom UIView subclass over the entire screen and in -hitTest:withEvent: you can test to see if the touch belongs to the area over your cell. If it doesn't, dismiss the cell "editing". In both cases, return nil to allow the event to proceed to the underlying views (and don't forget to remove this overlay view after the event happens). You may also want to test the UIEvent to make sure it contains a new touch before dismissing the cell.