Ancient Programming

What I encounter in my software part of life is in danger of being commented upon here

Drag and drop between multiple UIViews in iOS

Posted by Jacob von Eyben on April 5th, 2012

This is an example showing how to drag and and drop UIViews between other UIViews in iOS.

Illustrates the drag and drop UIView stack

Illustrates the drag and drop UIView stack

The example is broken into the following four and a half step.

Example broken into steps
1. register a UIPanGestureRecognizer to be able to get drag panning events  (in our case drag events).
2. detect if the panning started above the view we would like to drag.
3. move the view according to the panning - which means removing the object from the view we would like to drag it from.
4. when dragging ends, drop the view onto the view below the dragged object.
4.a. optionally reset the dragged object, if the object is dragged onto a view which we don’t consider as a valid drop view.


In my example I have created a more or less generic DragDropManager, which is responsible for actually handling the UIViews objects we allow to drag ‘n drop and the UIVeiws we allow to drop onto.

Step 1
The manager registers the draggable objects and drop areas and takes care of detecting when a object should be dragged and dropped. This is done by receiving pan events from the UIPanGestureRecognizer. I recommend registering the gesture recognizer to a UIView having a frame covering the entire drag and drop area, because if you bind the recognizer to the actual draggable objects, you will loose the pan sequence as soon as you change super view for the dragged object (and changing super view is the hole point of dragging objects between UIViews).

@implementation DragDropBetweenViewsViewController
...
- (void)viewDidLoad {
    [super viewDidLoad];
    _viewA = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 200)];
    [_viewA setBackgroundColor:[UIColor greenColor]];
    _viewA.tag = 1;
    _viewB = [[UIView alloc] initWithFrame:CGRectMake(0, 220, 320, 200)];
    [_viewB setBackgroundColor:[UIColor yellowColor]];
    _viewB.tag = 2;

    [[self view] addSubview:_viewA];
    [[self view] addSubview:_viewB];
    //[[self view] addSubview:_viewB];

    //add elements to drag and drop
    UIView *dragDropView1 = [[[UIView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)] autorelease];
    UIView *dragDropView2 = [[[UIView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)] autorelease];
    [_viewA addSubview:dragDropView1];
    [_viewB addSubview:dragDropView2];
    [dragDropView1 setBackgroundColor:[UIColor redColor]];
    [dragDropView2 setBackgroundColor:[UIColor blueColor]];
    NSMutableArray *draggableSubjects = [[NSMutableArray alloc] initWithObjects:dragDropView1, dragDropView2, nil];
    NSMutableArray *droppableAreas = [[NSMutableArray alloc] initWithObjects:_viewA, _viewB, nil];
    _dragDropManager = [[DragDropManager alloc] initWithDragSubjects:draggableSubjects andDropAreas:droppableAreas];
    [draggableSubjects release];
    [droppableAreas release];

    UIPanGestureRecognizer * uiTapGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:_dragDropManager action:@selector(dragging:)];
    [[self view] addGestureRecognizer:uiTapGestureRecognizer];
    [uiTapGestureRecognizer release];
}
...
@end

Step 2
The manager switches on the different states of the UIPanGestureRecognizer and detects if a panning  starts on one of our dragSubjects. If so we store the DragContext containing the dragged view and the starting point (to be able to snap back to the offset in case the drag ends outside a drop zone). At the same time we change superview for the dragged object, by attaching it to the UIView spanning the entire area. This is done to ensure our UIView is shown above all other subviews and hence is visible during the entire drag drop session.

Step 3
During the drag process we simply move the object according to the coordinates of the UIGestureRecognizer.

Step 4
When the panning ends (the UIGestureRecognizer is in state UIGestureRecognizerStateEnded), we check to see if the dropped view is above any registered drop areas. If so, we add the dragged UIView as a subview of the detected drop area.

Step 4.a.
If we don’t recognize a valid drop area beneath our dragged view, we snap the dragged UIView back to its original superview and position.


The DragManager code can be seen here:

@implementation DragDropManager {

    NSArray *_dragSubjects;
    NSArray *_dropAreas;
    DragContext *_dragContext;
}

@synthesize dragContext = _dragContext;
@synthesize dropAreas = _dropAreas;

- (id)initWithDragSubjects:(NSArray *)dragSubjects andDropAreas:(NSArray *)dropAreas {
    self = [super init];
    if (self) {
        _dropAreas = [dropAreas retain];
        _dragSubjects = [dragSubjects retain];
        _dragContext = nil;
    }

    return self;
}

