suvera-dev 🥦

iOS) CollectionView Cell - Drag & Drop 본문

iOS

iOS) CollectionView Cell - Drag & Drop

suvera 2022. 2. 1. 14:17

CollectionViewCell의 Drag & Drop 기능을 구현하여 Cell의 위치를 변경하는 방법을 알아보겠습니다 🍎

열심히 구글링을 한 결과 2가지 방법이 있었습니다 🧐

 

1. UICollectionViewDragDelegate , UICollectionViewDropDelegate 사용

2. LongPressGesture를 추가

 

어떤 방법으로 할까 하다가 저는 2가지 방법 모두 해봤습니다 ! 

 

 

1. Drag & Drop Delegate

collectionView.dragDelegate = self
collectionView.dropDelegate = self

- DragDelegate

extension ViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        return [UIDragItem(itemProvider: NSItemProvider())]
    }
}

- DropDelegate : CollectionView의 performBatchUpdates를 사용하여 순서 변경 구현 

extension ViewController: UICollectionViewDropDelegate {
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        if session.localDragSession != nil {
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        }
        return UICollectionViewDropProposal(operation: .cancel, intent: .unspecified)
    }
    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {

        guard let destinationIndexPath = coordinator.destinationIndexPath else {
            return
        }

        coordinator.items.forEach { dropItem in
            guard let sourceIndexPath = dropItem.sourceIndexPath else { return }
            let categoryCell = self.categoryList[sourceIndexPath.row]

            collectionView.performBatchUpdates({
                // reorder data list
                collectionView.deleteItems(at: [sourceIndexPath])
                collectionView.insertItems(at: [destinationIndexPath])
                self.categoryList.remove(at: sourceIndexPath.row)
                self.categoryList.insert(categoryCell, at: destinationIndexPath.row)
            }, completion: { _ in
                coordinator.drop(dropItem.dragItem, toItemAt: destinationIndexPath)
            })

        }
    }

- CollectionViewDataSource

 

1) canHandle은 true로 설정

2) moveItemAt 에서 move를 시작한 인덱스(sourceIndex)에 해당하는 셀을 저장한 뒤 기존 List에서 제거해주고, 저장해둔 셀은 move해서 도착한 인덱스(destintaionIndex)에 추가 해준다. 

    func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
        return true
    }

    func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let categoryCell = self.categoryList[sourceIndexPath.row]

        self.categoryList.remove(at: sourceIndexPath.row)
        self.categoryList.insert(categoryCell, at: destinationIndexPath.row)
    }

 

아래 사이트를 참고하였습니다 ! 

 

Swift drag, drop and reorder collectionView Cell

How to drag and drop collectionView Cell? You can use dragDelegate and dropDelegate! First, set delegate! Second, implement drag method! Note that replace item with your own cell item! Now you ca..

lemon-high.tistory.com

 

 

2. LongPressGesture 추가 

- LongPressGestureRecognizer를 추가 

private func setGesture() {
        let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:)))
        categoryCollectionView.addGestureRecognizer(longPressRecognizer)
}

 

- 제스처가 인식되었을 때 실행할 메서드 handleLongPressGesture 정의

    @objc
    private func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
        let startIndexPath = categoryCollectionView.indexPathForItem(at: gesture.location(in: categoryCollectionView))
        let cell = cellForItemAt(at: startIndexPath)
        switch gesture.state {
        case .began:
            guard let startIndexPath = startIndexPath else {
                break
            }
            cell?.backgroundColor = .purple002
            categoryCollectionView.beginInteractiveMovementForItem(at: startIndexPath)
        case .changed:
            categoryCollectionView.updateInteractiveMovementTargetPosition(gesture.location(in: categoryCollectionView))
        case .ended:
            cell?.backgroundColor = .purpleCategory
            categoryCollectionView.endInteractiveMovement()
        default:
            categoryCollectionView.cancelInteractiveMovement()
        }
    }
    
    private func cellForItemAt(at indexPath: IndexPath?) -> UICollectionViewCell? {
        guard let indexPath = indexPath else {
            return nil
        }
        return categoryCollectionView.cellForItem(at: indexPath)
    }

1) 제스처가 일어난 셀의 인덱스를 시작 인덱스로 설정

2) 제스처의 state에 따라 cell의 배경색 및 interactiveMovement 설정 

 

 

- CollectionView DataSource 

 

1) canMoveItemAt : true

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

2) moveItemAt : Drag를 시작한 인덱스(sourceIndexPath)에 해당하는 아이템을 기존 List에서 제거하고, Drop을 해서 도착한 인덱스 (destinationIndexPath)에 해당하는 아이템을 기존 List에 추가 (위의 delegate 와 방법 동일 !)

func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let cell = categoryCollectionView.cellForItem(at: destinationIndexPath)
        cell?.backgroundColor = .purpleCategory

        let categoryItem = categories.remove(at: sourceIndexPath.row)
        categories.insert(categoryItem, at: destinationIndexPath.row)
}

 

제가 했던 프로젝트에서는 결론적으로 2번을 사용했는데,

직접 여러가지를 테스트해본 결과 각각의 장단점이 존재하였습니다 🥸

 

 

1번째 방법은 기본으로 내장되어있는 효과를 더 쉽게 사용가능하는 점이 장점이 었습니다.

또한, 드래그 앤 드랍 인터랙션이 조금 더 자연스러웠습니다 ! 또한, 다른 제스처를 추가했을 경우 겹칠 위험이 없습니다. 

하지만, 드래그를 시작하였을 때 배경색을 바꾸는 방법을 찾지 못해 2번 방법으로 구현하게 되었습니다 ! 

 

2번째 방법은 제스처 변화 상태에 맞게 배경색을 커스텀 할수 있다는 장점이 있었습니다.

하지만, 드래그를하면서 양쪽 옆으로 과하게 벗어날 경우 indexPath가 nil로 감지되어 셀이 움직이다가 이상한 위치에서 멈추는 버그가 발생하였습니다. 따라서 아래의 기존 코드를 수정하여 indexPath가 nil일 때도 계속 flow 가 흘러가게 하고, cell을 optional로 반환해 사용하였습니다 ! 

// 기존 코드
guard let startIndexPath = categoryCollectionView.indexPathForItem(at: gesture.location(in: categoryCollectionView)) else {
            return
}
   
// 수정한 코드 
let startIndexPath = categoryCollectionView.indexPathForItem(at: gesture.location(in: categoryCollectionView))
let cell = cellForItemAt(at: startIndexPath)

 

 

 

 

 

 

 

참고한 사이트 

https://medium.com/hackernoon/how-to-drag-drop-uicollectionview-cells-by-utilizing-dropdelegate-and-dragdelegate-6e3512327202

 

How to Drag & Drop UICollectionView Cells by utilizing DropDelegate and DragDelegate

Think of an Instagram theme app where you can drag and drop your posts to create a clean feed before you post. If that doesn’t make sense…

medium.com

참고한 Youtube 영상 

https://www.youtube.com/watch?v=VrW_6EixIVQ

Comments