UICollectionViews Now Have Easy Reordering (译)
iOS 9.0 UICollectionView 拖拽效果实现
在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 是可以移动的。

那如果需要给某一个 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(): 手势取消时调用,取消拖拽
这样就实现了需要的拖拽效果:

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

昂~,看起来还凑合,但在移动的时候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带来体验效果真的非常棒~