Home > Articles > Mobile Application Development & Programming

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

Implementing Snap Zones

One of my favorite dynamic animator tricks involves creating snap zones—areas of your interface that pull in dragged items once they overlap a particular region. This approach allows you to collect items into well-managed zones and offer a pleasing “snap-into-place” animation. In the general form shown in Listing 6-3, there’s no further test beyond whether a dragged view has strayed into a zone. However, you might want to expand the approach to limit blue items to blue zones or red items to red zones, and so forth.

Listing 6-3 assumes that users will have access to multiple zones and even that a view might move from one zone directly to another. It uses a tagging scheme to keep track of this potential reparenting. A free view has no current parent and can move freely about. When a free view overlaps a snap zone, however, it suspends dragging by disabling the view’s gesture recognizer and adds a snap-to-parent behavior. The view slides into place into its new parent. Once it arrives, as the dynamic animator pauses, the recognizer is re-enabled.

Allowing a view to escape from its new parent’s bounds is the tricky bit—and the motivating reason for the view tagging. You do not want a view to recapture its child unless the dragging gesture has ended, which is why this method keeps track of the gesture state. With new parents, however, the snap behavior is added (and the gesture is suspended) as soon as a view strays over the line. Balancing the escapes and the captures ensures that the user experience is snappy and responsive and does not thwart the user’s desires to remove a view from a parent.

Listing 6-3 Handling Multiple Snap Zones

- (void) draggableViewDidMove: (NSNotification *) note
{
    // Check for view participation
    UIView *draggedView = note.object;
    UIView *nca = [draggedView nearestCommonAncestorWithView:
        _animator.referenceView];
    if (!nca) return;

    // Retrieve state
    UIGestureRecognizer *recognizer = (UIGestureRecognizer *)
        draggedView.gestureRecognizers.lastObject;
    UIGestureRecognizerState state = [recognizer state];

    // View frame and current attachment
    CGRect draggedFrame = draggedView.frame;
    BOOL free = draggedView.tag == 0;

    for (UIView *dropZone in _dropZones)
    {
        // Make sure all drop zones are views
        if (![dropZone isKindOfClass:[UIView class]])
            continue;

        // Overlap?
        CGRect dropFrame = dropZone.frame;
        BOOL overlap = CGRectIntersectsRect(draggedFrame, dropFrame);

        // Free moving
        if (!overlap && free)
        {
            continue;
        }

        // Newly captured
        if (overlap && free)
        {
            if (suspendedRecognizer)
            {
                NSLog(@”Error: attempting to suspend second recognizer”);
                break;
            }

            // New parent.
            // CAPTURED is an integer offset for tagging
            suspendedRecognizer = recognizer;
            suspendedRecognizer.enabled = NO; // stop!
            draggedView.tag = CAPTURED + dropZone.tag; // mark as captured
            UISnapBehavior *behavior = [[UISnapBehavior alloc]
                initWithItem:draggedView
                snapToPoint:RectGetCenter(dropFrame)];
            [_animator addBehavior:behavior];
            break;
        }

        // Is this the current parent drop zone?
        BOOL isParent = (dropZone.tag + CAPTURED == draggedView.tag);

        // Current parent
        if (overlap && isParent)
        {
            switch (state)
            {
                case UIGestureRecognizerStateEnded:
                {
                    // Recapture
                    UISnapBehavior *behavior = [[UISnapBehavior alloc]
                        initWithItem:draggedView
                        snapToPoint:RectGetCenter(dropFrame)];
                    [_animator addBehavior:behavior];
                    break;
                }
                default:
                {
                    // Still captured but no op
                    break;
                }
            }
            break;
        }

        // New parent
        if (overlap)
        {
            suspendedRecognizer = recognizer;
            suspendedRecognizer.enabled = NO; // stop!
            draggedView.tag = CAPTURED + dropZone.tag;
            UISnapBehavior *behavior = [[UISnapBehavior alloc]
                initWithItem:draggedView
                snapToPoint:RectGetCenter(dropFrame)];
            [_animator addBehavior:behavior];
            break;
        }
    }
}
  • + Share This
  • 🔖 Save To Your Account