On an iPad esspecially, it is often the case that you may have multiple UITableViews on the screen at the one time, unlike on the iPhone. I wanted to have two UITableView that I could drag and drop cells between the two to swap items between a set of all possible choices and a set of selected choices.

It made sense to me to use a long press to pop off a cell that can then be dragged across the screen and dropped on another view. I took inspiration from this YouTube Video however after looking at the the code on git hub I really wanted to work on trying to remove the need for an overlay view to capture the touches as it felt a little hacky to me (although kudos for the solution). So if you want to see what it looks like then you can check out the final demo running on YouTube and the following illustrates how I achieved it.

Creating DragAndDropTableViewController
The key to this method is recognising that the UILongPressGestureRecognizer is a continuous event that even after it has been fired your receiver will get UIGestureRecognizerStateChanged events that correspond to where your finger is after the long press has been performed. This is important to note since it lets you know where your finger is on the screen after the long press has begun allowing you to detect dragging.

First I added a long press gesture to the UITableViewCell that is to be dragged and dropped.

UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:cell action:nil];
longPress.delegate = self;
[cell addGestureRecognizer:longPress];

Then, as always with gestures we must implement the UIGestureRecognizerDelegate for more on Gestures you can check out my how to tutorial here or have a look at the video for this tutorial on YouTube.

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    if([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]){
        [gestureRecognizer addTarget:self action:@selector(longGestureAction:)];
    }
    return YES;
}
 
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    return YES;
}
 
-(void)longGestureAction:(UILongPressGestureRecognizer *)gesture{
    UITableViewCell *cell= (UITableViewCell *)[gesture view];
 
    switch ([gesture state]) {
        case UIGestureRecognizerStateBegan:{          
            NSIndexPath *ip = [self.tableView indexPathForCell:cell];
            [self.tableView setScrollEnabled:NO];
            if(ip!=nil){
                [self.draggableDelegate dragAndDropTableViewController:self  draggingGestureWillBegin:gesture forCell:cell];
                UIView *draggedView = [self.draggableDelegate dragAndDropTableViewControllerView:self ];
                //switch the view the gesture is associated with this will allow the dragged view to continue on where the cell leaves off from
                [draggedView addGestureRecognizer:[[cell gestureRecognizers]objectAtIndex:0]]; 
                [self.draggableDelegate dragAndDropTableViewController:self draggingGestureDidBegin:gesture forCell:cell];
            }
        }
            break;
        case UIGestureRecognizerStateChanged:{
            [self.draggableDelegate dragAndDropTableViewController:self draggingGestureDidMove:gesture];
        }
            break;
        case UIGestureRecognizerStateEnded:{
            UIView *draggedView = [self.draggableDelegate dragAndDropTableViewControllerView:self];
            if(draggedView==nil)
                return;
 
            //this does not seem like the best way to do this yet you really don't want to fire one after the other I don't think
            [self.draggableDelegate dragAndDropTableViewController:self draggingGestureDidEnd:gesture];
            [self.dropableDelegate dragAndDropTableViewController:self droppedGesture:gesture];           
 
            [self.tableView setScrollEnabled:YES];
            [self.tableView reloadData];
        }
            break;
 
//        case UIGestureRecognizerStateCancelled:
//        case UIGestureRecognizerStateFailed:
//        case UIGestureRecognizerStatePossible:
//            [self.dragAndDropDelegate dragAndDropTableViewController:self draggingGesture:gesture endedForItem:nil];
            break;
        default:
            break;
    }
}

I added this UILongPressGestureRecognizer into a superclass called DragAndDropTableViewController as the handler itself does not need to be exposed and while it is quite simple it is not the prettiest but it handles some of edge cases that allow this to work and uses delegation to leave the details up to you. Lets have a look and the handler in more detail, first when the long press gesture begins we grab the view that has been pressed (which we know is a cell) and find the NSindexPath for it in our UITableView. We then fire off a method to the delegate to let it know that the dragging has begun, this is where the delegate (in my example) stores the item that the cell represents and creates the draggable representation however, I will get to that soon. The you grab the view from the delegate that will be the draggable representation, now for the key part you have to take the gesture from the cell and pass it over to the draggable representation. This means that the draggable view will continue to receive the touch gestures for the current instance of the recogniser giving the seamless dragging experience. I disable the scrolling of the tableView otherwise the table scrolls as you move your dragged item around so the quick fix was just to disable this.

