Kuroiteiken
Kuroiteiken

Reputation: 308

UICollectionView Drag-N-Drop at swift

I'm making a project with UICollectionView at swift, I need to execute drag-and-drop with each boxes and swap their position. Each boxes has different data and different size, therefore user need to manipulate their layout, for that i need to enable users can drag-n-drop these Views. I wrote a code but when i start drag-n-drop, event will transfer their data instead of boxes itself. How can i solve this?

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

Upvotes: -1

Views: 859

Answers (1)

Starsky
Starsky

Reputation: 2048

Here is how I achieved an elegant drag & drop collectionView:

  • My class where I have the 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 = { [weak self] in
            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)
    }
}
  • The Drag&Drop Custom Flow Layout
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()
        }
    }
}

Upvotes: 0

Related Questions