Drag and Drop Between UICollectionViews

This tutorial shows how you can implement a simple drag and drop interface between UICollectionViews.  For this tutorial you will need basic knowledge of UICollectionView and UICollectionViewFlowLayout.

 

Demo Overview

The sample app is a single view application containing two vertically stacked UICollectionView subviews.  The bottom UICollectionView contains cells which the user can drag to the upper UICollectionView.  In addition the app does some validation such as preventing users from dragging cells in the same UICollectionView.  When this happens the dragged card appears faded indicating an invalid operation.

 

Getting Started

Open Xcode, select the "Single View Application" iPhone template and fill out the textfields.  At this point I like to setup group folders such as "Views", "View Controllers" and "Models", to make file navigation a bit easier.  To create groups open the "Project Navigator" pane and right click the app name folder and select "New Group" from the popup menu.  When you create the "View Controllers" group go ahead and highlight and drag the "ViewController.*" files to the group.  Again this is an optional step but for bigger projects sorting files like this makes it easier to find files.

Select the collectionview and open the Attributes inspector

Select the collectionview and open the Attributes inspector

Now select the Main.storyboard file and drag and drop two "Collection View" controls vertically under the main "View".  Make sure that the dimensions are 320x430 and 320x140 pixels respectively for the top and bottom UICollectionViews.  You can also give the views a distinct background to help distinguish them apart at runtime. 

 
outlet.png

With the collection views setup in the storyboard the main view controller needs references to them via IBOutlets.  To create the outlets select the Main.storyboard file and open an assistant editor (select View > Assistant Editor > Show Assistant Editor from the menu bar).  Make sure that the ViewController.m file is selected.  In the Main.storyboard hover over the top collection view and CTRL drag it view to the ViewController.m category declaration and name the outlet "destinationCollectionView".  Do the same for the bottom collection view and name it "sourceCollectionView".

 

Model Setup

new_class.png

Our collectionview contains number cards so we will implement a simple model which holds a single integer property.  From Xcode select "File > New > File..." from the menu to open the New File wizard.  In the presented window select the iOS Cocoa Touch Objective-C class option and select Next.  Set the "Class" field to "MyModel" and "Subclass of" field to "NSObject".  Once completed Xcode will prompt a file selector dialog asking you where to place the new class.  Since this is a model object make sure the "Group" combobox is set to "Models" and click "Create".  Open the MyModel.h file in Xcode and add a single integer property which stores the card number.

 

UICollectionViewCell Setup

cardcells.png

Each cell is presented as a simple number card with the ability to highlight itself when it is dragged.  To start create a new Objective-C class named "CardView" with "UIView" as its subclass and save the file in the "Views" group folder.  The CardView class contains a UILabel property that we will use to set the card number and a method to highlight the view.

// CardView.h

#import <UIKit/UIKit.h>

@interface CardView : UIView

@property (nonatomic, strong) UILabel *label;

- (void)setHighlightSelection:(BOOL)highlight;

@end

  • Add a property for the UILabel.  We will use this label to display the card integer value.
  • setHighlightSelection method is used to give a highlighting effect to the card.
// CardView.m

#import "CardView.h"

#pragma mark -
@implementation CardView

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    self.label               = [[UILabel alloc] init];
    self.label.font          = [UIFont boldSystemFontOfSize:48];
    self.label.textAlignment = NSTextAlignmentCenter;
    [self addSubview:self.label];

    self.backgroundColor    = [UIColor whiteColor];
    self.layer.cornerRadius = 10.0f;
  }
  return self;
}

- (void)layoutSubviews {
  [super layoutSubviews];

  self.label.frame = self.bounds;
}

#pragma mark - Public methods
- (void)setHighlightSelection:(BOOL)highlight {
  if (highlight) {
    self.layer.borderColor = [UIColor blueColor].CGColor;
    self.layer.borderWidth = 5.0f;
  } else {
    self.layer.borderWidth = 0.0f;
  }
}

@end

  • We allocate the label, set the font to a bold system font, center align and add the label as a subview.  
  • Set the card background to white and give the view rounded corners.  
  • Set the UILabel bounds to the parent UIView bounds. 
  • Implement the highlighting for the cardview by setting the border color to blue and giving the view a non zero width.