UITableViewCell *cell= (UITableViewCell *)[gesture view];
 
        case UIGestureRecognizerStateBegan:{          
            NSIndexPath *ip = [self.tableView indexPathForCell:cell];
            [self.tableView setScrollEnabled:NO];
            if(ip!=nil){
                [self.draggableDelegate dragAndDropTableViewController:self  draggingGestureWillBegin:gesture forCell:cell];
                UIView *draggedView = [self.draggableDelegate dragAndDropTableViewControllerView:self ];
                //switch the view the gesture is associated with this will allow the dragged view to continue on where the cell leaves off from
                [draggedView addGestureRecognizer:[[cell gestureRecognizers]objectAtIndex:0]]; 
                [self.draggableDelegate dragAndDropTableViewController:self draggingGestureDidBegin:gesture forCell:cell];
            }
        }

Now that we have attached the gesture recogniser to the new view if we delete the cell in the draggingGestureDidBegin in the delegate it will not be released and will keep firing so now we can pass on the UIGestureRecognizerStateChanged to the delegate allowing it to reposition the dragging view as the touch moves around the screen.

        case UIGestureRecognizerStateChanged:{
            [self.draggableDelegate dragAndDropTableViewController:self draggingGestureDidMove:gesture];
        }

Finally notify the delegate that the item has been dropped and that the dragging has ended. At this point the delegate should look at the view that the item has been dropped on and take appropriate action. You also have to remember to re-enable the scrolling so the tableView act as normal once dragging ends.

        case UIGestureRecognizerStateEnded:{
            UIView *draggedView = [self.draggableDelegate dragAndDropTableViewControllerView:self];
            if(draggedView==nil)
                return;
 
            //this does not seem like the best way to do this yet you really don't want to fire one after the other I don't think
            [self.draggableDelegate dragAndDropTableViewController:self draggingGestureDidEnd:gesture];
            [self.dropableDelegate dragAndDropTableViewController:self droppedGesture:gesture];           
 
            [self.tableView setScrollEnabled:YES];
            [self.tableView reloadData];
        }

Using DragAndDropTableViewController
As you have seen the DragAndDropTableViewController is relatively simple and you can use it very simply should you want to achieve the dragging and dropping of cells between tables By implementing the DraggableDelegate and DroppableDelegate. First you must implement draggingGestureDidBegin and decide how to handle the dragging of a cell. In the draggingGestureWillBegin I take the cell that has been selected and take a snap shot of it. Note: To grab the snapshot of the CALayer you need to add the QuartzCore framework and include it, I add it to the pre-compiled header (.pch file). Then I construct a view to drag with a UIView and a UIImageView subview which displays the snap shot of the cell.

