Search code examples
iosswiftuitableviewdrag-and-dropanimatewithduration

UITableView Drag & Drop Outside Table = Crash


The Good

My drag & drop function almost works wonderfully. I longPress a cell and it smoothly allows me to move the pressed cell to a new location between two other cells. The table adjusts and the changes save to core data. Great!

The Bad

My problem is that if I drag the cell below the bottom cell in the table, even if I don't let go (un-press) of the cell... the app crashes. If I do the drag slowly, really it crashes as the cell crosses the y-center of the last cell... so I do think it's a problem related to the snapshot getting a location. Less important, but possibly related, is that if I long press below the last cell with a value in it, it also crashes.

The drag/drop runs off a switch statement that runs one of three sets of code based on the status:

  • One case when the press begins
  • One case when the cell is being dragged
  • One case when when the user lets go of the cell

My code is adapted from this tutorial:

Drag & Drop Tutorial

My code:

 func longPressGestureRecognized(gestureRecognizer: UIGestureRecognizer) {

    let longPress = gestureRecognizer as! UILongPressGestureRecognizer
    let state = longPress.state

    var locationInView = longPress.locationInView(tableView)
    var indexPath = tableView.indexPathForRowAtPoint(locationInView)

    struct My {
        static var cellSnapshot : UIView? = nil
    }
    struct Path {
        static var initialIndexPath : NSIndexPath? = nil
    }

    let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;

    var dragCellName = currentCell.nameLabel!.text
    var dragCellDesc = currentCell.descLabel.text


    //Steps to take a cell snapshot. Function to be called in switch statement
    func snapshotOfCell(inputView: UIView) -> UIView {
        UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)
        inputView.layer.renderInContext(UIGraphicsGetCurrentContext())
        let image = UIGraphicsGetImageFromCurrentImageContext() as UIImage
        UIGraphicsEndImageContext()
        let cellSnapshot : UIView = UIImageView(image: image)
        cellSnapshot.layer.masksToBounds = false
        cellSnapshot.layer.cornerRadius = 0.0
        cellSnapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0)
        cellSnapshot.layer.shadowRadius = 5.0
        cellSnapshot.layer.shadowOpacity = 0.4
        return cellSnapshot
    }


    switch state {
        case UIGestureRecognizerState.Began:
            //Calls above function to take snapshot of held cell, animate pop out
            //Run when a long-press gesture begins on a cell
            if indexPath != nil && indexPath != nil {
                Path.initialIndexPath = indexPath
                let cell = tableView.cellForRowAtIndexPath(indexPath!) as UITableViewCell!
                My.cellSnapshot  = snapshotOfCell(cell)
                var center = cell.center

                My.cellSnapshot!.center = center
                My.cellSnapshot!.alpha = 0.0

                tableView.addSubview(My.cellSnapshot!)

                UIView.animateWithDuration(0.25, animations: { () -> Void in
                    center.y = locationInView.y

                    My.cellSnapshot!.center = center
                    My.cellSnapshot!.transform = CGAffineTransformMakeScale(1.05, 1.05)
                    My.cellSnapshot!.alpha = 0.98

                    cell.alpha = 0.0

                    }, completion: { (finished) -> Void in

                        if finished {
                            cell.hidden = true
                        }
                })
            }
        case UIGestureRecognizerState.Changed:

            if My.cellSnapshot != nil && indexPath != nil {
                //Runs when the user "lets go" of the cell
                //Sets CG Y-Coordinate of snapshot cell to center of current location in table (snaps into place)
                var center = My.cellSnapshot!.center
                center.y = locationInView.y
                My.cellSnapshot!.center = center

                var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
                var context: NSManagedObjectContext = appDel.managedObjectContext!
                var fetchRequest = NSFetchRequest(entityName: currentListEntity)
                let sortDescriptor = NSSortDescriptor(key: "displayOrder", ascending: true )
                fetchRequest.sortDescriptors = [ sortDescriptor ]


                //If the indexPath is not 0 AND is not the same as it began (didn't move)...
                //Update array and table row order
                if ((indexPath != nil) && (indexPath != Path.initialIndexPath)) {

                    swap(&taskList_Cntxt[indexPath!.row], &taskList_Cntxt[Path.initialIndexPath!.row])
                    tableView.moveRowAtIndexPath(Path.initialIndexPath!, toIndexPath: indexPath!)

                    toolBox.updateDisplayOrder()
                    context.save(nil)

                    Path.initialIndexPath = indexPath
                }
            }
        default:
            if My.cellSnapshot != nil && indexPath != nil {
                //Runs continuously while a long press is recognized (I think)
                //Animates cell movement
                //Completion block: 
                //Removes snapshot of cell, cleans everything up
                let cell = tableView.cellForRowAtIndexPath(Path.initialIndexPath!) as UITableViewCell!

                cell.hidden = false
                cell.alpha = 0.0
                UIView.animateWithDuration(0.25, animations: { () -> Void in
                    My.cellSnapshot!.center = cell.center
                    My.cellSnapshot!.transform = CGAffineTransformIdentity
                    My.cellSnapshot!.alpha = 0.0
                    cell.alpha = 1.0
                    }, completion: { (finished) -> Void in
                        if finished {
                            Path.initialIndexPath = nil
                            My.cellSnapshot!.removeFromSuperview()
                            My.cellSnapshot = nil
                        }
                })//End of competion block & end of animation

            }//End of 'if nil'

    }//End of switch

}//End of longPressGestureRecognized

Potential Culprit

My guess is that the issue is related to the cell being unable to get coordinates once it is below the last cell. It isn't really floating, it is constantly setting its location in relation to the other cells. I think the solution will be an if-statement that does something magical when there's no cell to reference for a location. But what!?! Adding a nil check to each case isn't working for some reason.

Clearly Stated Question

How do I avoid crashes and handle an event where my dragged cell is dragged below the last cell?

Screenshot of crash:

Out of Range


Solution

  • The Ugly

    It seems that you simply need to do a preemptive check, to ensure your indexPath is not nil:

    var indexPath = tableView.indexPathForRowAtPoint(locationInView)
    if (indexPath != nil) {
        //Move your code to this block
    }
    

    Hope that helps!