Now that we have a reusable UIView we can use it to render our UICollectionViewCell.  Create a new Objective-C class named "CardCell" as a subclass of "UICollectionViewCell" and place the created file under the "Views" group.  The cell will be responsible for updating the card number when it is assigned to a "MyModel" instance.


// CardCell.h

#import <UIKit/UIKit.h>
#import "MyModel.h"

extern NSString * const CELL_REUSE_ID;

@interface CardCell : UICollectionViewCell

@property (nonatomic, strong) MyModel *model;

@end



  • We extern a NSString constant for the cell reuse id.
  • Add a property for MyModel model.

// CardCell.m

#import "CardCell.h"
#import "CardView.h"

NSString * const CELL_REUSE_ID = @"CELL_ID";

@interface CardCell () {
  CardView *_cardView;
}

@end

#pragma mark -
@implementation CardCell

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    _cardView = [[CardView alloc] init];
    [self.contentView addSubview:_cardView];
  }
  return self;
}

- (void)setModel:(MyModel *)model {
  _model = model;

  _cardView.label.text = [NSString stringWithFormat:@"%d", _model.value];
}

#pragma mark - Overriden methods
- (void)layoutSubviews {
  [super layoutSubviews];

  _cardView.frame = self.bounds;
}

@end


  • Complete the extern definition and set the string to @"CELL_ID".  We will need this string to register a reusable collectionview cell.
  • Create a category Category extension and define a private iVar to store the CardView object.
  • In the initWithFrame initializer create a CardView instance  and make it a subview of the UICollectionViewCell contentView property.
  • Override the setModel setter property method and update the card view label text.  We will set it to the passed MyModel integer value.
  • Override the layoutSubviews method to make sure that the card view frame is set to the UICollectionViewCells bounds. 

Collection View Setup

With the card view and cell in place we can now populate the collection views.  In this demo we want to be able to detect when cells are dragged, dropped, and when an invalid drop is performed.  To do this we will create two lightweight view controllers to handle this logic rather than dumping everything in the main ViewController class.  Create a new Objective-C class named "DestinationViewController" with a subclass of "NSObject" and place it in the "View Controllers" group in the Project Navigator pane.  Do the same  for the "SourceViewController" class.

Source View Controller

// SourceViewController.h

#import <Foundation/Foundation.h>
#import "ViewController.h"
#import "MyModel.h"

@interface SourceViewController : NSObject

- (instancetype)initWithCollectionView:(UICollectionView *)view
               andParentViewController:(ViewController *)parent;

- (void)cellDragCompleteWithModel:(MyModel *)model
               withValidDropPoint:(BOOL)validDropPoint;

@end


  • Add a custom initializer that passes a reference to the UICollectionView and parent ViewController.  We need a reference to the ViewController to inform it when the user has selected a cell.
  • The cellDragCompleteWIthModel:withValidDropPoint is a callback that the ViewController will use to inform the SourceViewController when a cell drag operation has completed.  If the drop point is not valid (user drops the cell in the source view controller) the dragged cell should not be removed from the source collection.

// SourceViewController.m

#import "SourceViewController.h"
#import "CardCell.h"

@interface SourceViewController () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout> {
  UICollectionView *_collectionView;
  ViewController *_parentController;
  NSMutableArray *_models;
  MyModel *_selectedModel;
}
@end


  • Import the CardCell.h header
  • Add a category extension to the SourceViewController class to store private iVars and declare that the class implements the UICollectionViewDataSource and UICollectionViewDelegateFlowLayout protocols.  We will need references to the collectionview, the parent view controller, the array of models and the selected model.

- (instancetype)initWithCollectionView:(UICollectionView *)view andParentViewController:(ViewController *)parent {
  if (self = [super init]) {
    [self setUpModels];
    [self initCollectionView:view];
    [self setUpGestures];

    _parentController = parent;
  }
  return self;
}

