Ответ 1
Мне удалось достичь этого, создав подкласс UICollectionView
и добавив настраиваемую обработку для интерактивного перемещения. Рассматривая возможные подсказки о том, как решить вашу проблему, я нашел этот учебник: http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/.
Важнейшей частью было то, что интерактивное переупорядочение может быть сделано не только на UICollectionViewController
. Соответствующий код выглядит следующим образом:
var longPressGesture : UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
// rest of setup
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.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()
}
}
Это должно быть внутри вашего контроллера представлений, в котором находится ваш просмотр коллекции. Я не знаю, будет ли это работать с UICollectionViewController
, может потребоваться дополнительное вмешательство. Что привело меня к подклассу UICollectionView
, было осознание того, что всем другим связанным классам/методам делегатов сообщается только о первом и последнем пути указателя (т.е. О источнике и получателе), и нет информации обо всех остальных ячейках, которые были перегруппированы, поэтому его нужно было остановить в ядре.
SwappingCollectionView.swift:
import UIKit
extension UIView {
func snapshot() -> UIImage {
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
extension CGPoint {
func distanceToPoint(p:CGPoint) -> CGFloat {
return sqrt(pow((p.x - x), 2) + pow((p.y - y), 2))
}
}
struct SwapDescription : Hashable {
var firstItem : Int
var secondItem : Int
var hashValue: Int {
get {
return (firstItem * 10) + secondItem
}
}
}
func ==(lhs: SwapDescription, rhs: SwapDescription) -> Bool {
return lhs.firstItem == rhs.firstItem && lhs.secondItem == rhs.secondItem
}
class SwappingCollectionView: UICollectionView {
var interactiveIndexPath : NSIndexPath?
var interactiveView : UIView?
var interactiveCell : UICollectionViewCell?
var swapSet : Set<SwapDescription> = Set()
var previousPoint : CGPoint?
static let distanceDelta:CGFloat = 2 // adjust as needed
override func beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool {
self.interactiveIndexPath = indexPath
self.interactiveCell = self.cellForItemAtIndexPath(indexPath)
self.interactiveView = UIImageView(image: self.interactiveCell?.snapshot())
self.interactiveView?.frame = self.interactiveCell!.frame
self.addSubview(self.interactiveView!)
self.bringSubviewToFront(self.interactiveView!)
self.interactiveCell?.hidden = true
return true
}
override func updateInteractiveMovementTargetPosition(targetPosition: CGPoint) {
if (self.shouldSwap(targetPosition)) {
if let hoverIndexPath = self.indexPathForItemAtPoint(targetPosition), let interactiveIndexPath = self.interactiveIndexPath {
let swapDescription = SwapDescription(firstItem: interactiveIndexPath.item, secondItem: hoverIndexPath.item)
if (!self.swapSet.contains(swapDescription)) {
self.swapSet.insert(swapDescription)
self.performBatchUpdates({
self.moveItemAtIndexPath(interactiveIndexPath, toIndexPath: hoverIndexPath)
self.moveItemAtIndexPath(hoverIndexPath, toIndexPath: interactiveIndexPath)
}, completion: {(finished) in
self.swapSet.remove(swapDescription)
self.dataSource?.collectionView(self, moveItemAtIndexPath: interactiveIndexPath, toIndexPath: hoverIndexPath)
self.interactiveIndexPath = hoverIndexPath
})
}
}
}
self.interactiveView?.center = targetPosition
self.previousPoint = targetPosition
}
override func endInteractiveMovement() {
self.cleanup()
}
override func cancelInteractiveMovement() {
self.cleanup()
}
func cleanup() {
self.interactiveCell?.hidden = false
self.interactiveView?.removeFromSuperview()
self.interactiveView = nil
self.interactiveCell = nil
self.interactiveIndexPath = nil
self.previousPoint = nil
self.swapSet.removeAll()
}
func shouldSwap(newPoint: CGPoint) -> Bool {
if let previousPoint = self.previousPoint {
let distance = previousPoint.distanceToPoint(newPoint)
return distance < SwappingCollectionView.distanceDelta
}
return false
}
}
Я понимаю, что там много чего происходит, но я надеюсь, что все будет ясно через минуту.
- Расширение на
UIView
с помощью вспомогательного метода для получения моментального снимка ячейки. - Расширение на
CGPoint
со вспомогательным методом для вычисления расстояния между двумя точками. -
SwapDescription
вспомогательная структура - это необходимо для предотвращения множественных свопов одной и той же пары элементов, что привело к глючной анимации. Его методhashValue
можно было бы улучшить, но он был достаточно хорош для этого доказательства концепции. -
beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool
- метод, вызываемый при начале движения. Здесь все наладится. Мы получаем снимок нашей ячейки и добавляем ее как подвью - этот снимок будет тем, что пользователь фактически тащит на экране. Сама ячейка скрывается. Если вы вернетеfalse
из этого метода, интерактивное движение не начнется. -
updateInteractiveMovementTargetPosition(targetPosition: CGPoint)
- метод, вызываемый после каждого пользовательского движения, что много. Мы проверяем, достаточно ли расстояния от предыдущей точки для замены элементов - это предотвращает проблему, когда пользователь будет быстро перетаскивать по экрану, и несколько элементов будут заменены неочевидными результатами. Если обмен может произойти, мы проверяем, если это уже происходит, и если мы не поменяем два элемента. -
endInteractiveMovement()
,cancelInteractiveMovement()
,cleanup()
- после окончания движения нам нужно восстановить наши помощники в их состояние по умолчанию. -
shouldSwap(newPoint: CGPoint) -> Bool
- вспомогательный метод, чтобы проверить, достаточно ли движение достаточно, чтобы мы могли менять ячейки.
Это результат:
Сообщите мне, если это то, что вам нужно, и/или если вам нужно что-то прояснить.