CollectionView中的自定义单元格重新排序行为

时间:2016-08-26 18:01:06

标签: ios swift uicollectionview

我可以像这样重新排序我的collectionView:

enter image description here

然而,我不想将所有细胞水平移动,而是只想交换以下行为(即细胞洗牌次数减少):

enter image description here

我一直在使用以下委托方法:

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

但是,我不确定如何实现自定义重新排序行为。

3 个答案:

答案 0 :(得分:35)

我设法通过创建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
    }
}

我确实意识到那里有很多事情发生,但我希望一切都会在一分钟内明确。

  1. 使用辅助方法扩展UIView以获取单元格的快照。
  2. 使用辅助方法对CGPoint进行扩展,以计算两点之间的距离。
  3. SwapDescription辅助结构 - 需要防止同一对项目的多次交换,这会导致毛刺剧情。它的hashValue方法可以改进,但为了这个概念证明就足够了。
  4. beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool - 移动开始时调用的方法。一切都在这里设置。我们获取单元格的快照并将其添加为子视图 - 此快照将是用户实际在屏幕上拖动的内容。细胞本身被隐藏起来。如果从此方法返回false,则不会开始交互式移动。
  5. updateInteractiveMovementTargetPosition(targetPosition: CGPoint) - 在每次用户移动后调用的方法,这很多。我们检查前一点的距离是否足够小以交换项目 - 这可以防止用户在屏幕上快速拖动并且多个项目与非显而易见的结果交换时出现问题。如果交换可能发生,我们检查它是否已经发生,如果不是,我们交换两个项目。
  6. endInteractiveMovement()cancelInteractiveMovement()cleanup() - 在移动结束后,我们需要将帮助程序恢复为默认状态。
  7. shouldSwap(newPoint: CGPoint) -> Bool - 辅助方法,检查运动是否足够小,以便我们可以交换细胞。
  8. 这是它给出的结果:

    result

    请告诉我这是否是您需要的和/或您是否需要我澄清一些内容。

    这是demo project

答案 1 :(得分:0)

@Losiowaty解决方案的Swift 5解决方案:

var longPressGesture : UILongPressGestureRecognizer!

override func viewDidLoad()
{
    super.viewDidLoad()

    // rest of setup
    longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongGesture))
    self.collectionView?.addGestureRecognizer(longPressGesture)
}

@objc func handleLongGesture(gesture: UILongPressGestureRecognizer)
{
    switch(gesture.state)
    {
    case UIGestureRecognizerState.began:
        guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
            break
        }
        collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
    case UIGestureRecognizerState.changed:
        collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
    case UIGestureRecognizerState.ended:
        collectionView?.endInteractiveMovement()
    default:
        collectionView?.cancelInteractiveMovement()
    }
}


import UIKit

extension UIView {
    func snapshot() -> UIImage {
        UIGraphicsBeginImageContext(self.bounds.size)
        self.layer.render(in: 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 : IndexPath?
    var interactiveView : UIView?
    var interactiveCell : UICollectionViewCell?
    var swapSet : Set<SwapDescription> = Set()
    var previousPoint : CGPoint?

    static let distanceDelta:CGFloat = 2 // adjust as needed

    override func beginInteractiveMovementForItem(at indexPath: IndexPath) -> Bool
    {
        self.interactiveIndexPath = indexPath

        self.interactiveCell = self.cellForItem(at: indexPath)

        self.interactiveView = UIImageView(image: self.interactiveCell?.snapshot())
        self.interactiveView?.frame = self.interactiveCell!.frame

        self.addSubview(self.interactiveView!)
        self.bringSubviewToFront(self.interactiveView!)

        self.interactiveCell?.isHidden = true

        return true
    }

    override func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint) {

        if (self.shouldSwap(newPoint: targetPosition))
        {
            if let hoverIndexPath = self.indexPathForItem(at: 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.moveItem(at: interactiveIndexPath as IndexPath, to: hoverIndexPath)
                        self.moveItem(at: hoverIndexPath, to: interactiveIndexPath)
                        }, completion: {(finished) in
                            self.swapSet.remove(swapDescription)
                            self.dataSource?.collectionView?(self, moveItemAt: interactiveIndexPath, to: 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?.isHidden = 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(p: newPoint)
            return distance < SwappingCollectionView.distanceDelta
        }

        return false
    }
}

答案 2 :(得分:0)

如果您想跟踪像这种行为那样拖动的单元格数量:

@objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {

    switch gesture.state {
    case .began:
        guard let targetIndexPath = reorderCollectionView.indexPathForItem(at: gesture.location(in: reorderCollectionView)) else {
            return
        }
        reorderCollectionView.beginInteractiveMovementForItem(at: targetIndexPath)
   

    case .changed:
        reorderCollectionView.updateInteractiveMovementTargetPosition(gesture.location(in: reorderCollectionView))
        reorderCollectionView.performBatchUpdates {
            self.reorderCollectionView.visibleCells.compactMap { $0 as? ReorderCell}
                .enumerated()
                .forEach { (index, cell)  in
                    cell.countLabel.text = "\(index + 1)"
                }
        }
       
        
    case .ended:
        reorderCollectionView.endInteractiveMovement()
    default:
        reorderCollectionView.cancelInteractiveMovement()
    }

}