Back

TechnologyDec 05, 2016

Creating a Dropdown Field in Swift for iOS

Zachary Slayter

Recently, I was working on a native iOS app that needed a couple dropdowns added to a page. To my surprise, there was no native component that accomplished what I needed. Through a little bit of research, I found a commonly used method that would work for certain scenarios, but proved buggy and needlessly cluttered the code. Therefore, I created a new method that is cleaner, without bugs (as far as I know), and supports loading of dynamic data in the dropdowns.

the problem

For this post, I will stick with a very simple dropdown: Choosing a salutation in a registration form. For simplicity’s sake, the options will be to choose an empty string, “Mr.”, “Ms.”, or “Mrs.” The dropdown will be displayed as a text field on the page, with an arrow on the right side to indicate that options will expand if the user taps on the field. When the user taps on the field, a native picker view will be presented at the bottom of the screen, allowing the user to select one of the options. When an option is selected, the text field will be populated with the selected option, as seen below.

method 1

The first method is the method that you will find most often if you search for a solution online. Its one advantage is that it takes care of everything in one file. The disadvantage is that the file is the view controller, which generally doesn’t need any more clutter.

import Foundation   class MyViewController : UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {   @IBOutlet weak var pickerTextField : UITextField!   let salutations = ["", "Mr.", "Ms.", "Mrs."]   override func viewDidLoad() { super.viewDidLoad()   let pickerView = UIPickerView() pickerView.delegate = self   pickerTextField.inputView = pickerView }   // Sets number of columns in picker view func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { return 1 }   // Sets the number of rows in the picker view func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return salutations.count }   // This function sets the text of the picker view to the content of the "salutations" array func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return salutations[row] }   // When user selects an option, this function will set the text of the text field to reflect // the selected option. func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { pickerTextField.text = salutations[row] }   }

While the sample view controller looks fine, keep in mind that the only thing it is doing is managing this dropdown. If you have multiple dropdowns on the page, you have to put “if” statements in the four delegate functions and handle each dropdown separately. Also, if you try changing the data that a dropdown is presenting after the page loads, you will get very buggy results using this approach. If, however, you only have one dropdown, and the view controller isn’t already taking care of many other components, then this approach could work. Though personally I still wouldn’t use it because you never know when requirements are going to change and you will need to add another dropdown.

method 2

This method uses two files, an extension to the UITextField and a subclass of the UIPickerView, to turn the 25 lines from the above method to two to three lines in your view controller. The first file, the extension to UITextField, adds a function to load dropdown data into a text field. This function creates an instance of the custom picker view and assigns it as the input view of the text field.

import Foundationimport UIKit   extension UITextField { func loadDropdownData(data: [String]) { self.inputView = MyPickerView(pickerData: data, dropdownField: self) }}

The second file, the custom picker view, takes care of populating the picker view with the data and populating the text field with the selected option. With this code in its own file, you will never need to write it again, no matter how many dropdowns you are using.

import Foundation   class MyPickerView : UIPickerView, UIPickerViewDataSource, UIPickerViewDelegate {   var pickerData : [String]! var pickerTextField : UITextField!   init(pickerData: [String], dropdownField: UITextField) { super.init(frame: CGRectZero)   self.pickerData = pickerData self.pickerTextField = dropdownField   self.delegate = self self.dataSource = self   dispatch_async(dispatch_get_main_queue(), { if pickerData.count > 0 { self.pickerTextField.text = self.pickerData[0] self.pickerTextField.enabled = true } else { self.pickerTextField.text = nil self.pickerTextField.enabled = false } }) }   required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }   // Sets number of columns in picker view func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { return 1 }   // Sets the number of rows in the picker view func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return pickerData.count }   // This function sets the text of the picker view to the content of the "salutations" array func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return pickerData[row] }   // When user selects an option, this function will set the text of the text field to reflect // the selected option. func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { pickerTextField.text = pickerData[row] }     }

With these two files in your project, your view controller can now look like this:

import Foundation   class MyViewController : UIViewController {   @IBOutlet weak var pickerTextField : UITextField!   let salutations = ["", "Mr.", "Ms.", "Mrs."]   override func viewDidLoad() { super.viewDidLoad()   pickerTextField.loadDropdownData(salutations) }}

This code looks cleaner, is loosely coupled with the view controller, and if you ever want to change the data, you can just call the loadDropdownData function again with different data and it will change seamlessly on the device. Also, any time you need to duplicate this behavior in the future, it will take considerably less code and less time to implement. Apart from the ease of implementation, I have tested both of these methods and have found the second method to be much more reliable, and it does not produce some of the buggy behavior that method one has when loading dynamic data.

event handlers

For those of you who are looking for the default dropdown behavior and nothing more, then the above code should be sufficient for you. However, I have needed to add custom event handling to specific dropdowns. For example, when an option is selected, an image should show up on the screen. To do this, you will need to add function pointers to the custom picker view and pass a reference to the function through to the picker view. The picker view calls this function on initialization and when an item is selected. This will allow you to change the behavior of the view depending on the option selected. The new code with the selection handler included is below and anything new is highlighted.

import Foundationimport UIKit   extension UITextField { func loadDropdownData(data: [String]) { self.inputView = MyPickerView(pickerData: data, dropdownField: self) }   func loadDropdownData(data: [String], onSelect selectionHandler : (selectedText: String) -> Void) { self.inputView = MyPickerView(pickerData: data, dropdownField: self, onSelect: selectionHandler) }}import Foundation   class MyPickerView : UIPickerView, UIPickerViewDataSource, UIPickerViewDelegate {   var pickerData : [String]! var pickerTextField : UITextField! var selectionHandler : ((selectedText: String) -> Void)?   init(pickerData: [String], dropdownField: UITextField) { super.init(frame: CGRectZero)   self.pickerData = pickerData self.pickerTextField = dropdownField   self.delegate = self self.dataSource = self   dispatch_async(dispatch_get_main_queue(), { if pickerData.count > 0 { self.pickerTextField.text = self.pickerData[0] self.pickerTextField.enabled = true } else { self.pickerTextField.text = nil self.pickerTextField.enabled = false } })   if self.pickerTextField.text != nil && self.selectionHandler != nil { selectionHandler(selectedText: self.pickerTextField.text!) } }   init(pickerData: [String], dropdownField: UITextField, onSelect selectionHandler : (selectedText: String) -> Void) { self.selectionHandler = selectionHandler   self.init(pickerData, dropdownField) }   required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }   // Sets number of columns in picker view func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { return 1 }   // Sets the number of rows in the picker view func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return pickerData.count }   // This function sets the text of the picker view to the content of the "salutations" array func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return pickerData[row] }   // When user selects an option, this function will set the text of the text field to reflect // the selected option. func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { pickerTextField.text = pickerData[row]   if self.pickerTextField.text != nil && self.selectionHandler !=nil { selectionHandler(selectedText: self.pickerTextField.text!) } }}

The following code sample uses the selection handler to print a string to the console depending on which salutation the user selected.

import Foundation   class MyViewController2 : UIViewController {   @IBOutlet weak var pickerTextField : UITextField!   let salutations = ["", "Mr.", "Ms.", "Mrs."]   override func viewDidLoad() { super.viewDidLoad()   pickerTextField.loadDropdownData(salutations, salutations_onSelect) }   func salutations_onSelect(selectedText: String) { if selectedText == "" { print("Hello World") } else if selectedText == "Mr." { print("Hello Sir") } else { print("Hello Madame") } }}

This method is dynamic and does a great job filling a gap in the iOS native components. It can be easily customized to meet the needs of your project, and it could save you a lot of time and frustration. I hope you find it useful.