Search code examples
iosswiftuikituipickerview

UIPickerView where selected 1st component decides contents of 2nd component is out of sync


I have encountered some synchronisation/graphic update problems with my UIPickerView.

I want a view with 2 components, where the content of the second component depends on the selected row of the first component.

My code is inspired from: Swift UIPickerView 1st component changes 2nd components data

However, while it seems to work, sometimes (not every time) there are some visual problems, as seen on the screenshots below. (on the second screenshot, you can see that the rows of the second component are not really correct, and are a mix of the rows from the first and the second component)

Here is the code:

import UIKit

class AddActivityViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    @IBOutlet weak var typePicker: UIPickerView!
    var pickerData: [(String,[String])] = []


    override func viewDidLoad() {
        super.viewDidLoad()

        self.typePicker.delegate = self
        self.typePicker.dataSource = self

        pickerData = [("sport",["bike", "run", "soccer", "basketball"]),
                      ("games",["videogame", "boardgame", "adventuregame"])]

        // not sure if necessary
        typePicker.reloadAllComponents()
        typePicker.selectRow(0, inComponent: 0, animated: false)

//        pickerData = [("sport",["bike", "run", "soccer"]),
//        ("games",["videogame", "boardgame", "adventuregame"])]
    }

    // number of columns in Picker
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2
    }

    // number of rows per column in Picker
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        print("function 1 called")
        if component == 0 {
            return pickerData.count
        } else {
            let selectedRowInFirstComponent = pickerView.selectedRow(inComponent: 0)
            return pickerData[selectedRowInFirstComponent].1.count
        }
    }

    // what to show for a specific row (row) and column (component)
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        print("function 2 called with values: component: \(component), row: \(row)")
        if component == 0 {
            // refresh and reset 2nd component everytime another 1st component is chosen
            pickerView.reloadComponent(1)
            pickerView.selectRow(0, inComponent: 1, animated: true)

            // return the first value of the tuple (so the category name) at index row
            return pickerData[row].0
        } else {
            // component is 1, so we look which row is selected in the first component
            let selectedRowInFirstComponent = pickerView.selectedRow(inComponent: 0)

            // we check if the selected row is the minimum of the given row index and the amount of elements in a given category tuple array
            print("---",row, (pickerData[selectedRowInFirstComponent].1.count)-1)
            let safeRowIndex = min(row, (pickerData[selectedRowInFirstComponent].1.count)-1)
            return pickerData[selectedRowInFirstComponent].1[safeRowIndex]
        }
        //return pickerData[component].1[row]
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // This method is triggered whenever the user makes a change to the picker selection.
        // The parameter named row and component represents what was selected.
    }
}

Is this a problem with my code or generally a complicated aspect of UIPickers that can not be trivially solved?

Additionally, is there a nicer way to develop this functionality?

initial view

view out of sync


Solution

  • I solved the error, however I do not understand why this solves it.

    The solution is to imlement the func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)method, which I did not believe to be necessary just to show the fields.

    In other words, just add this to my existing code:

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        if component == 0 {
            pickerView.reloadComponent(1)
        } else {
            let selectedRowInFirstComponent = pickerView.selectedRow(inComponent: 0)
    
            print(pickerData[selectedRowInFirstComponent].1[row])
        }
    }