How to reuse a UIPickerView cell in a table?

My pickers are behaving erratically in my table. The data for the number of picker rows to show seems to be the problem.


I'm using a table to collect user settings and one section contains 5 rows, each with an expanding custom cell containing a picker view. So selecting one row expands and shows the following row with a picker to select a desired unit, °F or °C for instance in the temperature units picker and psi or atm in the pressure units picker


This all works perfectly for the first picker selected. However the next picker selected will only show as many rows as the previously selected picker. So if the first picker selected has two rows, the second one selected will show only the first two entries in its array of string values even though those two entries are from the correct array containing more values. Closing that row and reselecting it will show everything properly. If you select any picker with less values available in the array than the previously selected had, the app crashes because of an index out of range.


I'm at wit's end and feel like I've tried everything. What am I missing?


In my CustomCell:


class CustomCell: UITableViewCell, UITextFieldDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var descriptionLabel: UILabel!
    @IBOutlet weak var datePicker: UIDatePicker!
    @IBOutlet weak var unitPicker: UIPickerView!  // This is the troublemaker
    var pickArray = [String]()  // This is the list of values to show in the picker, set in the table VC
    override func prepareForReuse() {
        super.prepareForReuse()
        textField?.text = nil
        descriptionLabel?.text = nil
        textLabel?.text = nil
        textField?.placeholder = nil
        unitPicker?.reloadAllComponents()  // My picker
     }

func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 1
}

func pickerView(_ pickerView: UIPickerView!, numberOfRowsInComponent component: Int) -> Int {
   return pickArray.count  // This seems to be where the problem is, or this is not being requested when the picker is drawn
}


func pickerView(_ pickerView: UIPickerView!, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
    let pickerLabel = UILabel()
    pickerLabel.font = UIFont(name: "Avenir Next Condensed", size: 22) /
    pickerLabel.textAlignment = NSTextAlignment.center
    pickerLabel.text = pickArray[row]
    return pickerLabel
}


func pickerView(_ pickerView: UIPickerView!, didSelectRow row: Int, inComponent component: Int)
{
    }  // I'm using a button to process the selected value, so nothing here
}


