0

I have a table view with a custom cell. Once you select a row, some things will happen. Once the row gets deselected, some other things will happen.

This all works fine, but there is one bug I can't fix. I can tap on every row I want and 'nothing' happens, but if I tap on the second row and then the last row or the other way around I get this error:

unexpectedly found nil while unwrapping an Optional value

The LLDB showed me where the error occurred and it should be in this line of code:

let cell = tableView.cellForRowAtIndexPath(indexPath) as! TaskViewerCell

Is there anybody who knows how to fix this issue? I've added the code of both functions below.

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath) as! TaskViewerCell

    number = indexPath.row
    if cell.submit.hidden != false || next[0..<indexPath.row].contains(false){
        cell.submit.setTitle("Not completed", forState: .Normal)
        cell.submit.setTitleColor(UIColor.orangeColor(), forState: .Normal)
        logbook = "Pending"
    }else{
        logbook = "completed"
        cell.submit.setTitle("Completed", forState: .Normal)
        cell.submit.setTitleColor(UIColor.greenColor(), forState: .Normal)
    }
    if indexPath.row != 0 && next[indexPath.row-1] == true{
    cell.nSwitch.enabled = true
    cell.nSegment.enabled=true
    cell.nButton.enabled = true
    cell.submit.enabled = true
        print("tapped\(indexPath.row)")
    }else{
        cell.nSwitch.enabled = false
        cell.nSegment.enabled=false
        cell.nButton.enabled = false
        cell.submit.enabled = false
    }

}

override func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath) as! TaskViewerCell // This is where is should go wrong!!
    switch1[indexPath.row] = cell.nSwitch.on
    yesno1[indexPath.row] = cell.nSegment.selectedSegmentIndex
    if cell.nField != "" {
            field1[indexPath.row] = cell.nField.text!
          }else{
            field1[indexPath.row] = cell.nField.placeholder!
        }
    topField[indexPath.row] = cell.topField.text!
    topSegment[indexPath.row] = cell.topSegment.selectedSegmentIndex

    cell.nSwitch.enabled = false
    cell.nSegment.enabled = false
    cell.nButton.enabled = false
    cell.submit.enabled = false

}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Sven Cozijn
  • 119
  • 14
  • 1
    If you know what the error means, then stop using `!` (and use the debugger to inspect what the actual type is) – Claus Jørgensen Apr 10 '16 at 21:32
  • The answers below explain that you are. It handling the potential `nil` return from `cellForRowAtIndexPath` but you have a bigger issue in that you are tracking state in table cells. You need to track state in your data model. Cells can move off screen and then the state information will be lost as you are finding out with the `nil` cell – Paulw11 Apr 10 '16 at 22:23
  • Possible duplicate of [What does "fatal error: unexpectedly found nil while unwrapping an Optional value" mean?](http://stackoverflow.com/questions/32170456/what-does-fatal-error-unexpectedly-found-nil-while-unwrapping-an-optional-valu) – jtbandes Apr 10 '16 at 23:22

4 Answers4

1

@ryantxr's answer is correct, but I would use a slightly different approach.

override func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
    let untypedCell = tableView.cellForRowAtIndexPath(indexPath)
    guard let cell = untypedCell as? TaskViewerCell else {
        assertionFailure("CFRAIT returned nil or inappropriately typed cell: \(untypedCell)")
        return
    }

    // action on the cell
}

By using guard you're handling the "sad path" immediately (that is, a nil or inappropriately typed cell) and then moving on to the happy path without any further indentation (which can lead to the pyramid of doom).

assertionFailure will crash in debug mode and show you exactly where something went wrong, but outside of debug mode (ie, on the App Store) it will be skipped and you'll just return safely without having done anything. Rather than have this uncertain behavior you may want to use preconditionFailure instead which will also crash in production. This answer from Airspeed Velocity goes into more detail on assertions and preconditions.

If you add that guard to your code you should see it assert. In the debugger, see if untypedCell is nil or is just a type other than TaskViewerCell. That gives you your next step to debug this.

wjl
  • 7,143
  • 1
  • 30
  • 49
  • 1
    It is quite valid for `cellForRowAtIndexPath` to return nil in the case where that cell,is not currently on screen. The OP has a more fundamental problem in that they are trying to keep state in cells rather than in their data model. As cells can move off screen, state can be lost. If they fix this then they probably won't even need a didDeselect method – Paulw11 Apr 10 '16 at 22:20
  • It's true that the underlying problem is more fundamental but since Google searches often come here I try to give the answer to the question at hand rather than a re-architecture. – wjl Apr 10 '16 at 23:01
  • @Paulw11 Is it valid for `cellForRowAtIndexPath` to return `nil` when called with the `indexPath` from `didDeselect..`? – wjl Apr 10 '16 at 23:02
  • There are two `cellForRowAtIndexPath` methods - One that you implement in your datasource. This can't return nil. The second method, the one in use here, asks the tableview for the cell; the tableview may not have a cell for that indexpath as it may never have been displayed or it may have scrolled offscreen. In the case of `didDeselect..` if the table has only single selection and the currently selected row is off screen when a new row is selected you would expect nil as there is an implicit deselection resulting from the new selection – Paulw11 Apr 10 '16 at 23:10
  • @Paulw11 Thanks for that explanation, that makes sense. – wjl Apr 11 '16 at 20:27
0

The exclamation ! in Swift should almost never be used unless you are absolutely certain that you can unwrap the optional. In your case, the call is returning nil.

if let cell = tableView.cellForRowAtIndexPath(indexPath) as? TaskViewerCell {
    // it worked
}
else {
    // it didn't work
}
ryantxr
  • 4,119
  • 1
  • 11
  • 25
0

You need to learn how to deal with optionals. Until you understand it VERY well, such that it comes completely naturally, you should avoid using the ! force unwrap operator entirely.

Take a look at this question and answer I wrote addressing your crash:

How to deal with "Fatal error: unexpectedly found nil while unwrapping an Optional value."

Community
  • 1
  • 1
Duncan C
  • 128,072
  • 22
  • 173
  • 272
0

Thanks for all helpful answers. I didn't knew it had to do something with the reuse of cells (I thought this method finally fixed it). With the help of your answers I created a solution to bypass this problem with the help of scrollViewWillBeginDragging

In this function (almost) the same will be done as in didDeselectRowAtIndexPath and the selected cell will be deselected as well. So now if you want to go to the top of bottom by scrolling, the values will be saved before they will be reused.

Maybe this might also solve this problem for others.

Sven Cozijn
  • 119
  • 14