- (void)cellDragCompleteWithModel:(MyModel *)model withValidDropPoint:(BOOL)validDropPoint {
  if (model != nil) {
    // get indexPath for the model
    NSUInteger index = [_models indexOfObject:model];
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];

    if (validDropPoint) {
      [_models removeObjectAtIndex:index];
      [_collectionView deleteItemsAtIndexPaths:@[indexPath]];

      [_collectionView reloadData];
    } else {

      UICollectionViewCell *cell = [_collectionView cellForItemAtIndexPath:indexPath];
      cell.alpha = 1.0f;
    }
  }
}


#pragma mark - Setup methods
- (void)setUpModels {
  _models = [NSMutableArray array];

  for (int i=0; i<10; data-preserve-html-node="true" i++) {
    [_models addObject:[[MyModel alloc] initWithValue:i]];
  }
}

- (void)initCollectionView:(UICollectionView *)view {
  _collectionView            = view;
  _collectionView.delegate   = self;
  _collectionView.dataSource = self;

  [_collectionView registerClass:[CardCell class] forCellWithReuseIdentifier:CELL_REUSE_ID];
}

- (void)setUpGestures {

  UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self
                                                                                                 action:@selector(handlePress:)];
  longPressGesture.numberOfTouchesRequired = 1;
  longPressGesture.minimumPressDuration    = 0.1f;
  [_collectionView addGestureRecognizer:longPressGesture];
}

  • Implement the custom initializer method.   Here we will setup the data models, initialize the collectionview and setup the touch gesture to determine cell selection.
  • Implement the cellDragCompleteWithModel:withValidDropPoint method.  This is a callback method that the parent view controller will use once it detects that the drag operation has completed.  If the drop is a valid operation we remove the passed model and cell from the collection view.  If the drop is invalid we simply make the selected cell visible again.
  • The setUpModels class simple generates 10 MyModel classes as stores them in the private iVar
  • initCollectionView method sets the collectionview dataSource and delegate properties used to display cells in the collectionview.  We also register a reusable cell and assign it to the CardCell class.
  • The setUpGestures method creates a UILongPressGestureRecognizer to determine if the start of the press is on a cell.  Here we configure the numberOfTouchesRequired to 1 and minimumPressDuration properties to 0.1f so we can quickly determine if a cell has been selected.

#pragma mark - Gesture Recognizer
- (void)handlePress:(UILongPressGestureRecognizer *)gesture {
  CGPoint point = [gesture locationInView:_collectionView];

  if (gesture.state == UIGestureRecognizerStateBegan) {
    NSIndexPath *indexPath = [_collectionView indexPathForItemAtPoint:point];
    if (indexPath != nil) {
      _selectedModel = [_models objectAtIndex:indexPath.item];

      // calculate point in parent view
      point = [gesture locationInView:_parentController.view];

      [_parentController setSelectedModel:_selectedModel atPoint:point];

      // hide the cell
      [_collectionView cellForItemAtIndexPath:indexPath].alpha = 0.0f;
    }
  }
}

  • Add the handlePress gesture handler method to the file.
  • We first get the selection point in the collectionview and determine if it was over a collection cell using the UICollectionView indexPathForItemAtPoint method.
  • If the indexPath returned is not nil then the touch was over a cell and we pass the selected model and touch point to the parent view controller.  Notice the touch point is calculated with the view controllers view and not the collection view.  The parent view controller will use the updated touch point to render the dragged cell.
  • Lastly we hide the selected cell by settings its alpha to 0.0f until the drag operation is complete.  When the operation is complete we will need to check if the drop location is correct or not so we can remove or show the cell.

#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  return _models.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  CardCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:CELL_REUSE_ID forIndexPath:indexPath];

  MyModel *model = [_models objectAtIndex:indexPath.item];
  [cell setModel:model];

  return cell;
}

#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  return CGSizeMake(100, 120);
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
  return UIEdgeInsetsMake(0, 10, 0, 10);
}
  1. Add the setup methods below the custom initializer code.
  2. We will instantiate some models and store them in the array reference.
  3. We initialize the passed collection view by setting its new delegate and datasource properties and then register the reuse cell.
  4. To detect the start of a cell selection we will use a UILongPressGestureRecognizer.  We set the number of touches required to detect the gesture to 1 and set the minimum press duration to 0.1f;
  5. When the press gesture has been recognized in the UIGestureRecognizerStateBegan state we check if the touch was on a cell.  If the touch is over the cell we calculate the model selected, pass it to the parent controller (so it can render the dragged cell) and then hide the cell in the collection.  We hide the cell in the collection to reinforce to the end user that the card is being dragged.
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  return _models.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  CardCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:CELL_REUSE_ID forIndexPath:indexPath];

  MyModel *model = [_models objectAtIndex:indexPath.item];
  [cell setModel:model];

  return cell;
}

