Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Using UIPickerView in Swift forms: Tutorial with examples

8 min read 2400

Swift Logo With Rollercoaster in Background

Providing users with form field options to select from, rather than requiring them to manually enter information, can greatly improve the usability of your app. An example is a registration form, where a user may need to enter their state, country, or date of birth.

In this article, we’ll explore using Apple’s UIPickerView to provide users with a selection of data choices presented in a wheel format.

We’ll demonstrate how to add UIPickerView to a Swift form to provide users with choice selections for each field.

Let’s get started!

Jump ahead:

What is UIPickerView?

UIPickerView is a subclass of UIView. It is useful for iOS apps that use a spinning wheel with many choices for the user.

@MainActor class UIPickerView: UIView

One example of this type of app is a slot machine, where a user spins the wheels to play the game. Another classic example is the iOS Clock app, in which the wheel is used to set the alarm. This app uses UIDatePicker, a custom subclass of UIPickerView, to set the alarm time.

Edit Alarm Screen
Edit screen on an iOS Alarm app.

What is the UIPickerViewDataSource protocol?

The UIPickerViewDataSource protocol helps to provide the number of components and rows for the picker view. We conform to this protocol to provide the data source for the picker view like the number of components, and the number of rows in each component, for displaying the picker view data.

This protocol has two methods for providing the counts for the picker view. Both methods are required to be implemented for any objects that conform to the UIPickerViewDataSource protocol.

The first method sets the number of components or columns that the picker view should display:



func numberOfComponents(in pickerView: UIPickerView) -> Int

In the case of the alarm screen, we have two components.

The second method sets the number of rows in each component that the picker view should display:

func pickerView(_ pickerView: UIPickerView, 
     numberOfRowsInComponent component: Int) -> Int

Let’s consider the alarm screen example again. The first component (hours) has 24 rows, and the second component (minutes) has 60 rows. The specified component is a zero-indexed number identifying a component of UIPickerView. The components are numbered from left to right. So, the first component is 0 and the second component is 1.

Now, let’s look at some of UIPickerView’s main methods:

What is the UIPickerViewDelegate protocol?

The UIPickerViewDelegate protocol is used to provide the counts for the picker view to display. However, the UIPickerViewDelegate protocol is used to provide the picker view with the data it will need to construct itself.

@MainActor protocol UIPickerViewDelegate

The UIPickerViewDataSource protocol’s methods return:

  • Row dimensions: the row height and row width for each component
  • Row content: the title, attributed title, or view for each row
  • Row selection: the row selected by a user in a particular component

Setting the row dimensions

The rowHeightForComponent method is used to return the row height for each component that the picker view will need to draw the content on the screen:


More great articles from LogRocket:


optional func pickerView(_ pickerView: UIPickerView, 
              rowHeightForComponent component: Int) -> CGFloat

We return a float value indicating the height of the row in points.

Similarly, we have another method, widthForComponent to set the row width for each component:

optional func pickerView(_ pickerView: UIPickerView, 
              widthForComponent component: Int) -> CGFloat

Setting the row content

In order to provide content for the component’s rows, we must implement either a title, an attributed title, or a view for each row.

The simplest method, titleForRow, provides the title as a string for each row, like so:

optional func pickerView(_ pickerView: UIPickerView, 
              titleForRow row: Int, 
              forComponent component: Int) -> String?

Here, we have a new parameter row, which is a zero-indexed number identifying a row of a component. Remember that the rows are numbered from top to bottom.

To customize the string for more than a basic use case, we can create an attributed string and use the following attributedTitleForRow method:

optional func pickerView(_ pickerView: UIPickerView, 
              attributedTitleForRow row: Int, 
              forComponent component: Int) -> NSAttributedString?

This returns a styled title for the individual rows of the picker view.

N.B. if we implement both of these methods, the picker view will prioritize the attributed title method; if it returns a nil value, then the picker view will use the normal title instead

For advanced customization, we can create and return a whole UIView for each row:

optional func pickerView(_ pickerView: UIPickerView, 
              viewForRow row: Int, 
              forComponent component: Int, 
              reusing view: UIView?) -> UIView

The UIView is called by the picker view when it needs the view to use for a row in a given component. We also have access to the view object that was previously used for a row but is now hidden and cached by the picker view.

We can have a UILabel, UIImageView, or even a custom view to use in the picker view!

Responding to row selection

When the user makes a selection by clicking on a given row in a given component, the UIPickerViewDelegate calls the following method:

optional func pickerView(_ pickerView: UIPickerView, 
              didSelectRow row: Int, 
              inComponent component: Int)

Based on the row that’s selected for a given component, we can store its value in the app.

Next, let’s look at a custom subclass of the UIPickerView for working with a picker that handles date and time.

