60

I am currently trying to implement the UITableView reordering behavior using UICollectionView.

Let's call a UItableView TV and a UICollectionView CV (to clarify the following explanation)

I am basically trying to reproduce the drag&drop of the TV, but I am not using the edit mode, the cell is ready to be moved as soon as the long press gesture is triggered. It works prefectly, I am using the move method of the CV, everything is fine.

I update the contentOffset property of the CV to handle the scroll when the user is dragging a cell. When a user goes to a particular rect at the top and the bottom, I update the contentOffset and the CV scroll. The problem is when the user stop moving it's finger, the gesture doesn't send any update which makes the scroll stop and start again as soon as the user moves his finger.

This behavior is definitely not natural, I would prefer continu to scroll until the user release the CV as it is the case in the TV. The TV drag&drop experience is awesome and I really want to reproduce the same feeling. Does anyone know how they manage the scroll in TV during reordering ?

  • I tried using a timer to trigger a scroll action repeatedly as long as the gesture position is in the right spot, the scroll was awful and not very productive (very slow and jumpy).
  • I also tried using GCD to listen the gesture position in another thread but the result is even worst.

I ran out of idea about that, so if someone has the answer I would marry him!

Here is the implementation of the longPress method:

- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
    ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
    CGPoint gesturePosition = [sender locationInView:self.collectionView];
    NSIndexPath *selectedIndexPath = [self.collectionView indexPathForItemAtPoint:gesturePosition];

    if (sender.state == UIGestureRecognizerStateBegan)
    {
        layout.selectedItem = selectedIndexPath;
        layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
    }
    else if (sender.state == UIGestureRecognizerStateChanged)
    {
        layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
        [self swapCellAtPoint:gesturePosition];
        [self manageScrollWithReferencePoint:gesturePosition];
    }
    else
    {
        [self.collectionView performBatchUpdates:^
        {
            layout.selectedItem = nil;
            layout.gesturePoint = CGPointZero; // Setting gesturePoint invalidate layout
        } completion:^(BOOL completion){[self.collectionView reloadData];}];
    }
}

To make the CV scroll, I am using that method:

- (void)manageScrollWithReferencePoint:(CGPoint)gesturePoint
{
    ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
    CGFloat topScrollLimit = self.collectionView.contentOffset.y+layout.itemSize.height/2+SCROLL_BORDER;
    CGFloat bottomScrollLimit = self.collectionView.contentOffset.y+self.collectionView.frame.size.height-layout.itemSize.height/2-SCROLL_BORDER;
    CGPoint contentOffset = self.collectionView.contentOffset;

    if (gesturePoint.y < topScrollLimit && gesturePoint.y - layout.itemSize.height/2 - SCROLL_BORDER > 0)
        contentOffset.y -= SCROLL_STEP;
    else if (gesturePoint.y > bottomScrollLimit &&
             gesturePoint.y + layout.itemSize.height/2 + SCROLL_BORDER < self.collectionView.contentSize.height)
        contentOffset.y += SCROLL_STEP;

    [self.collectionView setContentOffset:contentOffset];
}
Petter Friberg
  • 19,652
  • 9
  • 51
  • 94
foOg
  • 3,076
  • 3
  • 17
  • 18

5 Answers5

70

This might help

https://github.com/lxcid/LXReorderableCollectionViewFlowLayout

This is extends the UICollectionView to allow each of the UICollectionViewCells to be rearranged manually by the user with a long touch (aka touch-and-hold). The user can drag the Cell to any other position in the collection and the other cells will reorder automatically. Thanks go to lxcid for this.

Blumer
  • 4,890
  • 2
  • 30
  • 46
dantes85
  • 724
  • 6
  • 2
  • 20
    Posting answers that are nothing more than a link is discouraged. Summarizing the contents of the post would greatly improve this answer. – David Oct 07 '12 at 02:14
  • Thank you for the answer, that's what I am looking for. He manage the scroll with a timer but he does it in a more intelligent way that I was. It gave me the proof that a timer could do the trick, I will have a look on that solution, thanks again. The code is quiet messy and doesn't respect the Apple standards for CVC but his solution for scrolling is interesting. Thank's! – foOg Oct 07 '12 at 22:02
  • Very simple, thanks. Only thing is, while dragging an item, its background turns black. Its original background is clear (transparent). Will try to fix it, and report back. – ancajic Dec 03 '13 at 15:05
  • 1
    Fixed it. Inside "LX_rasterizedImage" method find following line and put NO in place of commented segment: UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO/*self.isOpaque*/, 0.0f); – ancajic Dec 03 '13 at 16:15
  • 2
    How to drag the cells when the cells are of different sizes? – Imran Mar 13 '14 at 13:39
  • that is awesome, thanks! All i had to do is change Collection View Flow Layout class LXReorderableCollectionViewFlowLayout and all just worked. I can't believe it. – user1244109 Jul 17 '15 at 10:59