-(void)dragAndDropTableViewController:(DragAndDropTableViewController *)ddtvc draggingGestureWillBegin:(UIGestureRecognizer *)gesture forCell:(UITableViewCell *)cell{
 
    UIGraphicsBeginImageContext(cell.contentView.bounds.size);
    [cell.contentView.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
 
    UIImageView *iv = [[UIImageView alloc] initWithImage:img];
    self.dragAndDropView = [[UIView alloc]initWithFrame:iv.frame];
    [self.dragAndDropView addSubview:iv];
    [self.dragAndDropView setBackgroundColor:[UIColor blueColor]];
    [self.dragAndDropView setCenter:[gesture locationInView:self.view.superview]];
 
    [self.view.superview addSubview:self.dragAndDropView];
}

In the draggingGestureDidBegin I remove the cell from the table to make it look like the cell has been popped out of the table. You have to have a handle to the item that this cell represents so you can add it to the correct view when the user drops the view. The final method here returns the dragged view created in the draggingGestureWillBegin so that this can be used in the DragAndDropTableViewController to attach the gesture that was originally associated with the cell selected.

-(void)dragAndDropTableViewController:(DragAndDropTableViewController *)ddtvc draggingGestureDidBegin:(UIGestureRecognizer *)gesture forCell:(UITableViewCell *)cell;
{    
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    self.selectedChoice =[self.choices objectAtIndex:indexPath.row];     
    [self.choices removeObjectAtIndex:[self.choices indexOfObject:self.selectedChoice]];
    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
}
 
-(UIView *)dragAndDropTableViewControllerView:(DragAndDropTableViewController *)ddtvc{
    return self.dragAndDropView;
}

The final two delegate methods are very straight forward, in draggingGestureDidMove you must remember to update the position of the draggable view when the gesture moves and in draggingGestureDidEnd remove the dragged view when dragging ends.

-(void)dragAndDropTableViewController:(DragAndDropTableViewController *)ddtvc draggingGestureDidMove:(UIGestureRecognizer *)gesture{
    [self.dragAndDropView setCenter:[gesture locationInView:self.view.superview]];
}
 
-(void)dragAndDropTableViewController:(DragAndDropTableViewController *)ddtvc draggingGestureDidEnd:(UIGestureRecognizer *)gesture{    
    [self.dragAndDropView removeFromSuperview];
    self.dragAndDropView = nil;
}

The final piece of the jigsaw is to handle adding the item when the user drops on another view. This is done in the droppedGesture

-(void)dragAndDropTableViewController:(DragAndDropTableViewController *)ddtvc droppedGesture:(UIGestureRecognizer *)gesture{
 
    UIView *viewHit = [self.view hitTest:[gesture locationInView:self.view.superview] withEvent:nil];
 
    if([ddtvc isKindOfClass:[ChoicesTableViewController class]]){
        ChoicesTableViewController *fromTBCV=(ChoicesTableViewController *)ddtvc;
        id selectedChoice = fromTBCV.selectedChoice;
 
        if([fromTBCV.view isEqual:viewHit])
            [fromTBCV.choices addObject:selectedChoice];
        else if([viewHit.superview isKindOfClass:[UITableViewCell class]] && [fromTBCV.view isEqual:viewHit.superview.superview]){
            //we have dropped on a cell in our table
            NSIndexPath *ip = [fromTBCV.tableView indexPathForCell:(UITableViewCell *)viewHit.superview];
            [fromTBCV.choices insertObject:selectedChoice atIndex:ip.row];
        }
        else if([self.selectedChoicesViewController.view isEqual:viewHit]){
            [self.selectedChoicesViewController.selectedChoices addObject:selectedChoice];
            [self.selectedChoicesViewController.tableView reloadData];
        }
        else if([viewHit.superview isKindOfClass:[UITableViewCell class]] && [self.selectedChoicesViewController.view isEqual:viewHit.superview.superview]){
            //we have dropped on a cell in our table
            NSIndexPath *ip = [self.selectedChoicesViewController.tableView indexPathForCell:(UITableViewCell *)viewHit.superview];
            [self.selectedChoicesViewController.selectedChoices insertObject:selectedChoice atIndex:ip.row];
            [self.selectedChoicesViewController.tableView reloadData];
        }   
    }
    else if([ddtvc isKindOfClass:[SelectedChoicesTableViewController class]]){
        SelectedChoicesTableViewController *fromSCTVC=(SelectedChoicesTableViewController *)ddtvc;
        id selectedChoice = fromSCTVC.selectedChoice;
 
        if([fromSCTVC.view isEqual:viewHit])
            [fromSCTVC.selectedChoices addObject:selectedChoice];
        else if([viewHit.superview isKindOfClass:[UITableViewCell class]] && [fromSCTVC.view isEqual:viewHit.superview.superview]){
            //we have dropped on a cell in our table
            NSIndexPath *ip = [fromSCTVC.tableView indexPathForCell:(UITableViewCell *)viewHit.superview];
            [fromSCTVC.selectedChoices insertObject:selectedChoice atIndex:ip.row];
        }
        else if([self.choicesViewController.view isEqual:viewHit]){
            [self.choicesViewController.choices addObject:selectedChoice];
            [self.choicesViewController.tableView reloadData];
        }
        else if([viewHit.superview isKindOfClass:[UITableViewCell class]] && [self.choicesViewController.view isEqual:viewHit.superview.superview]){
            //we have dropped on a cell in our table
            NSIndexPath *ip = [self.choicesViewController.tableView indexPathForCell:(UITableViewCell *)viewHit.superview];
            [self.choicesViewController.choices insertObject:selectedChoice atIndex:ip.row];
            [self.choicesViewController.tableView reloadData];
        }        
    }
    else
        NSLog(@"Item dropped on a non droppable area");
}

At the moment if you drop the cell on a view that I haven’t handled for adding items through this method the item is effectively deleted but you could handle this to make the view bounce back the view it came from or use the draggingGestureDidMove to detect the view the item is over to show that it will be deleted similar to the smoke puff for items being pulled from the dock on Mac OS X. I would love to here your feedback to improve this as I think this could be something that is very useful for lots of people here is the code for the DragAndDropTutorial for you to give it a go.