UICollectionViews Now Have Easy Reordering

0x00 前言

这篇文章是 NSHint 上的 UICollectionViews Now Have Easy Reordering。你可以直接去看英文的原文。

0x01 正文

哥是 UICollectionView 的头号大粉丝,它比它的老大哥 UITableView 更加的可定制化,现在哥经常使用 collection view 而不使用 table view。而且 iOS 9 也支持简单重排了。在这之前不太可能实现,并且要实现意味着这是一件非常痛苦的事情。让我们一起来瞧一瞧这个 API。你可以在 GitHub 上找到这个 Xcode 项目。

使用 UICollectionViewController 是做简单重排的最简单的方法,现在,它多了一个新的属性 installsStandardGestureForInteractiveMovement,增加了重排 cells 的标准手势。这个属性默认为 true,这意味着我们只需要重载一个方法就可以完成我们想要的功能了,真是太赞了!

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

因为 moveItemAtIndexPath 被重载了,所以 collection view 能推断出它的 items 能够移动。

当我们想要在一个简单的 UIViewController 中使用 collection view,事情会变得非常繁琐。我们依然需要实现上述的 UICollectionViewDataSource 的几个方法,但是我们需要重写 installsStandardGestureForInteractiveMovement,不用担心,它也非常容易支持的。supported.UILongPressGestureRecognizer 是一个连续的手势识别,并且完全支持 panning。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()
}
}

我们保存了在长按手势 handler 中获得的选择的 item 的下标,and depending on wether it has any value we allow to pan gesture to kick in(这里的 wether 应该是 whether 吧)。然后,我们根据手势的状态调用一些 collection view 新的方法:

  • beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) 在特定的 index path 开始 cell 的交互移动动画
  • updateInteractiveMovementTargetPosition(targetPosition: CGPoint) 在手势的过程中更新目标位置的交互移动动画
  • endInteractiveMovement() 在你结束 pan 手势之后结束交互移动动画
  • cancelInteractiveMovement() 取消交互移动动画

这让处理 pan 手势显而易见。

这行为和标准的 UICollectionViewController 是一样的。非常 cool,不过,我们可以用我们自己定制的 collection view layout 来将 collection view 的重排实现得更 cool!先来看看简单的瀑布流布局的交互移动动画。

嗯哼,看起来很 cool,但是如果我们不想在移动的过程中改变 cell 的大小呢?在交互移动动画的过程中,选择的 cell 的大小应该保持不变,这是可能的。UICollectionViewLayout 也有另外的方法来处理重排。

1
2
3
4
5
6
7
8
func invalidationContextForInteractivelyMovingItems(targetIndexPaths: [NSIndexPath],
withTargetPosition targetPosition: CGPoint,
previousIndexPaths: [NSIndexPath],
previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext

func invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths(indexPaths: [NSIndexPath],
previousIndexPaths: [NSIndexPath],
movementCancelled: Bool) -> UICollectionViewLayoutInvalidationContext

前一个方法是在目标 IndexPath 与之前的 cell 的 indexPath 的 cells 交互移动动画中被调用的,后一个方法也是类似的,但是它只在交互移动动画结束之后被调用。有了这些知识,我们就可以使用一个小技巧来实现我们的需求了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 的之前和目标的 index path,然后调用 UICollectionViewDataSource 的方法来移动 items。

毫无疑问,一个 collection view 重排是一个非常神奇的 addition。UIKit 工程师们,干得漂亮!:)

P.S: 非常感谢 Douglas Hill 在我们的代码中提出的一些改进,谢谢你!继续保持!