web-dev-qa-db-ja.com

CollectionViewでのカスタムセルの並べ替え動作

私はcollectionViewを次のように並べ替えることができます:

enter image description here

ただし、すべてのセルが水平方向にシフトするのではなく、次の動作(つまり、セルのシャッフルが少ない)に交換したいと思います。

enter image description here

私は次のデリゲートメソッドで遊んでいます。

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

ただし、カスタムの並べ替え動作を実現する方法がわかりません。

16
vikzilla

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. 2点間の距離を計算するヘルパーメソッドを使用したCGPointの拡張。
  3. SwapDescriptionヘルパー構造-同じアイテムのペアの複数のスワップを防止する必要があり、その結果、アニメーションがグリッチになりました。そのhashValueメソッドは改善される可能性がありますが、この概念実証のためには十分です。
  4. beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool-移動が始まったときに呼び出されるメソッド。ここですべてが設定されます。セルのスナップショットを取得してサブビューとして追加します。このスナップショットは、ユーザーが実際に画面にドラッグしたものになります。セル自体が非表示になります。このメソッドからfalseを返すと、インタラクティブな動きは開始されません。
  5. updateInteractiveMovementTargetPosition(targetPosition: CGPoint)-ユーザーが移動するたびに呼び出されるメソッドです。前のポイントからの距離がアイテムを交換するのに十分小さいかどうかを確認します。これにより、ユーザーが画面上を高速でドラッグし、複数のアイテムが交換されて、明白でない結果が生じる場合の問題が回避されます。スワップが発生する可能性がある場合は、既に発生しているかどうかを確認し、発生していない場合は2つのアイテムを交換します。
  6. endInteractiveMovement()cancelInteractiveMovement()cleanup()-移動が終了したら、ヘルパーをデフォルトの状態に戻す必要があります。
  7. shouldSwap(newPoint: CGPoint) -> Bool-セルを交換できるように動きが十分に小さいかどうかを確認するヘルパーメソッド。

これはそれが与える結果です:

result

これがあなたが必要とするものであるかどうか、および/または何かを明確にする必要があるかどうかを知らせてください。

これが デモプロジェクト です。

37
Losiowaty

@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
    }
}
0
inexcitus