Monthly Archives: April 2015

Drag and Drop between UICollectionViews

(Code on GitHub – Built for Swift 3.x)

This took me a while to code! Extending the functionality of the drag and rearrange tutorial, we can add the ability to share data between collection views.

We create 3 collection views that feed from a common root view controller which will act as both dataSource and delegate.

Screen Shot 2015-05-05 at 17.08.34

We want to make them exchange data when a cell is dragged across. The data can be anything, from a string to a managed object and as an example we will create a DataItem class that will hold a colour and an id:

Noticed the  Equatable interface? We will need the ‘==’ operator to compare DataItem instances. The implementation is defined outside the class through Swift’s operator overloading:

In the root view controller we create some mock data and hold it inside a single multidimensional array .

Here I used a convention which would normally be considered an anti-pattern but for the purpose of this example it will keep our data tidy: I assigned different tags on each collection view on the Storyboard and used these as offsets to the data array as below:

After this initial setup, we need to start a gesture recogniser. Instead of  placing it in the UICollectionViewLayout object as we did in a previous tutorial we need to abstract it into its own Manager class. To clarify things, here is a rough UML diagram:

I took the same approach as  before by creating a Bundle structure to encapsulate information about the state of the drag, as well as a canvas object where the dragging will be animated.

As we drag a cell, we need to check its relative position on the collection views and call various methods, like moveItemAtIndexPath() that will perform a swap of cells. Although most of this code could be placed in the manager class, it would prohibit the extension of the d&d to non-UICollectionViews. So we will pass this responsibility to the views themselves through the use of protocols.

When the manager sends a didMoveItem for example, a UICollectionView could decide to call the moveItemAtIndexPath while a simple UIView could rearrange its elements manually or do nothing at all.

The first thing that the manager needs to catch is the long press and decide if it is a valid point for a the start drag and drop action. It loops over the views that implement the draggable protocol using the where clause, translating the press point to their coordinates and asking each one if it can start a dragging action.

Internally, each view can decide if it can do that. In our example it is a simple case of whether we get an indexPath back:

Most views will return false as the point will be outside their frame. The one that returns true is held within the bundle together with other information such as the offset to the element dragged:

Next on the flow of the gesture recogniser is the callback on every move of the finger or pen. It has 3 states of interest which will be caught in a case statement and described separately. On .Began we just add the snapshot and notify the view that a dragging action has started.

The collection view that will receive the message will simply store the indexPath of the cell being dragged…

…and check for it inside our root view controller’s cellForItemAtIndexPath.

As well as rendering the hidden state of a cell we also need to be able to move, insert and delete the data. The rationale is that while the collection view is responsible for all UI related tasks for the dragging and dropping, such as inserting and deleting cells in the right positions, the dataSource must be able to predictably update its mode before any visual change is made. Doing it in reverse or forgetting to update the model causes a very predictable and annoying crash! This we will achieve through a special protocol that dataSource targets of a KDDragAndDropCollectionView must implement on top of being UICollectionViewDataSource compliant.

The methods above update the model by leaving it to the controller to decide on how it is done. We can be using CoreData or plain arrays like in this example.

Let’s put everything together then.

On .Changed state of the gesture recogniser we first update the snapshot using the offset we have pre-calculated

Next we loop over our KDDroppable and calculate the intersection between their frames and the snapshot’s frame by projecting them both to the canvas for reference. We calculate the area of the intersection and keep the biggest so as to get the view that our snapshot is mostly on.

We then check to see if it is the first time we are entering into this area and if so we call the willMoveItem method from the KDDroppable protocol. We always call the didMoveItem regardless and update the overDroppableView variable so as not to call the willMoveItem again while we are on top of it:

How are these protocol methods implemented in the collection views?

We check as to whether the data item exists and If it is new we insert it.

On the delegate side we must implement the insertion so that the CollectionViews have the correct data

When the user releases his press, we remove the snapshot from the canvas.

In the stopDragging of the collection view we just nullify the draggingPathOfCellBeingDragged so that when the data gets reloaded there is not cell hidden. There is a fringe case we need to cater for which happens when we are in the middle of a move of cells. In this case the reload will skip these cells and so the hidden cell will never get unhidden. So we need to do that manually.

 

For more tutorials on what’s hottest in iOS

[catlist name=”iOS”]