Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Unable to long-press drag cells to empty sections in UICollectionViewController and UITableView

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

like image 758
Neil Avatar asked Mar 13 '23 23:03

Neil


2 Answers

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.

like image 59
Dale Avatar answered Apr 07 '23 20:04

Dale


Details

  • Xcode 10.2 (10E125), Swift 5, from iOS 11

Idea

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:

  1. Big chance to "make a bike"
  2. The solution can be very time consuming and difficult / expensive to maintain.
  3. Standard API for working with a table / collection deeper tested

Sample

Full sample of drag and drop

enter image description here

More Info

  • Supporting Drag and Drop in Collection Views
  • UICollectionViewDragDelegate
  • UICollectionViewDropDelegate
like image 23
Vasily Bodnarchuk Avatar answered Apr 07 '23 20:04

Vasily Bodnarchuk