UICollectionView 快速拖放

Posted

技术标签:

【中文标题】UICollectionView 快速拖放【英文标题】:UICollectionView Drag-N-Drop at swift 【发布时间】:2019-10-11 06:19:33 【问题描述】:

我正在快速使用 UICollectionView 制作一个项目,我需要对每个框执行拖放操作并交换它们的位置。每个盒子都有不同的数据和不同的大小,因此用户需要操纵他们的布局,因为我需要让用户可以拖放这些视图。我写了一个代码,但是当我开始拖放时,事件将传输他们的数据而不是盒子本身。我该如何解决这个问题?

    import UIKit

    class ViewController: UIViewController 

        var cellIds = ["1","2","3"]

            @IBOutlet weak var collectionView: UICollectionView!
          fileprivate var longPressGesture =  UILongPressGestureRecognizer();

        let cellSizes = [
            CGSize(width:190, height:200),
             CGSize(width:190, height:200),
              CGSize(width:190, height:200),

        ]

        @objc func longTap(_ gesture: UIGestureRecognizer)

            switch(gesture.state) 
            case .began:
                guard let selectedIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) else 
                    return
                
                collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
            case .changed:
                collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
            case .ended:
                collectionView.endInteractiveMovement()
                //doneBtn.isHidden = false
                //longPressedEnabled = true
                self.collectionView.reloadData()
            default:
                collectionView.cancelInteractiveMovement()
            
        

        override func viewDidLoad() 
            super.viewDidLoad()


            longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.longTap(_:)))
            collectionView.addGestureRecognizer(longPressGesture)


            collectionView!.register(UICustomCollectionViewCell.self, forCellWithReuseIdentifier: "MenuCell")

            self.automaticallyAdjustsScrollViewInsets = false
        

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets 
            //top, left, bottom, right
            return UIEdgeInsets(top: 130, left: 1, bottom: 0, right: 10)
        

    

    extension ViewController: UICollectionViewDelegate 
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) 
           // print("User tapped on \(cellIds[indexPath.row])")
        

        func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool 
            return true
        


         func userDidEndDragging(_ cell: UICollectionViewCell?) 


         




        func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 
            let item = cellIds.remove(at: sourceIndexPath.item)
            cellIds.insert(item, at: destinationIndexPath.item)
            print(cellIds);
        
     



    extension ViewController: UICollectionViewDataSource 
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
            return cellIds.count
        

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MenuCell", for: indexPath) as? UICustomCollectionViewCell

            cell!.layer.cornerRadius=10
            cell!.layer.masksToBounds=true

             var id = cellIds[indexPath.row];

            if (id == "1")
            

              cell?.lblTest1.text = "amsterdam";
                cell?.lblTest1.tag = 100
            
            if (id == "2")
            
                  cell?.lblTest2.text = "madrid";
                  cell?.lblTest2.tag = 101

            

            if (id == "3")
            
                 cell?.lblTest3.text = "istanbul";
                 cell?.lblTest3.tag = 102
            

            return cell ?? UICollectionViewCell()

         
    

    extension ViewController: UICollectionViewDelegateFlowLayout 
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 
            return cellSizes[indexPath.item]
        
    

import UIKit
class UICustomCollectionViewCell: UICollectionViewCell 

required init?(coder aDecoder: NSCoder) 
    fatalError("init(coder:) has not been implemented")


override func prepareForReuse() 
    super.prepareForReuse()


override init(frame: CGRect) 
    super.init(frame: frame)
       self.addSubview(lblTest1)
      self.addSubview(lblTest2)
      self.addSubview(lblTest3)


var lblTest1:UILabel = 
    let label1 = UILabel(frame: CGRect(x:10, y: 20, width: 90 , height: 40))
    label1.lineBreakMode = .byWordWrapping
    label1.numberOfLines = 0
    label1.tag = 100;
    label1.tintColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
    return label1
()


var lblTest2:UILabel = 
    let label2 = UILabel(frame: CGRect(x:10, y: 20, width: 90 , height: 40))
    label2.lineBreakMode = .byWordWrapping
    label2.numberOfLines = 0
    label2.tag = 101;
    label2.tintColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
    return label2
()


var lblTest3:UILabel = 
    let label3 = UILabel(frame: CGRect(x:10, y: 20, width: 90 , height: 40))
    label3.lineBreakMode = .byWordWrapping
    label3.numberOfLines = 0
    label3.tag = 102;
    label3.tintColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
    return label3
()

override func layoutSubviews() 
    super.layoutSubviews()


【问题讨论】:

发布您的代码。可见的代码比文字更清楚)) 我已经编辑了我的帖子 我建议你看看这个resource。这里不是“1个简单的答案”,而是教你如何制作自定义的“DragDropFlowLayout”。我实现了这个资源的拖放流布局示例,并使用了 collectionView 中的标准方法来实现美观流畅的“拖放”功能。您将需要更多的自定义,因为您说过要拖放不同大小的单元格。祝你好运! 你能把你的 Swift4 代码发给我们吗?你能帮我们吗? 【参考方案1】:

这是我如何实现优雅的拖放 collectionView:

