UICollectionViews Now Have Easy Reordering (译)

iOS 9.0 UICollectionView 拖拽效果实现

原文 UICollectionViews Now Have Easy Reordering

UICollectionView 刚出来的时候,我就对其产生了很大的兴趣。相比它的大哥 UITableView,它更加容易进行一些自定义的操作。我们组现在在项目中使用 UICollectionView 要多于 UITableView。伴随着 iOS 9 的 release,UICollectionView排序(动态拖拽)更加简单。在这之前,如果想对 UICollectionView 进行动态拖拽是非常困难的,要想实现动态拖拽,需要去做非常多的工作。话不多说,我们看一下新的 API。文章用到的Sample地址uicollectionview-reordering

实现拖动排序最简单的办法是使用 UICollectionViewController, 在 UICollectionViewController 中新增了一个属性 installsStandardGestureForInteractiveMovement, 通过添加手势对cells进行重排。该属性是BOOL型,默认值为 YES。 我们所需要做的,只要重载下面这个方法就好了.

override func collectionView(collectionView: UICollectionView,
    moveItemAtIndexPath sourceIndexPath: NSIndexPath,
    toIndexPath destinationIndexPath: NSIndexPath) {
    // move your data order
}

当程序中重载了moveItemAtIndexPath,collectionView 就认为 cell 是可以移动的。

![reordering-01](http://nshint.io/images/uicollectionview-reordering/1.gif)

那如果需要给某一个 UIViewController 中 collection view 实现动态拖动效果,该如何实现呢?事情会变的稍微复杂一点。除了需要实现UICollectionViewDataSource中上面提到的代理方法,还需要重写 installsStandardGestureForInteractiveMovement。不过不用当心,实现起来蛮蛮容易。这里我们需要长按手势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()
    }
}

这段代码主要是给 collectionView 添加一个长手势识别器,并根据手势的不同状态调用collectionView的相关方法。

  • beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath): 该方法在开始拖拽某个cell时被调用
  • updateInteractiveMovementTargetPosition(targetPosition:CGPoint): 根据手势更新拖拽cell的位置
  • endInteractiveMovement(): 手势结束时调用,结束拖拽
  • cancelInteractiveMovement(): 手势取消时调用,取消拖拽

这样就实现了需要的拖拽效果:

reordering_collection_view

这段代码出来的效果和 UICollectionViewController 是一致的。很厉害吧,但更厉害的是我们可以用上面的方法对自定义的 collection view layout 进行拖拽。下面我们来实现一个简单的瀑布流。

reordering_waterfall

昂~,看起来还凑合,但在移动的时候cell的size被改变了呢,如何才能保持cell的size不变呢? UICollectionViewLayout提供了相关办法可以帮助我们解决这个问题。

func invalidationContextForInteractivelyMovingItems(targetIndexPaths: [NSIndexPath],
    withTargetPosition targetPosition: CGPoint,
    previousIndexPaths: [NSIndexPath],
    previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext

func invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths(indexPaths: [NSIndexPath],
    previousIndexPaths: [NSIndexPath],
    movementCancelled: Bool) -> UICollectionViewLayoutInvalidationContext
  • 第一个函数会在 cells 拖拽过程中被调用
  • 第二个函数会在拖拽结束时候被调用。有了这些信息,我们可以使用一点小技巧去实现cell size不被改变的功能。

    internal override func invalidationContextForInteractivelyMovingItems(targetIndexPaths: [NSIndexPath],

    withTargetPosition targetPosition: CGPoint,
    previousIndexPaths: [NSIndexPath],
    previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext {
    
    var context = super.invalidationContextForInteractivelyMovingItems(targetIndexPaths,
        withTargetPosition: targetPosition, previousIndexPaths: previousIndexPaths,
        previousPosition: previousPosition)
    
    self.delegate?.collectionView!(self.collectionView!, moveItemAtIndexPath: previousIndexPaths[0],
        toIndexPath: targetIndexPaths[0])
    
    return context
    

    }

解决方法很直接。获取当前被拖拽的cell的起始indexPath和目标indexPath,然后调用UICollectionViewDataSource代理方法移动当前正在被拖拽的cell。

一个可以拖拽的的collectionView带来体验效果真的非常棒~