What is the UIDatePicker subclass?

The UIDatePicker is a subclass of UIPickerView that is used to display dates and times. As mentioned previously, this subclass can be seen in action in the Alarm app on your iPhone.

This subclass has an instance property, datePickerMode, that we can set to have either date, time, time interval, or both date and time. We can also provide minimum and maximum dates. Unlike the delegation pattern of UIPickerView, UIDatePicker uses a target-action pattern for us to get the date or time. iOS14 provides a new date picker style that gives us an inline picker apart from the wheels picker by setting the preferredDatePickerStyle property.

Finally, we add a target on the picker for the valueChanged event:

datePicker.addTarget(self, action: #selector(onDateValueChanged), for: .valueChanged)

@objc func onDateValueChanged(_ datePicker: UIDatePicker) { 
/// Use the latest date to update the view.
}

We’ve covered the basics of UIPickerView, the delegates that will help us to set the number of rows and components, the dimensions, the views, and the action for row selection. Now, we can move forward and create a sample form that uses a picker view, set the delegates, and then implement the corresponding methods!

Demo: Creating a sample Swift form with UIPickerView

To understand a practical example, we’ll create a Swift form with multiple picker views that have both single and double components, with each component having a different number of rows. Then, we’ll have a few labels that display the results when the user manipulates the picker wheel to select a particular row.

This demo will provide you with a thorough understanding of how to use UIPickerView in your own project.

The view controller will have three picker views: one for selecting the type of the income, a second for the source, and a third for the date when the income is received. The second picker will have two components: one for the source and a second that indicates if the income tax is fixed or variable.

N.B., this app demo is just for demonstration purposes, to show everything we can do with pickers; there are other ways to represent this kind of data

Setting the data source

We have different enumerations to represent the various lists of options that a user may choose from:

enum Income: Int, CaseIterable {
  case source
  case type
}

enum IncomeTax: String {
  case fixed
  case variable
}

extension IncomeTax: CustomStringConvertible, CaseIterable {
  var description: String {
    rawValue.capitalized
  }
}

enum IncomeSource: String {
  case salary
  case interest
  case dividend
  case capitalGains = "Capital Gains"
  case royalty
  case rental
}

extension IncomeSource: CustomStringConvertible, CaseIterable {
  var description: String {
    rawValue.capitalized
  }
}

enum IncomeType: String {
  case primary
  case secondary
  case passive
}

extension IncomeType: CustomStringConvertible, CaseIterable {
  var description: String {
    rawValue.capitalized
  }
}

As the enumerations conform to CaseIterable protocol, we can get all the cases from it to show in the picker form.

Creating the picker views

We have three picker views (incomeSourcePickerView, incomeTypePickerView, and incomeMonthPickerView) and three labels (incomeSourceLabel, incomeTypeLabel, and incomeDateLabel)that show the selected data for the particular picker:

class IncomeViewController: UIViewController {
  private let incomeSourcePickerView: UIPickerView = {
    let pickerView = UIPickerView()
    pickerView.translatesAutoresizingMaskIntoConstraints = false
    return pickerView
  }()

  private let incomeTypePickerView: UIPickerView = {
    let pickerView = UIPickerView()
    pickerView.translatesAutoresizingMaskIntoConstraints = false
    return pickerView
  }()

  private let incomeMonthPickerView: UIDatePicker = {
    let datePicker = UIDatePicker()
    datePicker.datePickerMode = .date
    datePicker.preferredDatePickerStyle = .wheels
    datePicker.translatesAutoresizingMaskIntoConstraints = false
    return datePicker
  }()

  private let incomeSourceLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
  }()

  private let incomeTypeLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
  }()

  private let incomeDateLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
  }()
}

Then we have three types for defining the data of the picker views:

private let sources = IncomeSource.allCases
private let types = IncomeType.allCases
private let taxes = IncomeTax.allCases

Setting up the constraints

Since we’re creating the picker views programmatically, we’ll add the constraints for it manually. The source picker view is on the top, followed by the type picker, and the date picker is last. We’ll add these views inside a stack view, like so:

private func setupStackView() {
  stackView.addArrangedSubview(incomeTypePickerView)
  stackView.addArrangedSubview(incomeTypeLabel)

  stackView.addArrangedSubview(incomeSourcePickerView)
  stackView.addArrangedSubview(incomeSourceLabel)

  stackView.addArrangedSubview(incomeDatePicker)
  stackView.addArrangedSubview(incomeDateLabel)

  view.addSubview(stackView)

  stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
  stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}

With the constraints set up, let’s add the delegate and data source for the picker views. UIPickerView uses delegates to communicate, but UIDatePicker uses a target action pattern, as mentioned earlier.