A portion of my table view controller:


    func configureTableView() {
        tblExpandable.register(UINib(nibName: "UnitsPickerCell", bundle: nil), forCellReuseIdentifier: "idCellUnitPicker")
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cellLabel = tableView.cellForRow(at: [indexPath.section, indexPath.row])?.textLabel?.text
        let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
        if (cellDescriptors[indexPath.section])[indexOfTappedRow] ["isExpandable"] as! Bool == true {
            var shouldExpandAndShowSubRows = false
            if (cellDescriptors[indexPath.section])[indexOfTappedRow]["isExpanded"] as! Bool == false {
                shouldExpandAndShowSubRows = true
            }
            (cellDescriptors[indexPath.section])[indexOfTappedRow].updateValue(shouldExpandAndShowSubRows, forKey: "isExpanded")
     
            for i in (indexOfTappedRow + 1)...(indexOfTappedRow + ((cellDescriptors[indexPath.section])[indexOfTappedRow]["additionalRows"] as! Int)) {
                print("Looping through additional row \(i)")
                ((cellDescriptors[indexPath.section])[i]).updateValue(shouldExpandAndShowSubRows, forKey: "isVisible")
         
                let  unitPicker = (cellDescriptors[indexPath.section])[indexOfTappedRow]["secondaryTitle"] as! String
                switch unitPicker {
                case "Temperature":
                   pickArray = tempPickerUnits  // These are all arrays of strings, 2-6 values each
                case "Pressure":
                     pickArray = pressurePickerUnits
                case "Humidity":
                    pickArray = humidityPickerUnits
                case "Enthalpy":
                    pickArray = enthalpyPickerUnits
                case "Volume":
                    pickArray = volumePickerUnits
                default:
                    pickArray = tempPickerUnits
                }
            }
        }
        self.getIndicesOfVisibleRows()
        self.tblExpandable.reloadSections(IndexSet(integer: indexPath.section), with: UITableViewRowAnimation.fade)


    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   
        let currentCellDescriptor = getCellDescriptorForIndexPath(indexPath)
        let cell = tableView.dequeueReusableCell(withIdentifier: currentCellDescriptor["cellIdentifier"] as! String, for: indexPath) as! CustomCell
       if currentCellDescriptor["cellIdentifier"] as! String == "idCellUnitPicker" {
            cell.pickArray = pickArray  // This should tell the picker which array of strings to show
}
Answered by waynehend in 276936022

I did it !!


The key line of code is shown below. This ensures the picker asks the custom cell for the numberOfRowsInComponent each time the picker is called. The problem I was having was that the picker row text array was updating properly for each row in the viewForRow, but for some reason the number of rows was not updating each time the picker cell opened. This single line of code fixed that. Woohoo!


    override func prepareForReuse() {
        super.prepareForReuse()
        textField?.text = nil
        descriptionLabel?.text = nil
        textLabel?.text = nil
        textField?.placeholder = nil
        unitPicker?.reloadAllComponents()
        unitPicker?.dataSource = self  // This is it !
     }

My problem seems to be time related. I logged to the console every time that the pickerview numberOf RowsInComponent function was called and, even when it works, the first several times show 0 rows and the pickArray (containing the strings to show in the picker) is empty. Five milliseconds later the proper values appear and the picker is drawn properly.

Where did you define visibleRowsPerSection


Could you test in indexPath.section, indexPath.row, indexOfTappedRow in tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

print(indexPath.section, indexPath.row, indexOfTappedRow)

Look at h ttps://stackoverflow.com/questions/43818004/making-table-view-section-expand-swift/43818310

They explain some problems of the tutorial used for this code.

It gets defined in the table's view controller.


    func getIndicesOfVisibleRows() {
        visibleRowsPerSection.removeAll()
        for currentSectionCells in cellDescriptors {
            var visibleRows = [Int]()
            let rowCount = (currentSectionCells as AnyObject).count as! Int
            for row in 0..<rowCount {
                var testDict = currentSectionCells[row]
                if testDict["isVisible"] as! Bool == true {
                    visibleRows.append(row)
                }
            }
            visibleRowsPerSection.append(visibleRows)
        }
    }


But I don't have any issues with the rows showing properly in each section. The problem is strictly with the picker views. If I use date pickers instead of UIPickerViews, there is no problem whatsoever. The section and row of the indexOfTappedRow are all good.


I'm familiar with that Stackoverflow post. There have been additional syntax changes since then but as I said, I've got it working fine with textfields and date pickers for custom cells.


Here's a bizarre aspect with my UIPickerView problem. As shown in the code I set cell.pickArray in the table's VC and calculate pickArray.count in the custom cell code. This works fairly well but has the problems I noted in my first post. If instead I determine pickArray.count in the table VC and pass it to the custom cell as cell.arrayRowCount, and then use that for the number of rows in the picker, it all fails horribly. Both pickArray and arrayRowCount are empty when the pickerview asks for the the row count and the rows for the picker, and the app crashes. Why would this subtle change of where the count is determined make such a huge difference?

I should add that the line the app crashes on is the one indicated below:


func pickerView(_ pickerView: UIPickerView!, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
  logSB.warning("Setting the view for row \(row) in component \(component)")
    let pickerLabel = UILabel()
    pickerLabel.font = UIFont(name: "Avenir Next Condensed", size: 22) /
    logSB.warning("About to center the pickerLabel text")
    pickerLabel.textAlignment = NSTextAlignment.center
    logSB.warning("About to set the pickerLabel text to pickArray \(pickArray) row \(row)")
    pickerLabel.text = self.pickArray[row]  // Fails here, index out of range, but it shouldn't be!
    logSB.warning("Set the pickerLabel text to \(pickArray[row])")
    return pickerLabel
}
Accepted Answer

I did it !!


The key line of code is shown below. This ensures the picker asks the custom cell for the numberOfRowsInComponent each time the picker is called. The problem I was having was that the picker row text array was updating properly for each row in the viewForRow, but for some reason the number of rows was not updating each time the picker cell opened. This single line of code fixed that. Woohoo!


    override func prepareForReuse() {
        super.prepareForReuse()
        textField?.text = nil
        descriptionLabel?.text = nil
        textLabel?.text = nil
        textField?.placeholder = nil
        unitPicker?.reloadAllComponents()
        unitPicker?.dataSource = self  // This is it !
     }

Hmmm... One more step has me stumped. I want to be able to set the selcted row in the picker. The solution above results in the first element of the picker array values to show.


I have no problem doing this in my pickers that are not in a table. I use:

var rowToSelect = x
myPicker.selectRow(rowToSelect!, inComponent: 0, animated: true)


That works great. But I cannot figure out how to accomplish the same thing for a picker embedded in a table cell as in the scheme above.


Any ideas?

Once again I solved my own problem.


func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
...
let cellToEdit = tableView.cellForRow(at: [indexPath.section, indexPath.row + 1])
        if let subviews = cellToEdit?.contentView.subviews {
            for view in subviews {
                if let picker = view as? UIPickerView {
                    picker.selectRow(pickerRowToShow, inComponent: 0, animated: true)
                    break
                }
            }
        }


I used this once before for placing the cursor into a newly-expanded textfield.

For anyone that comes across this in the future, today I found an additional and crucial line of code that should be in the tableview cell description, to ensure that your picker loads up its data before it shows. Once you've defined the picker's data, add this.


cell.yourPicker.reloadAllComponents()


I have a table with three pickers that appear in three cells when the 'header' row is tapped. The problem I was having was that they'd first appear empty. They'd show perfectly if I scrolled them off screen and back, or if I collapsed their cell and re-expanded it. I was tearing my hair out until I found that single line of code and placed it at the end of my "cellForRowAt".


I did not have always have this problem with empty pickers. However I got it to work before, I must have just been lucky. I'd rather be lucky than smart.

How to reuse a UIPickerView cell in a table?
 
 
Q