#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  return CGSizeMake(100, 120);
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
  return UIEdgeInsetsMake(0, 10, 0, 10);
}

We will now implement the UICollectionViewDataSource and UIViewCollectionFlowDelegateLayout methods so we can populate the collection cells.

  1. The collection view has only one section so the number of items in the section will be the number of models we generated.
  2. We create a reusable cell and populate it with the model index.
  3. Each cell will be 100x120.
  4. Add some insets to the left and right side. 

 

Destination View Controller Setup

We will create another lightweight view controller for the destination collectionview.   

#import <Foundation/Foundation.h>
#import "MyModel.h"
#import "ViewController.h"

@interface DestinationViewController : NSObject

- (instancetype)initWithCollectionView:(UICollectionView *)collectionView;
- (void)addModel:(MyModel *)model;

@end
  • Open the DestinationViewController.h file and add two methods.
  • Add a custom initializer method that takes a reference to the collection view
  • The addModel method takes the dragged model from the source collectionview which will be used to populate the destination collectionview
#import "DestinationViewController.h"
#import "CardCell.h"

@interface DestinationViewController ()  {
  NSMutableArray *_models;
  UICollectionView *_collectionView;
}

@end

#pragma mark -
@implementation DestinationViewController

- (instancetype)initWithCollectionView:(UICollectionView *)collectionView {
  if (self = [super init]) {
    _models = [NSMutableArray array];

    _collectionView = collectionView;
    _collectionView.delegate = self;
    _collectionView.dataSource = self;
    [_collectionView registerClass:[CardCell class] forCellWithReuseIdentifier:CELL_REUSE_ID];
  }
  return self;
}

- (void)addModel:(MyModel *)model {
  [_models addObject:model];

  [_collectionView reloadData];
}

#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  return _models.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  CardCell *cell = (CardCell *)[_collectionView dequeueReusableCellWithReuseIdentifier:CELL_REUSE_ID forIndexPath:indexPath];

  MyModel *model = [_models objectAtIndex:indexPath.item];
  cell.model = model;

  return cell;
}

#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  return CGSizeMake(100, 120);
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
  return UIEdgeInsetsMake(20, 40, 0, 40);
}

@end
  • Add a category extension to the class to implement the UICollectionViewDataSource and UICollecitonViewDelegateFlowLayout protocols.  We will need this to populate the collection view.
  • Add private iVars to store the added models and a reference to the collectionview
  • In the custom initializer method we need to instantiate a new mutable array to store the models and setup the passed collectionview.  We also register the CardCell class as the reusable cell.
  • In the addModel method we add the dragged model and reload the collectionview to populate the added cell.
  • Add the various UICollectionViewDataSource and UICollectionViewDelegateFlowLayout methods inorder to populate cells in the collectionview.
 

View Controller Setup

The main view controller communicate with the SourceViewController whenever the user starts a touch gesture on a cell.  When this happens the SourceViewController will pass the selected cell model to the main view controller so it can present the dragged card to the user.  When the drag operation is completed the view controller will detect if the drop point is valid and pass this information back to the SourceViewController and DestinationViewController classes.

 


#import <UIKit/UIKit.h>
#import "MyModel.h"

@interface ViewController : UIViewController

- (void)setSelectedModel:(MyModel *)model atPoint:(CGPoint)point;

@end

  • We declare a setSelectedModel:atPoint callback method.  This is used when the SourceViewController detects that a cell has been selected at the given point in the ViewController coordinates.
#import "ViewController.h"
#import "CardView.h"
#import "SourceViewController.h"
#import "DestinationViewController.h"

@interface ViewController () <UIGestureRecognizerDelegate> {
  SourceViewController *_sourceViewController;
  DestinationViewController *_destinationViewController;
  CardView *_draggedCard;
  MyModel *_model;
}

