Comportamiento de reorder de celda personalizado en CollectionView

Puedo reorderar mi colecciónView así:

introduzca la descripción de la imagen aquí

Sin embargo, en lugar de cambiar todas las celdas horizontalmente, simplemente me gustaría intercambiar con el siguiente comportamiento (es decir, con less shuffling de celdas):

introduzca la descripción de la imagen aquí

He estado jugando con el siguiente método de delegado:

func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath 

Sin embargo, no estoy seguro de cómo puedo lograr un comportamiento de reorderamiento personalizado.

UICollectionView lograr esto creando una subclass de UICollectionView y agregando un manejo personalizado al movimiento interactivo. Mientras miro posibles pistas sobre cómo resolver su problema, he encontrado este tutorial: http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/ . La parte más importante allí fue que la reorderación interactiva se puede hacer no solo en UICollectionViewController . El código relevante se ve así:

 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() } } 

Esto debe estar dentro del controller de vista en el que se coloca la vista de colección. No sé si esto funcionará con UICollectionViewController , es posible que se necesiten algunos retoques adicionales. Lo que me llevó a subclasificar UICollectionView fue la realización de que todos los demás methods de classs / delegates relacionados se informan solo sobre la primera y la última UICollectionView índice (es decir, la fuente y el destino), y no hay información sobre todas las otras celdas que se reorderaron, por lo que necesitaba detenerse en el núcleo.

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((px - x), 2) + pow((py - 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 } } 

Me doy count de que hay mucho que hacer allí, pero espero que todo esté claro en un minuto.

  1. Extensión en UIView con el método helper para get una instantánea de una celda.
  2. Extensión en CGPoint con el método helper para calcular la distancia entre dos puntos.
  3. SwapDescription auxiliar de SwapDescription : es necesaria para evitar intercambios múltiples del mismo par de elementos, lo que resultó en animaciones glitchy. Su método hashValue podría mejorarse, pero fue lo suficientemente bueno para esta testing de concepto.
  4. beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool – el método llamado cuando comienza el movimiento. Todo se configura aquí. Obtenemos una instantánea de nuestra celda y la agregamos como una subvista: esta instantánea será lo que el usuario realmente arrastra en la pantalla. La celda se oculta. Si devuelve false de este método, el movimiento interactivo no comenzará.
  5. updateInteractiveMovementTargetPosition(targetPosition: CGPoint) : método llamado después de cada movimiento de usuario, que es mucho. Verificamos si la distancia desde el punto anterior es lo suficientemente pequeña como para intercambiar elementos: esto evita el problema cuando el usuario arrastrará rápidamente a través de la pantalla y se intercambiarán múltiples elementos con resultados no obvios. Si el intercambio puede suceder, verificamos si ya está sucediendo y, si no, intercambiamos dos elementos.
  6. endInteractiveMovement() , cancelInteractiveMovement() , cleanup() – después de que finaliza el movimiento, necesitamos restaurar a nuestros ayudantes a su estado pnetworkingeterminado.
  7. shouldSwap(newPoint: CGPoint) -> Bool – método de ayuda para comprobar si el movimiento era lo suficientemente pequeño para que podamos cambiar las celdas.

Este es el resultado que da:

resultado

Avíseme si esto es lo que necesitaba y / o si necesita que aclare algo.

Aquí hay un proyecto de demostración .