Data Entry with UIPickerView

The keyboard can be a curse. Trying to validate data via keyboard can be a nightmare for many projects. One solution to this problem is using a UIPickerView to limit the values the user can enter. In this lesson, we’ll explore the UIPickerView for numerical input.

UIPickerViews are are series of wheel like scrolling objects. For more on the basics, you can read this article on them. They are made of series of components which contain rows of strings. Picker views use delegates and data sources for most of their functionality instead of actions. More often than not I see people using them totally and completely wrong. Often they are lazy man’s table views or a control exploited to pack more information than a user can handle. What they are good at is giving one pice of data with multiple parts. The digits of a number for example, or Apple’s subclass of UIPickerView UIDatePickerView, which returns a date.

Set Up the Project

For this project you’ll find a started file  advanced-picker-demo-_startwhere I added a label, picker view and a segmented control to a storyboard view controller.

2016-12-12_07-29-51

I gave some specific titles on the segmented control, 99.9, 999.9,59:59.99, and 99x, which I’ll discuss in a moment. I added an outlet for the picker and label, and an action and outlet for the segmented control.

 @IBOutlet weak var displayLabel: UILabel!
 @IBOutlet weak var picker: UIPickerView!
 @IBOutlet weak var segmentedControl: UISegmentedControl!
 @IBAction func segmentedControl(_ sender: UISegmentedControl) {
 }

Again you can find this in the starter file advanced-picker-demo-_start if you wish.

Setting Up a Picker View.

Picker views don’t have actions, but delegates and data sources, just like table views. Adopt the delegate and data source in the class declaration.

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource

Add two properties to the view controller class. components will hold the components for the UIPickerView and resultString will be the string that the picker view outputs.

var components = [[String]]()
var resultString = ""

The components property is an array of string arrays. The outer array is each digit of the picker, called a component. The inner array is the string title for a given row in the the component. The two data sources tell the picker how many elements are in the outer and inner arrays. For the outer array, Add the following:

 //:MARK - Delegates and data sources
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return components.count
    }
 

For the number of rows in each inner array, add the following:

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return components[component].count
    }

There’s two  more or less mandatory delegate methods for picker views . One we’ll add now, the other a bit later. Add the following to show the titles in the picker view

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    return components[component][row]
}

Adding Component Content

The delegate and data sources you just added are pretty generic. The content to the components property are where the magic happens. Add the following function that returns a string array. We’ll use it to set a component on the picker view.

func numberPickerComponent(from char:Character) -> [String]{
    switch char{
        case "9":
            return ["0","1","2","3","4","5","6","7","8","9"]
        case "5":
            return ["0","1","2","3","4","5"]
        case "x":
            return ["0"," 1/16","1/8","3/16","1/4","5/16","3/8","7/16","1/2","9/16","5/8","11/16","3/4","13/16","7/8","15/16"]
        default:
            return [String(char)]
    }
}

This function returns an array of strings that starts at 0 and goes to the digit indicated. You could make a case for all the numbers between 0 a 9, but I only picked a few for the purposes I have in mind. Most decimal numbers will be 0 through 9. Time functions will need 0 to 5 for minutes and seconds. The x will stand for fractions. I set my fractions to increments of 1/16 to 15/16. The default returns a string of a single character for all other cases.

With these four cases, you can create formats in your picker from a control string. I stuck those control strings in the segmented control, but you could assign them directly of course. A string of 99.9 is a value from 0 to 99.9. A string of 99x is a string of 0 to 99 15/16. While the UIDatePickerView gets times, if you need a TimeInterval for values with less than second, you’ll find it difficult. 59:59.99 give us times from 0 seconds to 59 minutes 59.99 seconds or 3599.99 seconds.

Add the function to take a string and make it into an array of components. This iterates over the string, calls the numberPickerComponent function you just defined to grab an array, and adds that array to the components array. Once assembled, the function returns the completed array.

 func numberPickerComponents(from string:String)->[[String]]{
        
        var components = [[String]]()
        for char in string.characters{
            components += [numberPickerComponent(from:char)]
        }
        return components
    }

Since I’m changing the pickerComponents in a segmented control, on a change of segment, the code updates the picker. It grabs the string for the segment’s titles and makes that the components. Add this:

    @IBAction func segmentedControl(_ sender: UISegmentedControl) {
        let index = sender.selectedSegmentIndex
        let pickerComponentString = segmentedControl.titleForSegment(at: index)!
        components = numberPickerComponents(from: pickerComponentString)
        resetPicker()
            }