@property (weak, nonatomic) IBOutlet UICollectionView *destinationCollectionView;
@property (weak, nonatomic) IBOutlet UICollectionView *sourceCollectionView;

@end
  • Import the "SourceViewController.h" and "DestinationViewController.h" headers
  • Add private iVars in the ViewController category extension.  We will need a reference to the source and destination viewcontrollers, a reference for the dragged card and the current dragged card model.  We also need to make the class conform to the UIGestureRecognizer protocol so multiple gestures can be recognized.

- (void)viewDidLoad {
  [super viewDidLoad];

  _sourceViewController = [[SourceViewController alloc] initWithCollectionView:self.sourceCollectionView
                                                       andParentViewController:self];
  _destinationViewController = [[DestinationViewController alloc] initWithCollectionView:self.destinationCollectionView ];

  [self initDraggedCardView];

  UIPanGestureRecognizer *panGesture =
    [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
  panGesture.delegate = self;
  [self.view addGestureRecognizer:panGesture];
}

- (void)setSelectedModel:(MyModel *)model atPoint:(CGPoint)point {
  _model = model;

  if (_model != nil) {
    _draggedCard.label.text = [NSString stringWithFormat:@"%d", model.value];
    _draggedCard.center = point;
    _draggedCard.hidden = NO;

    [self updateCardViewDragState:[self isValidDragPoint:point]];
  } else {
    _draggedCard.hidden = YES;
  }
}

#pragma mark - Validation methods on drag and drop
- (BOOL)isValidDragPoint:(CGPoint)point {
  return !CGRectContainsPoint(self.sourceCollectionView.frame, point);
}

- (void)updateCardViewDragState:(BOOL)validDropPoint {
  if (validDropPoint) {
    _draggedCard.alpha = 1.0f;
  } else {
    _draggedCard.alpha = 0.2f;
  }
}

#pragma mark - initialization code
- (void)initDraggedCardView {
  _draggedCard = [[CardView alloc] initWithFrame:CGRectMake(0, 0, 120, 140)];
  _draggedCard.hidden = YES;
  [_draggedCard setHighlightSelection:YES];

  [self.view addSubview:_draggedCard];
}

#pragma mark - Pan Gesture Recognizers/delegate
- (void)handlePan:(UIPanGestureRecognizer *)gesture {
  CGPoint touchPoint = [gesture locationInView:self.view];
  if (gesture.state == UIGestureRecognizerStateChanged
             && !_draggedCard.hidden) {
    // card is dragged
    _draggedCard.center = touchPoint;
    [self updateCardViewDragState:[self isValidDragPoint:touchPoint]];
  } else if (gesture.state == UIGestureRecognizerStateRecognized
             && _model != nil) {
    _draggedCard.hidden = YES;

    BOOL validDropPoint = [self isValidDragPoint:touchPoint];
    [_sourceViewController cellDragCompleteWithModel:_model withValidDropPoint:validDropPoint];
    if (validDropPoint) {
      [_destinationViewController addModel:_model];
    }
  }
}

#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  return YES;
}

  • Override the viewDidLoad method. In the method create a SourceViewController, DestinationViewController, CardView objects and setup a UIPanGestureRecognizer.  We set the pan gesture recognizer delegate to the class.  We are only interested in allowing multiple gestures to be recognized so implement the gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: UIGestureRecognizerDelegate method and simply return YES;
  • The initDraggedCardView creates a CardView object with a slightly bigger dimension than the collectionview cell.  By default we hide the cards visibility and only display it when the user has started the drag gesture on a cell. Note the views center point property will be updated to the drag point during the pan gesture.
  • The setSelectedModel:atPoint method is called from the SourceViewController class when it detects that the user has selected a cell.  If the model is not nil then we populate the dragged card view and set its center point to the passed point.
  • The handlePan method is where we detect drag movement and completion.  On drag movement we update the cards center property to make it move along with the drag gesture.  On completion we determine if the card drop location is valid with the help of isValidDragPoint and updataeCardViewDragState private methods.  A card drop is considered invalid if the drop lies in the source collection view.  When this happens the card is given a transparent look.

 

Final Thoughts

We can expand this example to support drag and drop for both collection views but I leave this exercise to you.  You can find the complete sources on GitHub.