我有 collectionView 的班级
class FavoritesVC: UIViewController 

    //MARK: IBOutlets
    @IBOutlet weak var collectionView: UICollectionView!

    //MARK: View Model
    let viewModel = FavoritesViewModel()

    //MARK: Properties
    lazy var dragDropFlowLayout: UICollectionViewLayout = 
        let flow = DragDropFlowLayout()
        flow.scrollDirection = .vertical
        flow.headerReferenceSize = CGSize(width: collectionView.frame.width, height: 0.5)
        return flow
    ()

    //MARK: Lifecycle
    override func viewDidLoad() 
        super.viewDidLoad()

        setupCollectionView()
        viewModel.competitionsUpdateCallback = 
            self.collectionView.reloadData()
        
    

    //MARK: Private functions
    private func setupCollectionView() 

        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.collectionViewLayout = dragDropFlowLayout
    


//MARK: CollectionView DataSource
extension FavoritesVC: UICollectionViewDataSource 

    ///...methods for the collectionViewDatasource which aren't relevant here

    func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool 

        return true
    

    func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destination: IndexPath) 

        //save the new order of the objects in the data
        ...
    


//MARK: CollectionView Flow Layout Delegate
extension FavoritesVC: UICollectionViewDelegateFlowLayout 

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 

        let size = CGSize(width: collectionView.frame.width / 2 - 15, height: 150)
        return size
    

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat 

        return 10
    

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat 

        return 10
    

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets 

        return UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10)
    

拖放自定义流布局
class DragDropFlowLayout: UICollectionViewFlowLayout 

    var longPress: UILongPressGestureRecognizer!
    var originalIndexPath: IndexPath?
    var draggingIndexPath: IndexPath?
    var draggingView: UIView?
    var dragOffset = CGPoint.zero

    override func prepare() 
        super.prepare()

        installGestureRecognizer()
    

    func applyDraggingAttributes(attributes: UICollectionViewLayoutAttributes) 
        attributes.alpha = 0
    

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 
        let attributes = super.layoutAttributesForElements(in: rect)
        attributes?.forEach  a in
            if a.indexPath == draggingIndexPath 
                if a.representedElementCategory == .cell 
                    self.applyDraggingAttributes(attributes: a)
                
            
        
        return attributes
    

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? 
        let attributes = super.layoutAttributesForItem(at: indexPath)
        if let attributes = attributes, indexPath == draggingIndexPath 
            if attributes.representedElementCategory == .cell 
                applyDraggingAttributes(attributes: attributes)
            
        
        return attributes
    

    func installGestureRecognizer() 
        if longPress == nil 
            longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
            longPress.minimumPressDuration = 0.2
            collectionView?.addGestureRecognizer(longPress)
        
    

    @objc func handleLongPress(_ longPress: UILongPressGestureRecognizer) 
        let location = longPress.location(in: collectionView!)
        switch longPress.state 
        case .began: startDragAtLocation(location: location)
        case .changed: updateDragAtLocation(location: location)
        case .ended: endDragAtLocation(location: location)
        default:
            break
        
    

    func startDragAtLocation(location: CGPoint) 
        guard let cv = collectionView else  return 
        guard let indexPath = cv.indexPathForItem(at: location) else  return 
        guard cv.dataSource?.collectionView?(cv, canMoveItemAt: indexPath) == true else  return 
        guard let cell = cv.cellForItem(at: indexPath) else  return 

        originalIndexPath = indexPath
        draggingIndexPath = indexPath
        draggingView = cell.snapshotView(afterScreenUpdates: true)
        draggingView!.frame = cell.frame
        cv.addSubview(draggingView!)

        dragOffset = CGPoint(x: draggingView!.center.x - location.x, y: draggingView!.center.y - location.y)

        draggingView?.layer.shadowPath = UIBezierPath(rect: draggingView!.bounds).cgPath
        draggingView?.layer.shadowColor = UIColor.black.cgColor
        draggingView?.layer.shadowOpacity = 0.8
        draggingView?.layer.shadowRadius = 10

        invalidateLayout()

        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.55, initialSpringVelocity: 0, options: [], animations: 
            self.draggingView?.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
        , completion: nil)
    

    func updateDragAtLocation(location: CGPoint) 
        guard let view = draggingView else  return 
        guard let cv = collectionView else  return 

        view.center = CGPoint(x: location.x + dragOffset.x, y: location.y + dragOffset.y)

        if let newIndexPath = cv.indexPathForItem(at: location) 
            cv.moveItem(at: draggingIndexPath!, to: newIndexPath)
            draggingIndexPath = newIndexPath
        
    

    func endDragAtLocation(location: CGPoint) 
        guard let dragView = draggingView else  return 
        guard let indexPath = draggingIndexPath else  return 
        guard let cv = collectionView else  return 
        guard let datasource = cv.dataSource else  return 

        let targetCenter = datasource.collectionView(cv, cellForItemAt: indexPath).center

        UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.55, initialSpringVelocity: 0, options: [], animations: 
            dragView.center = targetCenter
            dragView.transform = .identity
            dragView.layer.shadowColor = UIColor.white.cgColor
            dragView.layer.shadowOpacity = 0
            dragView.layer.shadowRadius = 0

        )  (completed) in

            if indexPath != self.originalIndexPath! 
                datasource.collectionView?(cv, moveItemAt: self.originalIndexPath!, to: indexPath)
            

            dragView.removeFromSuperview()
            self.draggingIndexPath = nil
            self.draggingView = nil
            self.invalidateLayout()
        
    

【讨论】:

以上是关于UICollectionView 快速拖放的主要内容,如果未能解决你的问题,请参考以下文章

拖放 UICollectionView 单元格重用问题

UICollectionView 拖放单元格

collectionView代理方法快速设置cell大小上下左右间隔

在两个 UICollectionView 之间拖放

在具有 UITableView 的 UICollectionView 中拖放

UICollectionView 与 SwiftUI + 拖放重新排序可能吗?