- (void)dealloc {
    [_dragSubjects release];
    [_dragContext release];
    [_dropAreas release];
    [super dealloc];
}

- (void)dragObjectAccordingToGesture:(UIPanGestureRecognizer *)recognizer {
    if (self.dragContext) {
        CGPoint pointOnView = [recognizer locationInView:recognizer.view];
        self.dragContext.draggedView.center = pointOnView;
    }
}

- (void)dragging:(id)sender {
    UIPanGestureRecognizer *recognizer = (UIPanGestureRecognizer *) sender;
    switch (recognizer.state) {
        case UIGestureRecognizerStateBegan: {
            for (UIView *dragSubject in _dragSubjects) {
                CGPoint pointInSubjectsView = [recognizer locationInView:dragSubject];
                BOOL pointInSideDraggableObject = [dragSubject pointInside:pointInSubjectsView withEvent:nil];
                NSLog(@"point%@ %@ subject%@", NSStringFromCGPoint(pointInSubjectsView), pointInSideDraggableObject ? @"inside" : @"outside", NSStringFromCGRect(dragSubject.frame));
                if (pointInSideDraggableObject) {
                    NSLog(@"started dragging an object");
                    self.dragContext = [[[DragContext alloc] initWithDraggedView:dragSubject] autorelease];
                    [dragSubject removeFromSuperview];
                    [recognizer.view addSubview:dragSubject];
                    [self dragObjectAccordingToGesture:recognizer];
                } else {
                    NSLog(@"started drag outside drag subjects");
                }
            }
            break;
        }
        case UIGestureRecognizerStateChanged: {
            [self dragObjectAccordingToGesture:recognizer];
            break;
        }
        case UIGestureRecognizerStateEnded: {
            if (self.dragContext) {
                UIView *viewBeingDragged = self.dragContext.draggedView;
                NSLog(@"ended drag event");
                CGPoint centerOfDraggedView = viewBeingDragged.center;
                BOOL droppedViewInKnownArea = NO;
                for (UIView *dropArea in self.dropAreas) {
                    CGPoint pointInDropView = [recognizer locationInView:dropArea];
                    NSLog(@"tag %li pointInDropView %@ center of dragged view %@", dropArea.tag, NSStringFromCGPoint(pointInDropView), NSStringFromCGPoint(centerOfDraggedView));
                    if ([dropArea pointInside:pointInDropView withEvent:nil]) {
                        droppedViewInKnownArea = YES;
                        NSLog(@"dropped subject %@ on to view tag %li", NSStringFromCGRect(viewBeingDragged.frame), dropArea.tag);
                        [viewBeingDragged removeFromSuperview];
                        [dropArea addSubview:viewBeingDragged];
                        //change origin to match offset on new super view
                        viewBeingDragged.frame = CGRectMake(pointInDropView.x - (viewBeingDragged.frame.size.width / 2), pointInDropView.y - (viewBeingDragged.frame.size.height / 2), viewBeingDragged.frame.size.width, viewBeingDragged.frame.size.height);
                    }
                }

                if (!droppedViewInKnownArea) {
                    NSLog(@"release draggable object outside target views - snapping back to last known location");
                    [self.dragContext snapToOriginalPosition];
                }

                self.dragContext = nil;
            } else {
                NSLog(@"Nothing was being dragged");
            }
            break;
        }
    }
}
@end

The DragContext containing the dragged view and the original position and view

#import "DragContext.h"

@implementation DragContext {

    UIView *_draggedView;
    CGPoint _originalPosition;
    UIView *_originalView;
}
@synthesize draggedView = _draggedView;

- (id)initWithDraggedView:(UIView *)draggedView {
    self = [super init];
    if (self) {
        _draggedView = [draggedView retain];
        _originalPosition = _draggedView.frame.origin;
        _originalView = [_draggedView.superview retain];
    }

    return self;
}

- (void)dealloc {
    [_draggedView release];
    [_originalView release];
    [super dealloc];
}

- (void)snapToOriginalPosition {
    [_draggedView removeFromSuperview];
    [_originalView addSubview:_draggedView];
    _draggedView.frame = CGRectMake(_originalPosition.x, _originalPosition.y, _draggedView.frame.size.width, _draggedView.frame.size.height);
}
@end

Room for improvements
- animations should be added to show more smooth look and feel.. This blog post demonstrates how to add animations to the snap back to orignial position.
- Consider the visible order of the views when selecting draggable UIView, if multiple draggable UIViews is stacked upon each other.

