iOS/Swift — Creating Multiple Dynamic Picker Views: a Quick Tutorial
I’ve been scrambling to finish the final touches to my project at Facebook for my internship which you can read more about here. Amongst all the features I’ve contributed to our app, püler, one of my favorites is the car picker view:
In order to implement this, understanding the UIPickerView object is extremely important and the way that the picker views interact with the data that you have. In my case I have a data structure that looks something like this:
{ make1: {model1: {year1, year2, … }, model2: {year1, year2, …}, …}, make2: {…}, …}
So basically a dictionary of dictionaries of dictionaries and yes, I know you probably already hate me at this point but hear me out. This was necessary due to structure of how cars work, which looks something like this for those of you who still don’t understand what’s going on.
(Please excuse the ugly drawing^). Anyway, in the grand scheme of things, there are several makes, each make has several models, and each model has several years. Now in order to make the dynamic picker views, we start with adding three UIPickerViews to our storyboard and creating outlets for them. Name each one accordingly, mine were called:
After this, we need to set up the PickerViewDelegate so that Xcode knows we’ll be providing data/instructions for the pickerView and not it. To do so, make sure you add the UIPickerViewDataSource and UIPickerViewDelegate in your class heading. After you add the two, you should get errors saying that you haven’t declared some functions, but do not fear, we’re getting to those right after!
The three functions we have to implement now are:
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Intfunc pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Intfunc pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String?
The first asks for how many components should be in your pickerView. I chose to have 1, but a pickerView with multiple components would look like this:
The reason I chose not to use this was a simple design decision, however my code can be edited to fit something like this as well (maybe a future article?).
Because I decided to keep only one component per pickerView (and not have 3 components in one pickerView), I simply returned 1 in the function as such:
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { return 1}
Now for the two slightly more complicated functions. Let’s start with
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int
This function is asking for the number of rows you want in each component, but since each of our pickerViews have only one component, we can just return a certain integer for each one. We want to return the number of makes for our makePicker, the number of models for our modelPicker, and the number of years for our yearPicker. If we had arrays for all the makes, the models, and the years, all we would need to do is:
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { if pickerView == makePicker { return listOfMakes.count } else if pickerView == modelPicker { return listOfModels.count } else { return listOfYears.count }}
Unfortunately we don’t have listOfMakes, listOfModels, or listOfYears because listOfModels depends on the make that was selected, and listOfYears depends on the model that is selected. To get the listOfMakes we simply get all the makes from our dictionary as such (make sure that makeList is declared in the class as a class variable:
let listOfMakes: [String] = [String]()
let listOfModels: [String] = [String]()
let listOfYears: [String = [String]()
var carlist: Dictionary <String, Dictionary<String,Dictionary<String,String>>> = Dictionary() //assuming you have already populated this data structure.
//the innermost dictionary holds {year: id} and was needed to obtain //the mpg. Without the Id, the structure may look something like //this: Dictionary <String, Dictionary<String,Dictionary<String>>> override func viewDidLoad() {
listOfMakes = Array(carDictionary.keys)
}
We then want the only the models from the first make to show up in our makeViewController when the user views the PickerViews for the first time. So, in my view did load, I add the following lines:
override func viewDidLoad() {
listOfMakes = Array(carDictionary.keys)
listOfModels = (CarList.getAllModels(self.carlist, make: self.listOfMakes[0])).sort()
let firstCar = listOfMakes[0] let modelsDictionary = carDictionary[firstCar]! as Dictionary<String, Dictionary<String, String>> let listOfModels = Array(modelsDictionary.keys)
}
Then do the same for the listOfYears as we need to display the years for only the first model out of the several that the first make contains.
override func viewDidLoad() {
self.listOfMakes = Array(carDictionary.keys)
let firstCar = listOfMakes[0] let modelsDictionary = carList[firstCar]! as Dictionary<String, Dictionary<String, String>> self.listOfModels = Array(modelsDictionary.keys).sort()
let firstModel = listOfModels[0] let yearsDictionary = modelsDictionary[firstModel]! as Dictionary<String, String>
self.listOfYears = Array(yearsDictionary.keys).sort()
}
Once again, I have assumed that you have already populated your “tree-like” data structure.
Now that we have set our lists, our numberOfRowsInComponent function should be complete.
Finally, to populate the pickerView with data, we use the same three arrays and simply put:
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { if pickerView == makePicker { return listOfMakes[row] } else if pickerView == modelPicker { return listOfModels[row] } else { return listOfYears[row] }}
The most important part of this pickerView is that it needs to respond to every time you change one of the PickerViews. Now that we have the two required functions for the delegate, we must implement one additional one in order to add the dynamic characteristic.
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {}
Before we start filling in the function we need to add the following to our class variables:
var makeRow = 0var modelRow = 0var yearRow = 0
Now we can keep track of what row each picker is on, so we can change the other two dynamically (if needed).
We first add the following helper functions that is something very similar to what we did in viewDidLoad.
setModelsList(make: String) { let modelsDictionary = carList[make]! as Dictionary<String, Dictionary<String, String>> self.listOfModels = Array(modelsDictionary.keys).sort()}setYearsList(make: String, model: String) {
let modelsDictionary = carList[make]! as Dictionary<String, Dictionary<String, String>> let modelsList = Array(modelsDictionary.keys)
let yearsDictionary = modelsDictionary[model]! as Dictionary<String, String>
self.listOfYears = Array(yearsDictionary.keys).sort()}
These helper functions allow us easily set us to the list of models and list of years depending on which make has been chosen (for the list of makes) and which make and model have been chosen (for the list of years).
Now, for the actual magic:
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { if pickerView == makePicker { makeRow = row setModelsList(listOfMakes[makeRow]) setYearsList(listOfMakes[makeRow], model: listOfModels[0]) modelPicker.selectRow(0, inComponent: 0, animated: true) yearPicker.selectRow(0, inComponent: 0, animated: true) self.modelPicker.reloadAllComponents() self.yearPicker.reloadAllComponents()} else if pickerView == modelPicker { modelRow = row setYearsList(listOfMakes[makeRow], model: listOfModels[modelRow]) yearPicker.selectRow(0, inComponent: 0, animated: true) self.modelPicker.reloadAllComponents() self.yearPicker.reloadAllComponents()} else { //do anything you want once you have chosen a specific year. }}
At this point, the pickerView should respond to any change in the make or model picker. The
A few notes:
yearPicker.selectRow(0, inComponent: 0, animated: true)
This statement scrolls up the viewPicker up to the top (or index 0, which is what the first parameter represents).
2.
reloadAllComponents()
This feature should be called every time you change the data of each picker view. In this case we changed the data in the model and the year lists, so each had to reloaded to present new values.
Additional features that can be implemented include: displaying the car that is currently chosen, adding the current car with a button, etc. Any questions about the implementation or the features on the gif can be redirected to sumedha.mehta@gmail.com .