override func viewDidLoad() {
  super.viewDidLoad()

  view.backgroundColor = .white

  incomeTypePickerView.delegate = self
  incomeTypePickerView.dataSource = self

  incomeSourcePickerView.delegate = self
  incomeSourcePickerView.dataSource = self

  setupStackView()

  incomeDatePicker.addTarget(self, action: #selector(onIncomeDateValueChanged), for: .valueChanged)
}

@objc private func onIncomeDateValueChanged(_ datePicker: UIDatePicker) {
  incomeDateLabel.text = datePicker.date.formatted(date: .long, time: .omitted)
}

When the date value is changed, we get the date from the date picker, format it to show a long date, omit the time, and show it in incomeDateLabel.

After setting the delegate and dataSource for both picker views to self, we have to provide the corresponding methods of both protocols.

Providing the UIPickerViewDataSource methods

First, we’ll provide the UIPickerViewDataSource methods. We have to provide the number of components and the number of rows in each component for each picker view:

// MARK: - UIPickerViewDataSource
extension IncomeViewController: UIPickerViewDataSource {
  func numberOfComponents(in pickerView: UIPickerView) -> Int {
    // 1
    let incomePicker = Income(rawValue: pickerView.tag)

    // 2
    switch incomePicker {
      case .type:
        return 1
      case .source:
        return 2
      default:
        return 0
    }
  }

  func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    let incomePicker = Income(rawValue: pickerView.tag)

    // 3
    switch incomePicker {
      case .type:
        return types.count
      case .source:
        // 4
        let sourceComponent = IncomeSourcePicker(rawValue: component)

        // 5
        switch sourceComponent {
          case .sourceName:
            return sources.count
          case .taxType:
            return taxes.count
          default:
            return 0
        }
      default:
        return 0
    }
  }
}

Let’s review this code.

  1. First, we create an enumeration of the type Income from the picker view’s tag property. We previously set the tag for each picker view; this helps us to uniquely identify the view
  2. Then, based on the type of picker view, we return the number of components. Here, the income .type: picker view has 1 component (column), whereas income .source: picker view has 2 components (columns)
  3. Similarly, for the number of rows in each component, we get the type of the picker view and switch over the values. For the income .type: picker view, we return the count of all types
  4. For the income .source: picker view, we have an interesting case as it has two different components. We create an enumeration, IncomeSourcePicker, from the component’s integer value
  5. Finally, we return all the sources and taxes for the given cases of IncomeSourcePicker

Providing the UIPickerViewDelegate methods

Next, we’ll provide the UIPickerViewDelegate methods. This involves displaying the title for each row and responding to user selection of a row.

// MARK: - UIPickerViewDelegate
extension IncomeViewController: UIPickerViewDelegate {
  func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    // 1
    let incomePicker = Income(rawValue: pickerView.tag)

    switch incomePicker {
      case .type:
        return types[row].description
      case .source:
        let sourceComponent = IncomeSourcePicker(rawValue: component)

        switch sourceComponent {
          case .sourceName:
            return sources[row].description
          case .taxType:
            return taxes[row].description
          default:
            return nil
        }
      default:
        return nil
    }
  }

  func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    let incomePicker = Income(rawValue: pickerView.tag)

    switch incomePicker {
      case .type:
        incomeTypeLabel.text = "Income type is \(types[row].description)."
      case .source:
        let sourceComponent = IncomeSourcePicker(rawValue: component)

        // 2
        switch sourceComponent {
          case .sourceName:
            incomeSourceLabel.text = "Income source is \(sources[row].description)."
          case .taxType:
            incomeSourceLabel.text = "\(taxes[row].description) taxes."
          default: ()
        }
      default: ()
    }
  }
}

Again, let’s review the above code:

  1. Similar to our previous process, we get the picker view based on its tag. Next, we switch over its values and then provide the corresponding description as the title for the row for each component
  2. When the user selects a row, the didSelectRow method is called. We use this method to update the labels for the particular row and component

We’ve set up two picker views and one date picker! Now, let’s build and run the app to see how it looks when the user manipulates the different pickers:

Income Type

Conclusion

For views that require selecting from multiple options, consider using UIPickerView in your iOS app. To provide an option for your user to select a date and time, UIDatePicker is the go-to view. With the delegation pattern in UIPickerView, you provide and handle the data in your app and use a target-action pattern for UIDatePicker.

In this article, we explored working with UIPickerView and h UIDatePicker. as well as the various methods available for you to use. We also explored a practical example, using UIPickerView in a Swift form, to provide you with a solid understanding of working with picker views.

I hope you find this article helpful and will try using UIPickerView in your iOS apps!

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

.
Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Leave a Reply