Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Horizontal movement of a UITableView?

I'm trying to distinguish between horizontal swiping / panning and vertical scrolling in a UITableView. The behavior I'm looking to imitate (in a sense) is that of the Twitter iPad app, that has multiple UITableView that can be moved horizontally on the screen. If I slide my finger left or right on one of these UITableView, the view itself moves horizontally. If I swipe vertically, the view scrolls as expected.

I'm having trouble figuring out the correct way to implement this behavior. I've seen some tutorials on this which involve adding touch event handlers in the UITableViewCells, and overriding hitTest in the UITableViewto appropriately route events depending on which direction the gesture is moving. I've implemented some of these techniques, but none of them work particularly well.

Does anyone know the correct way to implement this sort of behavior? Conditionally performing actions on a UITableViewdependent on the direction of the user's finger movement?

Thanks.

like image 449
jimt Avatar asked Jul 09 '11 19:07

jimt


1 Answers

I've been struggling with a similar problem for days, and I've went through several potential solutions. I've found the best way and also the simplest solution to be subclassing UIGestureRecognizer to handle horizontal movement and attach it to your UITableViews.

The way it works is that it intercepts any touch events before they go to the UITableView (also UIScrollView). The UITableView, being a subclass of UIScrollView, has a custom UIPanGestureRecognizer built in which detects dragging and scrolls it's view accordingly. By adding your own subclass of UIGestureRecognizer, you can get the touches before the UIScrollView's gesture recognizer does. If your recognizer sees that the user is dragging horizontally, it should change it's state in an overridden touchesMoved: method to UIGestureRecognizerStateBegan. Otherwise, it sets it's state to UIGestureRecognizerCancelled, which lets the underlying UIScrollView handle the touches instead.

Here's what my UIGestureRecognizer subclass looks like:

#import <UIKit/UIGestureRecognizerSubclass.h>

@interface TableViewCellPanGestureRecognizer : UIGestureRecognizer
{
    CGPoint startTouchPoint;
    CGPoint currentTouchPoint;
    BOOL isPanningHorizontally;
}

- (void)reset;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

@end

@implementation TableViewCellPanGestureRecognizer

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    startTouchPoint = [[touches anyObject] locationInView:nil];
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 
{
    [super touchesMoved:touches withEvent:event];
    currentTouchPoint = [[touches anyObject] locationInView:nil];

    if ( !isPanningHorizontally ) {

        float touchSlope = fabsf((currentTouchPoint.y - startTouchPoint.y) / (currentTouchPoint.x - startTouchPoint.x));

        if ( touchSlope < 1 ) {
            self.state = UIGestureRecognizerStateBegan;
            isPanningHorizontally = YES;
            [self.view touchesCancelled:touches withEvent:event];
        } else {
            self.state = UIGestureRecognizerStateCancelled;
            [self.view touchesCancelled:touches withEvent:event];
        }

    } else {
        self.state = UIGestureRecognizerStateChanged;
    }
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    self.state = UIGestureRecognizerStateCancelled;
    [self.view touchesCancelled:touches withEvent:event];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    self.state = UIGestureRecognizerStateCancelled;
}

-(void)reset
{
    [super reset];
    startTouchPoint = CGPointZero;
    currentTouchPoint = CGPointZero;
    isPanningHorizontally = NO;
}

@end

Then I have a subclassed UITableView that attaches the recognizer to itself and implements an action method to trigger horizontal movement of individual rows:

In my UITableView init:

horizontalPanGesture = [[TableViewCellPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleHorizontalDrag:)];
[self addGestureRecognizer:horizontalPanGesture];
[horizontalPanGesture release];

And the action method:

-(void)handleHorizontalDrag:(UIGestureRecognizer *)gesture
{   
    UIGestureRecognizerState state = gesture.state;

    // Set the touched cell
    if (!touchedCell){
        NSIndexPath *indexPathAtHitPoint = [self indexPathForRowAtPoint:[gesture locationInView:self]];
        id cell = [self cellForRowAtIndexPath:indexPathAtHitPoint];
        touchedCell = cell;
        startTouchPoint = [gesture locationInView:touchedCell];
    }

    if ( state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged ) {

        // move your views horizontally          

    } else if ( state == UIGestureRecognizerStateEnded || state == UIGestureRecognizerStateCancelled ) {

        touchedCell = nil;

    }
}

The above gets the current cell being touched within the table view, and then applies horizontal movements to it as the user drags left or right. However, if my gesture recognizer determines that the touches are meant to scroll vertically, it just cancels itself and the following touches are sent on to the UITableView to initiate vertical scrolling automatically.

This setup seems to be much simpler than overriding hitTest and doing all sorts of touch event trickery within the UITableView itself. It simply makes an immediate determination about the direction of the touch movement. You'll want to read up on UIGestureRecognizers - specifically about how it should be subclassed. You need to make sure to forward on certain touch events like touchesCancelled to the UITableView, as the UITableView's built in panGestureRecognizer won't be handling these events as it normally does. Obviously, you'll want to move entire table views and not individual cells, but it should be pretty straightforward.

This solution, as simple as it is, took me awhile to get exactly right for my exact needs. I am pretty new to IOS development, so I had to spend a lot of time reading about and tinkering with gesture recognizers and scroll views to figure this out.

like image 95
Darek Rossman Avatar answered Oct 14 '22 03:10

Darek Rossman