Previously, I tried to replace the standard apple reorder controls (dragging cell from handle on right) with a long press drag in a UITableView. However, with the longpress drag I was for some reason unable to move cells to a section with no cells in it already. Now I am trying to implement a function where users can drag cells between 2 sections in a UICollectionViewController instead of a UITableView. I implemented the long-press drag function but I am having the same issue for some reason. How would I add a dummy cell to the sections so that they are never empty or is there a better way around this? Also is there a way to drag cells without having to longpress?
These are the functions I added to my UICollectionViewController class to enable the long-press drag:
override func viewDidLoad() {
super.viewDidLoad()
let longPressGesture = UILongPressGestureRecognizer(target: self, action: "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()
}
}
override func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let fromRow = sourceIndexPath.row
let toRow = destinationIndexPath.row
let fromSection = sourceIndexPath.section
let toSection = destinationIndexPath.section
var item: Item
if fromSection == 0 {
item = section1Items[fromRow]
section1Items.removeAtIndex(fromRow)
} else {
item = section2Items[sourceIndexPath.row]
section2Items.removeAtIndex(fromRow)
}
if toSection == 0 {
section1Items.insert(score, atIndex: toRow)
} else {
section2Items.insert(score, atIndex: toRow)
}
}
override func collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
Thanks
To answer the second part of your question first, use a UIPanGestureRecogniser rather than a UILongPressRecogniser.
Regarding empty sections, you can add a dummy cell, which has no visibility, to empty sections. Create a prototype cell in the storyboard with no subviews, and make sure it has the same background colour as the collection view.
You need to arrange that this cell is displayed if the section would otherwise be empty. But also you need to add a dummy cell when a move begins in a section which has only 1 cell, otherwise the section will collapse during the move and the user is unable to move the cell back into the section from which it started.
In the gesture handler, add a temporary cell when beginning a move. Also remove the cell when the drag completes if it has not already been removed (the delegate moveItemAtIndexPath method is not called if the cell does not actually move):
var temporaryDummyCellPath:NSIndexPath?
func handlePanGesture(gesture: UIPanGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
if model.numberOfPagesInSection(selectedIndexPath.section) == 1 {
// temporarily add a dummy cell to this section
temporaryDummyCellPath = NSIndexPath(forRow: 1, inSection: selectedIndexPath.section)
collectionView.insertItemsAtIndexPaths([temporaryDummyCellPath!])
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
// remove dummy path if not already removed
if let dummyPath = self.temporaryDummyCellPath {
temporaryDummyCellPath = nil
collectionView.deleteItemsAtIndexPaths([dummyPath])
}
default:
collectionView.cancelInteractiveMovement()
// remove dummy path if not already removed
if let dummyPath = temporaryDummyCellPath {
temporaryDummyCellPath = nil
collectionView.deleteItemsAtIndexPaths([dummyPath])
}
}
}
In collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int)
always return 1 more than the number of items in your model, or an additional cell if a temporary dummy cell has been added.
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// special case: about to move a cell out of a section with only 1 item
// make sure to leave a dummy cell
if section == temporaryDummyCellPath?.section {
return 2
}
// always keep one item in each section for the dummy cell
return max(model.numberOfPagesInSection(section), 1)
}
In collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath)
allocate the dummy cell if the row is equal to the number of items in the model for that section.
Disable selection of the cell in collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath)
by returning false for the dummy cell and true for other cells.
Similarly disable movement of the cell in collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath)
Make sure the target move path is limited to rows in your model:
func collectionView(collectionView: UICollectionView, targetIndexPathForMoveFromItemAtIndexPath originalIndexPath: NSIndexPath, toProposedIndexPath proposedIndexPath: NSIndexPath) -> NSIndexPath
{
let proposedSection = proposedIndexPath.section
if model.numberOfPagesInSection(proposedSection) == 0 {
return NSIndexPath(forRow: 0, inSection: proposedSection)
} else {
return proposedIndexPath
}
}
Now you need to handle the final move. Update your model, and then add or remove a dummy cell as required:
func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
{
// move the page in the model
model.movePage(sourceIndexPath.section, fromPage: sourceIndexPath.row, toSection: destinationIndexPath.section, toPage: destinationIndexPath.row)
collectionView.performBatchUpdates({
// if original section is left with no pages, add a dummy cell or keep already added dummy cell
if self.model.numberOfPagesInSection(sourceIndexPath.section) == 0 {
if self.temporaryDummyCellPath == nil {
let dummyPath = NSIndexPath(forRow: 0, inSection: sourceIndexPath.section)
collectionView.insertItemsAtIndexPaths([dummyPath])
} else {
// just keep the temporary dummy we already created
self.temporaryDummyCellPath = nil
}
}
// if new section previously had no pages remove the dummy cell
if self.model.numberOfPagesInSection(destinationIndexPath.section) == 1 {
let dummyPath = NSIndexPath(forRow: 0, inSection: destinationIndexPath.section)
collectionView.deleteItemsAtIndexPaths([dummyPath])
}
}, completion: nil)
}
Finally make sure the dummy cell has no accessibility items so that it is skipped when voice over is on.
I would like to suggest to handle
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) {...}
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {...}
When you will start dragging, you can add temporary cells to the end of the empty (or all) sections. And your empty sections will not be empty). Then you can drop your cell using standard UIKit api. And before dragging did end, you can remove temporary cells.
WHY?
I do not recommend to use gesture handler, because:
Full sample of drag and drop
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With