Drag and drop data giữa nhiều UICollectionView với nhau (P1)
1. Drag and drop data trong UICollectionView Nếu chỉ đơn thuần kéo thả trong 1 collectionview duy nhất thì chúng ta có thể dùng các hàm delegate có sẵn của UICollectionView optional func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, ...
1. Drag and drop data trong UICollectionView
Nếu chỉ đơn thuần kéo thả trong 1 collectionview duy nhất thì chúng ta có thể dùng các hàm delegate có sẵn của UICollectionView
optional func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
Còn nếu gỉa sử chúng ta có 3 collectionView, chúng ta kéo data từ collectionView này sang collectionView khác và ngược lại, hôm nay chúng ta sẽ cùng đi giải quyết bài toán đó.
2. Giải quyết bài toán
Chúng ta có thể tượng tượng như thế này
Tất cả UICollectionView của chúng ta sẽ nằm trong 1 vùng gọi là canvas, vùng này có tác dụng xử lý sự kiện kéo thả, khi chúng ta kéo 1 cell data từ collectionView này thả sang collectionView khác, chúng ta phải thực hiện đồng thời 2 sự kiện, xoá cell ở collectionView này và insert vào collectionView khác.
2.1 Setup UICollectionView
Để thực hiện việc kéo thả được data giữa các collectionView với nhau, chúng ta cần xây dựng 2 protocol cho việc này:
- Draggable: xử lý các sự kiện kéo data, tính toán việc kéo data trong collectionView.
- Droppable: xử lý các sự kiện sau khi kéo data, xoá thêm data trong collectionView
protocol Draggable { func canDragAtPoint(_ point : CGPoint) -> Bool func representationImageAtPoint(_ point : CGPoint) -> UIView? func dataItemAtPoint(_ point : CGPoint) -> AnyObject? func dragDataItem(_ item : AnyObject) func startDraggingAtPoint(_ point : CGPoint) func stopDragging() }
protocol Droppable { func canDropAtRect(_ rect : CGRect) -> Bool func willMoveItem(_ item : AnyObject, inRect rect : CGRect) func didMoveItem(_ item : AnyObject, inRect rect : CGRect) func didMoveOutItem(_ item : AnyObject) -> Void func dropDataItem(_ item : AnyObject, atRect : CGRect) }
Tiếp theo khi kéo các cell từ collectionView này sang collectionView khác, data tương ứng trong các dataSource của từng collectionView cũng phải di chuyển theo(delete từ thằng này xong insert vào thằng khác), do đó chúng ta cần 1 dataSource 1 delegate để làm việc này.
public protocol DragDropCollectionViewDataSource : UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, indexPathForDataItem dataItem: AnyObject) -> IndexPath? func collectionView(_ collectionView: UICollectionView, dataItemForIndexPath indexPath: IndexPath) -> AnyObject func collectionView(_ collectionView: UICollectionView, cellIsDraggableAtIndexPath indexPath: IndexPath) -> Bool }
public protocol DragDropCollectionViewDelegate : UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, moveDataItemFromIndexPath from: IndexPath, toIndexPath to : IndexPath) func collectionView(_ collectionView: UICollectionView, insertDataItem dataItem : AnyObject, atIndexPath indexPath: IndexPath) func collectionView(_ collectionView: UICollectionView, deleteDataItemAtIndexPath indexPath: IndexPath) }
Chúng ta tạo tiếp 1 class DragDropCollectionView, rồi cho nó adopt 2 protocol Draggable và Droppable
class DragDropCollectionView: UICollectionView, Draggable, Droppable { required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override open func awakeFromNib() { super.awakeFromNib() } override public init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: layout) } }
2.1 Implement các function trong protocol draggable
// MARK : Draggable public func canDragAtPoint(_ point : CGPoint) -> Bool { if let dataSource = self.dataSource as? DragDropCollectionViewDataSource, let indexPathOfPoint = self.indexPathForItem(at: point) { return dataSource.collectionView(self, cellIsDraggableAtIndexPath: indexPathOfPoint) } return false }
Hàm canDragAtPoint kiểm tra xem cell chúng ta kick vào có thể drag được ko, việc drag đc hay ko sẽ do chúng ta setup ở viewcontroller
public func representationImageAtPoint(_ point : CGPoint) -> UIView? { guard let indexPath = self.indexPathForItem(at: point) else { return nil } guard let cell = self.cellForItem(at: indexPath) else { return nil } UIGraphicsBeginImageContextWithOptions(cell.bounds.size, cell.isOpaque, 0) cell.layer.render(in: UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() let imageView = UIImageView(image: image) imageView.frame = cell.frame return imageView }
hàm representationImageAtPoint sẽ chụp lại view tại vị trí cell chúng ta kíck vào sau đó chuyển nó thành dạng ảnh, việc này sẽ cho chúng ta 1 snapshot của cell
public func dataItemAtPoint(_ point : CGPoint) -> AnyObject? { guard let indexPath = self.indexPathForItem(at: point) else { return nil } guard let dragDropDS = self.dataSource as? DragDropCollectionViewDataSource else { return nil } return dragDropDS.collectionView(self, dataItemForIndexPath: indexPath) }
hàm dataItemAtPoint sẽ lấy data tại cell chúng ta kích
public func startDraggingAtPoint(_ point : CGPoint) -> Void { self.draggingIndexPath = self.indexPathForItem(at: point) self.reloadData() }
Hàm startDraggingAtPoint sẽ lấy indexPath tại cell chúng ta kicks sau đó lưu lại
public func stopDragging() -> Void { if let idx = self.draggingIndexPath { if let cell = self.cellForItem(at: idx) { cell.isHidden = false } } self.draggingIndexPath = nil self.reloadData() }
Hàm stopDragging sẽ được gọi sau khi kết thúc việc drag and drop, ở đây nó xoá đi indexPath cell đã lưu trước đó và hiện lại cell đã ẩn.
2.2 Implement các function trong protocol droppable
// MARK: Droppable public func dragDataItem(_ item : AnyObject) -> Void { guard let dragDropDataSource = self.dataSource as? DragDropCollectionViewDataSource else { return } guard let dragDropDelegate = self.delegate as? DragDropCollectionViewDelegate else { return } guard let existngIndexPath = dragDropDataSource.collectionView(self, indexPathForDataItem: item) else { return } dragDropDelegate.collectionView(self, deleteDataItemAtIndexPath: existngIndexPath) if self.animating { self.deleteItems(at: [existngIndexPath]) } else { self.animating = true self.performBatchUpdates({ () -> Void in self.deleteItems(at: [existngIndexPath]) }, completion: { complete -> Void in self.animating = false self.reloadData() }) } }
Hàm dragDataItem sẽ được gọi khi chúng ta kéo 1 cell data ra khỏi collectionView thì nó sẽ tiến hành xoá bỏ cell đó cũng như data của cell đó.
public func canDropAtRect(_ rect : CGRect) -> Bool { return (self.indexPathForCellOverlappingRect(rect) != nil) }
Hàm canDropAtRect sẽ kiểm tra xem tại vị trí chúng ta drop cell có thể insert được ko
public func willMoveItem(_ item : AnyObject, inRect rect : CGRect) { guard let dragDropDataSource = self.dataSource as? DragDropCollectionViewDataSource else { return } guard let dragDropDelegate = self.delegate as? DragDropCollectionViewDelegate else { return } if let _ = dragDropDataSource.collectionView(self, indexPathForDataItem: item) { return } if let indexPath = self.indexPathForCellOverlappingRect(rect) { dragDropDelegate.collectionView(self, insertDataItem: item, atIndexPath: indexPath) self.draggingIndexPath = indexPath self.animating = true self.performBatchUpdates({ () -> Void in self.insertItems(at: [indexPath]) }, completion: { complete -> Void in self.animating = false if self.draggingIndexPath == nil { self.reloadData() } }) } currentInRect = rect }
Hàm willMoveItem sẽ thực hiện insert cell data mới vào collectionView mà chúng ta vừa thả data vào
public func didMoveItem(_ item : AnyObject, inRect rect : CGRect) -> Void { guard let dragDropDataSource = self.dataSource as? DragDropCollectionViewDataSource else { return } guard let dragDropDelegate = self.delegate as? DragDropCollectionViewDelegate else { return } if let existingIndexPath = dragDropDataSource.collectionView(self, indexPathForDataItem: item), let indexPath = self.indexPathForCellOverlappingRect(rect) { if indexPath.item != existingIndexPath.item { dragDropDelegate.collectionView(self, moveDataItemFromIndexPath: existingIndexPath, toIndexPath: indexPath) self.animating = true self.performBatchUpdates({ () -> Void in self.moveItem(at: existingIndexPath, to: indexPath) }, completion: { (finished) -> Void in self.animating = false self.reloadData() }) self.draggingIndexPath = indexPath } } var normalizedRect = rect normalizedRect.origin.x -= self.contentOffset.x normalizedRect.origin.y -= self.contentOffset.y currentInRect = normalizedRect self.checkForEdgesAndScroll(normalizedRect) }
Hàm didMoveItem sẽ được gọi nếu chúng ta chỉ di chuyển cell trong cùng collectionView mà ko sang collectionView khác, việc này chỉ đơn giản là hoán đổi vị trí cell cũ và cell mới
public func didMoveOutItem(_ item : AnyObject) { guard let dragDropDataSource = self.dataSource as? DragDropCollectionViewDataSource, let existngIndexPath = dragDropDataSource.collectionView(self, indexPathForDataItem: item) else { return } guard let dragDropDelegate = self.delegate as? DragDropCollectionViewDelegate else { return } dragDropDelegate.collectionView(self, deleteDataItemAtIndexPath: existngIndexPath) if self.animating { self.deleteItems(at: [existngIndexPath]) } else { self.animating = true self.performBatchUpdates({ () -> Void in self.deleteItems(at: [existngIndexPath]) }, completion: { (finished) -> Void in self.animating = false; self.reloadData() }) } if let idx = self.draggingIndexPath { if let cell = self.cellForItem(at: idx) { cell.isHidden = false } } self.draggingIndexPath = nil currentInRect = nil }
Hàm didMoveOutItem sẽ được gọi nếu chúng ta di chuyển cell và đè nó lên 1 cell khác, cell cũ tại ví trí đó sẽ bị xoá đi.