Reputation: 320
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
Upvotes: 4
Views: 2973
Reputation: 498
I've tried using LongPress or Dummy/Empty Cell solutions, but they didn't work for me.
If you have section headers (SupplementaryViews), you could calculate the correct IndexPath by finding the closest SupplementaryView and it's IndexPath to the current drag location:
extension CollectionViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath ?? closestIndexPath(coordinator.session, collectionView) else { return }
// Do your reordering work here
}
private func closestIndexPath(_ session: UIDropSession, _ collectionView: UICollectionView) -> IndexPath? {
let location = session.location(in: collectionView)
let indexPaths = collectionView.indexPathsForVisibleSupplementaryElements(ofKind: "SectionHeaderSupplementaryViewIdentifier")
let supplementaryViews = collectionView.visibleSupplementaryViews(ofKind: "SectionHeaderSupplementaryViewIdentifier")
var correctDestination: IndexPath?
collectionView.performUsingPresentationValues {
correctDestination = closestIndexPath(to: location, using: supplementaryViews, with: indexPaths)
}
return correctDestination
}
private func closestIndexPath(to point: CGPoint, using views: [UIView], with indexPaths: [IndexPath]) -> IndexPath? {
guard views.count == indexPaths.count else {
print("Views and indexPaths arrays must have the same count.")
return nil
}
var minDistance: CGFloat = .greatestFiniteMagnitude
var closestIndexPath: IndexPath?
for (index, view) in views.enumerated() {
let distance = abs(point.y - view.frame.maxY)
if distance < minDistance {
minDistance = distance
closestIndexPath = indexPaths[index]
}
}
return closestIndexPath
}
}
Upvotes: 0
Reputation: 25294
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:
Upvotes: 5
Reputation: 3323
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.
Upvotes: 4