bibscy
bibscy

Reputation: 2708

cellForRow(at: indexPath) returns nil Swift3

I have UITableView with a custom cell IconsTableViewCell holding one UIImageView and one UILable.
If a row was previously selected, when user taps on a new row, the previous row is deselected and the label's textcolor for the new row should change.
However, when I try to get a reference to the current cell using indexPath, the app crashes. I am stuck at this for the past few hours.

class EighthViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {

let checkedImage = UIImage(named: "checked")!
let uncheckedImage = UIImage(named: "unchecked")!

struct Item {
    var name:String // name of the rows
    var selected:Bool // whether is selected or not
    var amount: Int // value of the items
}
var frequency = [
        Item(name:"Every week",selected: false, amount: 0),
        Item(name:"Every 2 weeks",selected: false, amount: 0),
        Item(name:"Every 4 weeks",selected: false, amount: 0),
        Item(name:"Once",selected: false, amount: 0),
        Item(name:"End of tenancy cleaning", selected: false, amount: 0)
    ]

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    // retrieve indexPathForCellSelected from UserDefaults
   if let retrievedIndexPath = UserDefaults.standard.data(forKey: indexKey) {
    if let data1 = NSKeyedUnarchiver.unarchiveObject(with: retrievedIndexPath) as? IndexPath {
    indexPathForCellSelected = data1

 /* Inform the delegate that the row has already been selected.

      When calling 'tableView:didSelectRowAtIndexPath:', it will calculate the total amount depending on the type of cleaning:
           Weekly, End of Tenancy..etc
             Call calculateTotal() function which is using `indexPathForCellSelected`to calculate the total */
           self.tableView(self.tableView, didSelectRowAt: indexPathForCellSelected!)

  // assign the indexPath retrieved to StructS.indexPath
       StructS.indexPath = indexPathForCellSelected

      // assign StructS.price to  self.frequencyTotalPrice
                self.frequencyTotalPrice = StructS.price

            // assign self.frequencyTotalPrice to FullData.finalFrequecyAmount
                FullData.finalFrequecyAmount = self.frequencyTotalPrice

       // assign a Checkmark to the row with the corresponding indexPathForCellSelected retrieved
         tableView.cellForRow(at: indexPathForCellSelected!)?.accessoryType = .checkmark

         tableView.cellForRow(at: indexPathForCellSelected!)?.imageView?.image = checkedImage

    let cell = tableView.cellForRow(at: indexPathForCellSelected!) as! IconsTableViewCell
    cell.frequencyLabel.textColor = .black

        // assign frequency[indexPath.row].name to FullData structure
            FullData.finalFrequencyName = frequency[indexPathForCellSelected!.row].name

    //assign the row as Int value to a global var so as to determine which ViewController to unwind segue in 10th ViewController
    StructS.frequencyRowSelectedEighthVC = indexPathForCellSelected!.row
  }
 }

    // handle the selection of the row so as to update the values of labels in section header. 
   // if indexPathForCellSelected == nil, select a default type of cleaning for the first time
    if indexPathForCellSelected == nil {
        // construct an indexPath for the row we want to select when no previous row was selected ( not already saved in UserDefaults)
    let rowToSelect:IndexPath = IndexPath(row: 1, section: 0)

        // select the row at `rowToSelect` indexPath. This will just register the selectd row, However,the code that you have in tableView:didSelectRowAtIndexPath: is not yet executed because the delegate for the tablewView object in the ViewController has not been called yet. 
      self.tableView.selectRow(at: rowToSelect, animated: true, scrollPosition: UITableViewScrollPosition.none)

        // inform the delegate that the row was selected
        // stackoverflow.com/questions/24787098/programmatically-emulate-the-selection-in-uitableviewcontroller-in-swift
        self.tableView(self.tableView, didSelectRowAt: rowToSelect)

        //assign the row as Int value to a global var so as to determine which ViewController to unwind segue in 10th ViewController
        StructS.frequencyRowSelectedEighthVC = rowToSelect.row
        print("the row that was selected is\(StructS.frequencyRowSelectedEighthVC) ")
    }
}


func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return frequency.count
}


func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

   // configure the cell
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
    -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! IconsTableViewCell
        cell.frequencyLabel.text = frequency[indexPath.row].name
        cell.frequencyLabel.textColor = .gray
        cell.iconImageView.image = uncheckedImage
         return cell
    }


    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if !frequency[indexPath.row].selected {
            // this avoid set initial value for the first time
            if let index = indexPathForCellSelected {
                // clear the previous cell
                frequency[index.row].selected = false
                tableView.cellForRow(at: index)?.accessoryType = .none
                tableView.cellForRow(at: index)?.imageView?.image = nil
            }
            //mark the new row
            frequency[indexPath.row].selected = true
            tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark

            //assign checked image to row
            tableView.cellForRow(at: indexPath)?.imageView?.image = checkedImage

            //evaluates to nil when trying to get a reference to the cell at the selected indexPath
            let cell = tableView.cellForRow(at: indexPath) as! IconsTableViewCell   
            cell.frequencyLabel.textColor = .black

            //save indexPathForCellSelected in UserDefaults
            if indexPathForCellSelected != nil { 
                // used to check if there is a selected row in the table
                let data = NSKeyedArchiver.archivedData(withRootObject: indexPathForCellSelected!)
                UserDefaults.standard.set(data, forKey: indexKey)
                self.tableView.reloadData()
            } // end of if indexPathForCellSelected
        }
    }
} //end of class

Upvotes: 0

Views: 1631

Answers (2)

vadian
vadian

Reputation: 285059

  • Create a property selectedRow. By default the first row is selected.

    var selectedRow = 0
    
  • In viewWillAppear read the selected row from UserDefaults and reload the table view

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        selectedRow = UserDefaults.standard.integer(forKey: indexKey)
        frequency[selectedRow].selected = true
        tableView.reloadData()
    }
    
  • In cellForRow set color, image and accessory view depending on the selected property

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell" for: indexPath) as! IconsTableViewCell
        let freq = frequency[indexPath.row]
        if freq.selected {
            cell.accessoryType = .checkmark
            cell.imageView?.image = checkedImage
            cell.frequencyLabel.textColor = .gray
        } else {
            cell.accessoryType = .none
            cell.imageView?.image = uncheckedImage
            cell.frequencyLabel.textColor = .black
        }
    
        cell.frequencyLabel.text = freq.name
        return cell
    }
    
  • In didSelectRowAt compare the actual index path with selectedRow. If they are not equal set the selected property of the former selected cell to false and of the new selected cell to true. Then set selectedRow to the row of the index path, save the row to UserDefaults and reload the table view.

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
        if indexPath.row != selectedRow {
            let previousIndexPath = IndexPath(row:selectedRow, section:0)
            frequency[selectedRow].selected = false
            frequency[indexPath.row].selected = true
            selectedRow = indexPath.row   
    
            UserDefaults.standard.set(selectedRow, forKey: indexKey)
            tableView.reloadRows(at: [indexPath, previousIndexPath], with: .none)
        }
    }
    

Upvotes: 1

koen
koen

Reputation: 5729

I would try this (assuming you have an array of Item structs):

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
   let item = myItemArray[indexPath.row]
   (cell as! IconsTableViewCell).frequencyLabel.textColor = // get the color from your item selection state here
   ... // do other configurations to your cell
}

Upvotes: 0

Related Questions