UITableView Drag & Drop Outside Table = Crash
Asked Answered
H

2

4

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

Holden answered 27/8, 2015 at 18:21 Comment(5)
Might be the case the it's hitting an "array index out of bounds" thinking that the cell is somehow being placed in a position in the tableview that would need to append a new item to the tableview array which isn't possible so the system get's pissed off and crashes, whats the error saying specificaly?Affine
@Larcerax Ha! Well, I guess you're on the right track. It throws an EXC: Bad Instruction code. Console reads: "fatal error: Array index out of range (lldb)". Any idea how to make this user behavior safe and not cause a crash?Holden
yeah, so before the event, or always when you are dragging a cell, you may need to some how throw out a hiccup, like a refresh of sorts, or a dispatch get main queue event, or dispatch after time blah blah, to give a short interrupt. If you can force add a cell right when dragging begins, then you are fine, then this cell is deleted, the cell woudl be a hidden cell, but this is very messyAffine
@Larcerax What about an if-statement that says if the dragged cell has gone too far south and can't get a location from a cell, the dragged cell's location is equal to the last cell? That way, if the user tries to drag below the last cell, the cell will just appear to hit a wall when it gets to the bottom?Holden
that would probably work, I've seen a few solutions sort of like this, and this a very interesting problem. let me see what I can find on this one because I knwo that I'm going to have the same problem very soon here myself.Affine
G
3

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!

Ghirlandaio answered 31/8, 2015 at 6:5 Comment(5)
Move the entire switch statement or just the individual cases? I already had a nil check for the snapshot wrapping the code within each case. I just added your code, telling each case to check that both the snapshot and the indexPath are not nil. Still, I get: "fatal error: Array index out of range"Holden
Added my full longPress code so you can see my full and most recent attempt at making it work (including your code!). Thx for that first try, hope you're the wizard that can solve this one because it's driving me nuts and it's like 1 of 2 remaining bugs I have to fix before the app is ready for upload!Holden
This poster never answered my comments but the answer is that the "if indexPath != nil" statement needs to immediately follow the declaration of the indexPath var and the rest of the drag and drop code should sit in that if-statement. I had each case statement nil-checking, but that wasn't enough. Also, I had a second problem causing the drag-crash. All my cells are generated from CoreData, except I then add one last placeholder cell. That was causing the "swap" function to fail, so I nested the swap in an if-statement runs only when the indexPath.row is less than a count of my CoreData items.Holden
Sorry for not answering your comments earlier! I'm glad you figured out! Do you also use a placeholder cell when you have no data?Ghirlandaio
This solution worked! But I get crash: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (9) must be equal to the number of rows contained in that section before the update (9), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 1 moved out).' When move a cell to another tableview section. How do I fix this?Perse
B
0

You don't state where in the code the crash occurs, which makes it harder to determine what is going on. Set a breakpoint on exceptions to determine which line is the culprit. To do that, use the '+' in the bottom-left corner of the breakpoint list in XCode.

The main issue I think is with the indexPath. There are a couple of issues:

  1. You are using the indexPath even though it might be nil, in this line:

    let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;
    
  2. The indexPath can be invalid, even though it is not nil. Check for its section and row members to be different from NSNotFound.

Finally, I have been using a pre-made, open source, UITableView subclass that does all the moving for you, so you don't have to implement it yourself anymore. It also takes care of autoscrolling, which you have not even considered yet. Use it directly, or use it as inspiration for your code: https://www.cocoacontrols.com/controls/fmmovetableview

Bukhara answered 5/9, 2015 at 10:0 Comment(1)
Thanks for throwing out some ideas. I added a screenshot of the crash. Looking into your suggestions nowHolden

© 2022 - 2024 — McMap. All rights reserved.