47

Here is an alternative:

The differences between DraggableCollectionView and LXReorderableCollectionViewFlowLayout are:

  • The data source is only changed once. This means that while the user is dragging an item the cells are re-positioned without modifying the data source.
  • It's written in such a way that makes it possible to use with custom layouts.
  • It uses a CADisplayLink for smooth scrolling and animation.
  • Animations are canceled less frequently while dragging. It feels more "natural".
  • The protocol extends UICollectionViewDataSource with methods similar to UITableViewDataSource.

It's a work in progress. Multiple sections are now supported.

To use it with a custom layout see DraggableCollectionViewFlowLayout. Most of the logic exists in LSCollectionViewLayoutHelper. There is also an example in CircleLayoutDemo showing how to make Apple's CircleLayout example from WWDC 2012 work.

Jagat Dave
  • 1,633
  • 3
  • 22
  • 30
Luke
  • 11,790
  • 6
  • 40
  • 78
  • The DraggableCollectionView seems perfect for me but I can not see how it is implemented in the Demo (namely the Delegate and Datasource are not set), are there any more examples? – Recycled Steel Sep 09 '13 at 12:57
  • Figured it out, needed to set the Collection View flow to the custom class DraggableCollectionViewFlow – Recycled Steel Sep 09 '13 at 13:05
  • This is perfect and easy if you're looking for something basic just to add draggable functionality. – Hayro Oct 06 '13 at 18:18
  • Its working well, although you need to maintain a fixed cell size. (And I'm still working on how to do a horizontal UICollectionView with it) – Peter Johnson Feb 07 '14 at 00:56
  • @PeterJohnson That's the unfortunate side-effect of doing it post-calculation. I have an experimental rewrite that solves this issue. Due to lack of time, I've pushed what I have so far, and I welcome any help. See https://github.com/lukescott/DraggableCollectionView/issues/30 – Luke Feb 12 '14 at 18:53
  • How to drag the cells when the cells are of different sizes? – Imran Mar 13 '14 at 13:38
  • I am finding that the collection view is retained. – Morkrom Aug 04 '14 at 22:22
  • To set custom layout in story board, select the layout "Collection View Flow Layout" and change the class to "DraggableCollectionViewFlowLayout" – david72 Mar 24 '15 at 18:48
31

As of iOS 9, UICollectionView now supports reordering.

For UICollectionViewControllers, just override collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)

For UICollectionViews, you'll have to handle the gestures yourself in addition to implementing the UICollectionViewDataSource method above.

Here's the code from the source:

private var longPressGesture: UILongPressGestureRecognizer!

override func viewDidLoad() {
    super.viewDidLoad()

    longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
    self.collectionView.addGestureRecognizer(longPressGesture)
}

func handleLongGesture(gesture: UILongPressGestureRecognizer) {

    switch(gesture.state) {

    case UIGestureRecognizerState.Began:
        guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
            break
        }
        collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
    case UIGestureRecognizerState.Changed:
        collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
    case UIGestureRecognizerState.Ended:
        collectionView.endInteractiveMovement()
    default:
        collectionView.cancelInteractiveMovement()
    }
}

Sources: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionView_class/#//apple_ref/doc/uid/TP40012177-CH1-SW67

http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/

chrisamanse
  • 3,933
  • 1
  • 21
  • 32
3

If you want to experiment rolling out your own, I just wrote a Swift based tutorial you can look. I tried to build the most basic of cases so as to be easier to follow this.

Jagat Dave
  • 1,633
  • 3
  • 22
  • 30
Mike M
  • 4,447
  • 2
  • 33
  • 49
2

Here is another approach:

Key difference is that this solution does not require a "ghost" or "dummy" cell to provide the drag and drop functionality. It simply uses the cell itself. Animations are in line with UITableView. It works by adjusting the collection view layout's private datasource while moving around. Once you let go, it will tell your controller that you can commit the change to your own datasource.

I believe it's a bit simpler to work with for most use cases. Still a work in progress, but yet another way to accomplish this. Most should find this pretty easy to incorporate into their own custom UICollectionViewLayouts.

Jagat Dave
  • 1,633
  • 3
  • 22
  • 30
Henry T Kirk
  • 961
  • 9
  • 13