This function calls a function we haven’t defined yet, resetPicker. There’s two jobs for the resetPicker function. One is to reload the components of the picker so the correct ones are there. the reloadAllComponenets method of UIPickerView does that. The second can be a bit tricky. The wheels will be inaccurate if you leave the setting from one format to another. I took the simplest approach and zero the components with a loop that selects the first element in each component. The selectRow:inComponenet:Animated: method can animate the change, which I set to true to roll the components back to zero. Add this to your code:

 func resetPicker(){
     picker.reloadAllComponents()
     for index in 0..<components.count{
       picker.selectRow(0, inComponent: index, animated: true)
     }
 }

This code initializes all this in viewDidLoad, setting the delegate and data source to self, and setting the initial segement control, picker view and components to the first segment. Add this to viewDidLoad:

override func viewDidLoad() {
   super.viewDidLoad()
   picker.dataSource = self
   picker.delegate = self
   segmentedControl.selectedSegmentIndex = 0
   let pickerComponentString = segmentedControl.titleForSegment(at: 0)!
   components = numberPickerComponents(from: pickerComponentString)
}

That sets the picker to display correctly. We’d like some output, in this case a string we’ll parse later. The last delegate function is the selection function. I’ll output the selection to the label. Add this to your code where you placed your data sources.

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        resultString = ""
    
        displayLabel.text = resultString
    }

This is another tricky spot. The parameters of the selection method only gives us the component that changed, not all the components. There’s a picker view method selectedRow(inComponent:) which can get us the index of any component. I loop through all the components and concatenate each to resultString. Add this below resultString:

for index in 0..<components.count{
    let digit = components[index][pickerView.selectedRow(inComponent: index)]
    resultString += digit
}

This almost works. It still has a bug. If using fractions, this code will make 33 1/3 be 331/3. For cases where the component is larger than one character, add an extra space. Add this under the let digit assignment:

if digit.characters.count > 1 {
    resultString += " " 
}

Build and run. The picker view will look like this.

2016-12-12_07-31-26
You can dial a number of 3.1.

2016-12-12_07-32-14
Set the segmented control to 999.99. Dial up 203.14.
2016-12-12_07-33-06

Go to the time of 59:99.00, put a time of 12:54.37 and you get this
2016-12-12_07-34-30
Finally click on 99X for the fractions and try 12 3/8
2016-12-12_07-35-25

You get the values showing up as a string in the title. However you may not want a string for any of these, but a double instead. We’ll need to parse the string into a number. In next week’s lesson, we’ll tackle that part of the picker.

The Whole Code

You can find the finished lesson here for download:advanced-picker-demo.

//
//  ViewController.swift
//  Advanced Picker Demo
//
//  Created by Steven Lipton on 12/12/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit

class ViewController: UIViewController, UIPickerViewDelegate,UIPickerViewDataSource {
    
    var components = [[String]]()
    var resultString = ""
    
    
    @IBOutlet weak var displayLabel: UILabel!
    @IBOutlet weak var picker: UIPickerView!
    @IBOutlet weak var segmentedControl: UISegmentedControl!

    @IBAction func segmentedControl(_ sender: UISegmentedControl) {
        let index = sender.selectedSegmentIndex
        let pickerComponentString = segmentedControl.titleForSegment(at: index)!
        components = numberPickerComponents(from: pickerComponentString)
        resetPicker()
    }
    
    func numberPickerComponent(from char:Character) -> [String]{
        switch char{
        case "9":
            return ["0","1","2","3","4","5","6","7","8","9"]
        case "5":
            return ["0","1","2","3","4","5"]
        case "x":
            return ["0"," 1/16","1/8","3/16","1/4","5/16","3/8","7/16","1/2","9/16","5/8","11/16","3/4","13/16","7/8","15/16"]
        default:
            return [String(char)]
        }
    }
    
    func numberPickerComponents(from string:String)->[[String]]{
        
        var components = [[String]]()
        for char in string.characters{
            components += [numberPickerComponent(from:char)]
        }
        return components
    }
    func resetPicker(){
        picker.reloadAllComponents()
        for index in 0..<components.count{ picker.selectRow(0, inComponent: index, animated: true) } } override func viewDidLoad() { super.viewDidLoad() picker.dataSource = self picker.delegate = self segmentedControl.selectedSegmentIndex = 0 let pickerComponentString = segmentedControl.titleForSegment(at: 0)! components = numberPickerComponents(from: pickerComponentString) } //:MARK - Delegates and data sources func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return components.count
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return components[component].count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return components[component][row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        resultString = ""
        for index in 0..<components.count{ let digit = components[index][pickerView.selectedRow(inComponent: index)] if digit.characters.count > 1 {//add space if more than one character
                resultString += " " //add space if more than one character
            }
            resultString += digit
        }
        displayLabel.text = resultString
    }

}

One thought on “Data Entry with UIPickerView”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s