bibscy bibscy - 1 year ago 75
iOS Question

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

Answer Source
  • 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)
        }
    }
    
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download