Ad

Our DNA is written in Swift
Jump

Tap&Hold for TableView Cells, Then and Now

It was before SDK 3.2 that I developed a technique to add tap-and-hold interactivity to your tableview cells. In this article I’ll demonstrate the old technique, which still works, and contrast it with how much easier it has become if you can target iOS 3.2 and above.

First the “old way”. It needs to customize touch handling for the tableview cells themselves, which means you have to subclass UITableViewCell.

TouchAndHoldCell.h

#import 
 
@class TouchAndHoldCell;
 
@protocol TouchAndHoldCellDelegate
@optional
- (void)didTouchAndHoldForCell:(TouchAndHoldCell *)cell;
@end
 
@interface TouchAndHoldCell : UITableViewCell
{
	id  delegate;
}
 
@property (nonatomic, assign) id  delegate;
@end

TouchAndHoldCell.m

#import "TouchAndHoldCell.h"
 
@implementation TouchAndHoldCell
 
@synthesize delegate;
 
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        // Initialization code
    }
    return self;
}
 
- (void)touchAndHold:(NSDictionary *)params
{
	NSSet *touches = [params objectForKey:@"touches"];
	UIEvent *event = [params objectForKey:@"event"];
	[super touchesCancelled:touches withEvent:event];
 
	if (delegate && [delegate respondsToSelector:@selector(didTouchAndHoldForCell:)])
	{
		[delegate didTouchAndHoldForCell:self];
	}
}
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	// 1 second holding triggers touchAndHOld
	[self performSelector:@selector(touchAndHold:) withObject:
		[NSDictionary dictionaryWithObjectsAndKeys:touches, @"touches", event, @"event", nil] afterDelay:0.5];
	[super touchesBegan:touches withEvent:event];
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	[NSObject cancelPreviousPerformRequestsWithTarget:self];
	[super touchesMoved:touches withEvent:event];
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	[NSObject cancelPreviousPerformRequestsWithTarget:self];
	[super touchesEnded:touches withEvent:event];
}
 
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
	[NSObject cancelPreviousPerformRequestsWithTarget:self];
	[super touchesCancelled:touches withEvent:event];
}

This works by calling a selector with half a second delay. If the finger is lifted before this expires the delayed call is cancelled. If it does get to expire then we have a long press and thus the cell can tell it’s delegate (the tableview controller) that a long press (that’s how Apple calls it) transpired.

To use this you would override the cellForRowAtIndexPath method and provide a method to react to the long press.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    static NSString *CellIdentifier = @"Cell";
 
    TouchAndHoldCell *cell = (TouchAndHoldCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[TouchAndHoldCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
		cell.delegate = self;
    }
 
	// Configure the cell.
	cell.textLabel.text = @"Text";
    return cell;
}
 
#pragma mark TouchAndHoldCell Delegate
- (void)didTouchAndHoldForCell:(TouchAndHoldCell *)cell
{
	UIActionSheet *action = [[UIActionSheet alloc] initWithTitle:@"Actions" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Destruct" otherButtonTitles:nil];
	[action showInView:self.view];
	[action release];
}
 
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
{
	if (buttonIndex==0)
	{
		NSLog(@"Destruct");
	}
	else
	{
		NSLog(@"Cancel");
	}
}

That’s the historic evidence of past ingenuity. Next we’re going to examine how that’s done today. Easier, faster, more Apple if you will.

iOS and SDK 3.2 gave us one of the most ingenious things that Apple could ever invent. For two years we had to code all our touch handling ourselves which resulted in a variety of different approaches, all with a different feel to them. Gesture Recognizers changed all that, 3.2 brought them first to the iPad and they are also part of iOS 4.x. Unfortunately they are not backwards compatible.

All it takes is to create such a gesture recognizer instance and attach it to the appropriate view.

- (UITableViewCell *)tableView:(UITableView *)tableView
            cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
	{
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
		reuseIdentifier:CellIdentifier] autorelease];
 
		UILongPressGestureRecognizer *longPressGesture =
			[[[UILongPressGestureRecognizer alloc]
			  initWithTarget:self action:@selector(longPress:)] autorelease];
		[cell addGestureRecognizer:longPressGesture];
    }
 
	// Configure the cell.
	cell.textLabel.text = @"Text";
    return cell;
}
 
- (void)longPress:(UILongPressGestureRecognizer *)gesture
{
	// only when gesture was recognized, not when ended
	if (gesture.state == UIGestureRecognizerStateBegan)
	{
		// get affected cell
		UITableViewCell *cell = (UITableViewCell *)[gesture view];
 
		// get indexPath of cell
		NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
 
		// do something with this action
		NSLog(@"Long-pressed cell at row %@", indexPath);
	}
}

Gesture Recognizers employ the same methodology as we have seen before for adding target/actions to UIControls, for example UIButtons. The target is the instance of a class to receive the message, the action is the selector. And the parameter that’s being passed is the sender of the message. For UIControl events that’s the sending view, for UIGestureRecognizer events it’s the instance of the gesture recognizer you have set up.

In the above example we simply create a UILongPressGestureRecognizer (which has the appropriate expiration time interval already built in) and attach it to each cell we create. If the tableview is long enough so that we get to recycle cells it would still work, because we don’t care which cell a recognizer is attached to, as long as there is one for each cell you can tap on, recycled or new.

In the longPress: method we can query the tableView for the “line” we tapped on. For a while I was setting the cell’s tag value to the number of the row, but I found that this ceases to work if you allow for deletion of rows, because then the indexes you put into the tags have incorrect values. Better to ask the tableView itself at time of the pushing via the conveneient indexPathForCell method.

Also note that this method is being called twice, namely when the gesture is recognized (when the finger was on the cell long enough) and when it’s ended (when you lift the finger). Usually you’ll want to only react when it’s started.

You see, that gesture recognizers really reduce the amount of code we need to write to achieve some advanced touch handling.


Categories: Recipes

8 Comments »

  1. It is useful for a fresher to Apple like me. Thanks

  2. Best example of tap and hold I’ve seen so far. Also, drop-and-use code for the UITableCellView. Bravo!

  3. Thanks for the very useful snippets. Others are suggesting attaching to the UITableView, but this seems like a better way, since it yields the cell index. Speaking of which, there’s a minor typo in that last NSLog: indexPath should be indexPath.row (or change “row %d” to “%@”).

Trackbacks

  1. iOS 4 – Gesture Recognizers