Drag and Rearrange UICollectionViews through Layouts

We will add a much wanted functionality to our UICollectionViews. The ability to drag a cell and reposition it somewhere else in the view.

(full code hosted at BitBucket)

To achieve this we need:

  1. Add a gesture recogniser. In this example we chose a long press recogniser which will give us a clear indication that the user wants to drag a specific cell.
  2. Set an object that knows about the collection view to be the gesture’s delegate and action target. Catch the dragging actions.
  3. Create a snapshot of the cell (UIImageView instance), hide the cell so that we only drag its representation. Add the representation on a superview called canvas.
  4. When drag over another cell swap the data (if the collection view is data driven which should be the majority of cases) and then swap the cells.
  5. When the user lets go, remove the image being dragged and unhide the cell.

One decision that must be made is where to put the recogniser. This is an arbitrary choice with a few options available, but here we chose the collection view’s layout class. This will make our code more portable since we essentially need to drop a single file into a new project and simply replace a collection view’s layout to achieve the drag and rearrange functionality.

We create KDRearrangeableCollectionViewFlowLayout.swift

We need to make a decision as to where the dragging action will be drawn (canvas). If a view has not been assigned explicitly we will use the collection view’s superview. This assumes that it what we want.

As we drag, we need a reference to the original cell being dragged, its representation image and its offset from the press point. We also need to track the current index path so it is better to hold all that information in a separate structure we will call Bundle and will be internal to the class.

The offset it the difference between the point where the user presses and the cell’s origin. This will help maintain the same distance as we drag, rather than snapping the representation view’s origin to the press point.

UIGestureRecognizerDelegate defines the method gestureRecognizerShouldBegin giving us the chance to stop gestures that are not happening on cells. We loop over the cells and convert their frames to the canvas where we perform a hit test against the press point. After we find the correct cell we initialise a bundle instance. The existence this instance will later serve as a flag to indicate whether we are in a dragging state or not.

Now, for the meet of the matter. The UILongPressGestureRecognizer has 3 states of interest Began, Changed and Ended. At the first we hide the cell being dragged and add its representation to the view hierarchy.

As we drag we need to update the position of the representation image, get the index path under the current point of the user’s finger (if any) and check it against the last index path saved in the bundle. If the index paths are different it is time to swap the cells!

The only unwrapping happening it at the end where the bundle needs its value updated. Also note that the code that swaps the actual data by calling moveDataItem is optional. To implement it have created a protocol defining a single method for moving data from one index path to another.

The controller in the project implements it like so:

Finally, on .Ended we need to remove the representation view and unhide the cell that is being dragged. We check as to whether we have a proper delegate one last time and we call reloadData. Otherwise there is no real need since the collection view is not data driven.

And Voilà!

There is one thing missing, paging. We need to be able to drag to the edges of our collection view and have it pan to the next page regardless of whether it is scrolling horizontally or vertically. We define a method checkForDraggingAtTheEdgeAndAnimatePaging that will do what it is named to! We can pre-cache the areas of collision as 4 CGRects and store them in a dictionary as can be seen in the code.  The only thing worth mentioning here is that we need to be able to pan over multiple pages. We do that by putting a timer and checking again. If the representation image is still over an area where drag is available we repeat.

Have a go and let me know!