Home > Articles > Programming > General Programming/Other Languages

  • Print
  • + Share This
This chapter is from the book

Recipe: Dragging from a Scroll View

iOS’s rich set of gesture recognizers doesn’t always accomplish exactly what you’re looking for. Here’s an example. Imagine a horizontal scrolling view filled with image views, one next to another, so you can scroll left and right to see the entire collection. Now, imagine that you want to be able to drag items out of that view and add them to a space directly below the scrolling area. To do this, you need to recognize downward touches on those child views (that is, orthogonal to the scrolling direction).

This was the puzzle developer Alex Hosgrove encountered while he was trying to build an application roughly equivalent to a set of refrigerator magnet letters. Users could drag those letters down into a workspace and then play with and arrange the items they’d chosen. There were two challenges with this scenario. First, who owned each touch? Second, what happened after the downward touch was recognized?

Both the scroll view and its children own an interest in each touch. A downward gesture should generate new objects; a sideways gesture should pan the scroll view. Touches have to be shared to allow both the scroll view and its children to respond to user interactions. This problem can be solved using gesture delegates.

Gesture delegates allow you to add simultaneous recognition, so that two recognizers can operate at the same time. You add this behavior by declaring a protocol (UIGestureRecognizerDelegate) and adding a simple delegate method:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:
        (UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

You cannot reassign gesture delegates for scroll views, so you must add this delegate override to the implementation for the scroll view’s children.

The second question, converting a swipe into a drag, is addressed by thinking about the entire touch lifetime. Each touch that creates a new object starts as a directional drag but ends up as a pan once the new view is created. A pan recognizer works better here than a swipe recognizer, whose lifetime ends at the point of recognition.

To make this happen, Recipe 1-12 manually adds that directional-movement detection, outside the built-in gesture detection. In the end, that working-outside-the-box approach provides a major coding win. That’s because once the swipe has been detected, the underlying pan gesture recognizer continues to operate. This allows the user to keep moving the swiped object without having to raise his or her finger and retouch the object in question.

The implementation in Recipe 1-12 detects swipes that move down at least 16 vertical pixels without straying more than 12 pixels to either side. When this code detects a downward swipe, it adds a new DragView (the same class used earlier in this chapter) to the screen and allows it to follow the touch for the remainder of the pan gesture interaction.

At the point of recognition, the class marks itself as having handled the swipe (gestureWasHandled) and disables the scroll view for the duration of the panning event. This gives the child complete control over the ongoing pan gesture without the scroll view reacting to further touch movement.

Recipe 1-12 Dragging Items Out of Scroll Views

@implementation DragView

#define DX(p1, p2)    (p2.x - p1.x)
#define DY(p1, p2)    (p2.y - p1.y)

const NSInteger kSwipeDragMin = 16;
const NSInteger kDragLimitMax = 12;

// Categorize swipe types
typedef enum {
    TouchUnknown,
    TouchSwipeLeft,
    TouchSwipeRight,
    TouchSwipeUp,
    TouchSwipeDown,
} SwipeTypes;

@implementation PullView
// Create a new view with an embedded pan gesture recognizer
- (instancetype)initWithImage:(UIImage *)anImage
{
    self = [super initWithImage:anImage];
    if (self)
    {
        self.userInteractionEnabled = YES;
        UIPanGestureRecognizer *pan =
            [[UIPanGestureRecognizer alloc] initWithTarget:self
                action:@selector(handlePan:)];
        pan.delegate = self;
        self.gestureRecognizers = @[pan];
}

// Allow simultaneous recognition
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
        shouldRecognizeSimultaneouslyWithGestureRecognizer:
            (UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

// Handle pans by detecting swipes
- (void)handlePan:(UISwipeGestureRecognizer *)uigr
{
    // Only deal with scroll view superviews
    if (![self.superview isKindOfClass:[UIScrollView class]]) return;

    // Extract superviews
    UIView *supersuper = self.superview.superview;
    UIScrollView *scrollView = (UIScrollView *) self.superview;

    // Calculate location of touch
    CGPoint touchLocation = [uigr locationInView:supersuper];

    // Handle touch based on recognizer state

    if(uigr.state == UIGestureRecognizerStateBegan)
    {
        // Initialize recognizer
        gestureWasHandled = NO;
        pointCount = 1;
        startPoint = touchLocation;
    }

    if(uigr.state == UIGestureRecognizerStateChanged)
    {
        pointCount++;

        // Calculate whether a swipe has occurred
        float dx = DX(touchLocation, startPoint);
        float dy = DY(touchLocation, startPoint);

        BOOL finished = YES;
        if ((dx > kSwipeDragMin) && (ABS(dy) < kDragLimitMax))
            touchtype = TouchSwipeLeft;
        else if ((-dx > kSwipeDragMin) && (ABS(dy) < kDragLimitMax))
            touchtype = TouchSwipeRight;
        else if ((dy > kSwipeDragMin) && (ABS(dx) < kDragLimitMax))
            touchtype = TouchSwipeUp;
        else if ((-dy > kSwipeDragMin) && (ABS(dx) < kDragLimitMax))
            touchtype = TouchSwipeDown;
        else
            finished = NO;

        // If unhandled and a downward swipe, produce a new draggable view
        if (!gestureWasHandled && finished &&
            (touchtype == TouchSwipeDown))
        {
            dragView = [[DragView alloc] initWithImage:self.image];
            dragView.center = touchLocation;
            [supersuper addSubview: dragView];
            scrollView.scrollEnabled = NO;
            gestureWasHandled = YES;
        }
        else if (gestureWasHandled)
        {
            // allow continued dragging after detection
            dragView.center = touchLocation;
        }
    }

    if(uigr.state == UIGestureRecognizerStateEnded)
    {
        // ensure that the scroll view returns to scrollable
        if (gestureWasHandled)
            scrollView.scrollEnabled = YES;
    }
}
@end
  • + Share This
  • 🔖 Save To Your Account