18 Responses to “Drag and drop between multiple UIViews in iOS”

  1. Samuel Williams Says:

    Thanks for this fantastic example.

  2. Ancient Programming » Blog Archive » Animating the drag drop example Says:

    [...] Drag and drop between multiple UIViews in iOS [...]

  3. Harold Chattaway Says:

    Great example! Is there a complete project available for this that can be downloaded?

    Thanks

  4. Jacob von Eyben Says:

    I have a simple test project which contains the the code I have shown above - I will make it available for download in the next couple of days.

    Then you are free to copy and modify the code as you like.

  5. Jack Says:

    When will you post the test project?
    I just can’t understand the multiView code if you can explain it again, please?

    thank you, it was a great tutorial

  6. Jacob von Eyben Says:

    Until I have found a better way to share the example (like a github repo or something), you can download the drag/drop files here: http://dl.dropbox.com/u/6258534/blog/download/ios/dragdrop_version_0_1.zip

    The idea about the drag and drop between multiple UIViews, is that you can register a list of drop areas (list of UIViews) you can drag objects (other UIViews) on to. That means that the dragged UIView is added as subview to the drop area they are dropped on to.
    If you drop your view outside a drop area, your view is transformed back to its previous valid position (the position where you started dragging it from).

  7. codemonkey Says:

    AWESOME example mate. Thanks a lot. Integrated it without a sweat into my project :)

  8. Bill Says:

    Great example. Thanks for the post. Have you found a solution for the pointInSubjectsView always returning false? The pointInside check in StateEnded seems to work correctly. Very strange.

  9. Micha Molko Says:

    I wrote some similar code awhile back, my code is heavily based on Scott Sherwood’s code.
    If anyone wants a look, Tell me some way to post it!

  10. Sher Says:

    I have made a jigsaw puzzle in which i am placing small UIImage view in a view from a scroll view. The problem is when i touch (by mistake ) two image view in the puzzle, one of them becomes the “drag” view but the other one just misplaces itself . how to avoid touch on multiple views???

  11. Jacob von Eyben Says:

    @Sher
    The example I have provided, only have one view registered for touches and that is the DragDropBetweenViewsViewController.
    You then let that ViewController together with the DragDropManager, decide which one you logically did touch and hence would like to start dragging.

    If two puzzle pieces is placed upon each other, and both will answer yes to the [dragSubject pointInside:pointInSubjectsView withEvent:nil];, you could extend the example above to find the piece with the highest index in the view stack.

  12. saswata Says:

    @Jacob your code works great but does not change the size or location of the dragged view in relation to the dropped view area. So, how do I resize the dragged image and snap it to the center of the dropped view location and fill the dropped area?

  13. Mathias Åberg Says:

    Thanks a lot for some excellent code. As codemonkey said, this was very easily added to an existing project. Right now I’m trying to figure out how to make a copy of the dragged subject that it wont disappear from the original view.

    good stuff!

  14. Sebastien Says:

    @Jaboc.

    Thanks for this nice code sample. I am seeing the same weird behavior when testing it in the simulator (not tested on a real device yet):
    “pointInside: seems to answer NO even though the point is actually inside the view”.
    It’s like touches near the edges of the view are considered outsite the view…

    Did you find out anything about this issue ?

  15. Jacob von Eyben Says:

    @Sebastien: do you mean that the pointinside returns NO when you start dragging?
    The general problem in my example is that I rely on the UIPanGestureRecognizer, which first sends the first event ‘UIGestureRecognizerStateBegan’, after a little while - it has to know that it is actually Panning. The solution to this would be to attach a UITapGestureRecognizer or something similar to detect the initial touch. When the UIPanGestureRecognizer detects a pan start, the initial touch coordinate could be used to detect pointinside

  16. Sebastien Says:

    @Jacob: Your analysis is right. I have had no luck with setting up an extra UITapGestureRecognizer though (it was never going through UIGestureRecognizerStateBegan state). Instead, I use a workaround whichworks great (found here: http://stackoverflow.com/questions/10728066/why-is-there-a-delay-when-moving-object-using-uipangesturerecognizer): replacing the UIPanGestureRecognizer by a UILongPressGestureRecognizer with a minimumPressDuration of 0.

  17. Moree john Says:

    where is the sample code????
    I need it immediately please help me out…

  18. Sivamurugan Says:

    @Jacob:
    Thanks a lot for some excellent patch of code. Integrated to my app and its working fine !… Great work keep it up :) :) :)

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>