Tag Archives: UIPickerView in Swift

This Old App: Why I Hate PickerViews, but Learned To Love Them.

A few months ago, Apple removed my app Interval RunCalc from the App Store for being too old. In this series of articles I’ll document what I did on rebuilding and improving the app. In the last installment, I placed my new model into the old app, fixed a lot of errors from taking 2013 Objective-C code in 2017’s Xcode and make a quick, temporary view and controller to test it. Next, I’ll replace text fields I used for input. My best choice for this bothers me. UIPickerView is one of my biggest love/hate relationship in all of iOS development.

The app from my last installment looks something like this:

While this is okay for testing the model, it is not something I’d want for users. There’s a principle I’d like to introduce: Get Good, Get Fast. When working with user interfaces, get the data from your user as quickly as possible. The more time it takes a user to input data, the less likely he or she will use the app. They will get frustrated. What they enter must be good data. Data must makes sense to the application. We refer to this as valid data.

The current user interface is fast since I can type on the keyboard, but fails miserably in the validation arena. Because I’m using a keyboard and my values from the keyboard are strings, there’s a lot of data validation code I need to be sure I’m getting good data from the user.

In the version 1.0 of Interval RunCalc, I used keyboards. I tried controlling data validation by breaking up numbers into parts. For example, time used a special controller that broke up time into three integers were easier to input but restricted themselves to certain values

I did this for HH:MM:SS, MM:SS and decimal values. Problem was I had those values everywhere in the application, so my storyboard ended up a huge mess of segues.

My rationale at the time was simple: I didn’t want to use a UIPickerView for input. I hated UIPickerViews. There’s two reasons I hate them. Picker views break the Get Fast Get Good principle on the get fast side. They are effective for no more than ideally ten to twenty items. The classic picker view was one like this with 100 components.

It takes forever to get to higher numbers, breaking get fast. I get frustrated with these in other apps, and did not want to subject my users to such a thing.

The other objection separates new developers from seasoned ones. New developers, upon learning about picker views want to abuse them. They’ll put everything in picker views, and several on the same view. They’ll throw insane amounts of data in the most confusing ways, often making the UI unusable. For example here’s six components and two picker views. You can only read it in landscape

This would take forever to input data and gets very confusing fast. I get more weird questions about bizarre uses for picker views than any other control. Most of these developers do not think like users. Users hate picker views because they are confusing and take forever to use.

There is one strength for UIPickerView: Validation. UIPickerViews constrain input. Apple uses this for the date picker, taking four elements and combining them to get a Date value.

This is how picker views are supposed to be used. But even here I hate anything over 20 rows, like the date.

The date picker gives us the clue of how to use them right: Separate components into discrete elements than can have only constrained values. Make those elements with as few elements as possible. I set a limit of twenty rows. The keyboard input was a horrible mess in version 1.0. It never worked right. Despite my objections to them, I decided to use a picker view for my input to avoid all the data validation. I want one class of UIPickerView controllers. It will adapt to the different scenarios I need: HH:MM:SS, HH:MM:SS, and two digit decimal. I’ll make this a modal view instead of the navigation view I used in the original application to get rid of all those segues. I wrote my storyboard for it, which I placed on my Main.Storyboard:

This looks like a navigation controller, but it is an independent view controller on my storyboard, which will be a modal controller. I set a storyboardID of InputController. I made an illusion of a toolbar from two buttons, a label, and a UIView behind them, arranged with some auto layout.

This is a very flexible input system, giving the user the units I’m using and giving me power over all the buttons and labels. I’ll make a view controller PickerViewController for it, and I’ll add the following outlets and actions to that view controller:

@IBOutlet weak var backButton: UIButton!
@IBOutlet weak var doneButton: UIButton!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var toolBar: UIView!
@IBOutlet weak var unitsOfMeasureLabel: UILabel!
@IBOutlet weak var picker: UIPickerView!

@IBAction func didPressDone<span class="hljs-params">(_ sender: UIButton) {

}

@IBAction func didPressBack<span class="hljs-params">(_ sender: UIButton) {

}


Adding the Picker View 

In this article I’m more interested in the application than the implementation of a picker view. I’ll be leaving out steps. If you need to understand picker views and picker components, I’ve written out the whole process in detail which you can find here. I’ll be making a variation of that code, but understanding the way I did that will help you here.

My picker view is based on digits. No component will have more than 10 rows, speeding up the input process. Looking at the types of components I have:

  • Decimal digit – values of 0,1,2,3,4,5,6,7,8,9
  • Tens of minutes and seconds – values of 0,1,2,3,4,5
  • Time separator – a colon(:)
  • Decimal separator – a period(.)

I could add constants for this to my code, making four types of component wheels to add to my code and making some string arrays to do this.

let decimalDigit = ["0","1","2","3","4","5","6","7","8","9"]
let timeDigit = ["0","1","2","3","4","5"]
let timeSeperator = [":"]
let decimalSeperator = ["."]

That’s what I did in the article I mentioned above. There is another way to do this based on the row number and a bit of control logic. Instead of constants, I’ll use an enumeration.

enum ComponentType {

&nbsp;&nbsp;&nbsp;&nbsp;case ten,six,colon,decimal

&nbsp;&nbsp;}

I’ll make an array of this enum.

private var components = [ComponentType]()

This way I can describe any set of wheels quickly. MM:SS is [.six,.ten,.colon,.six.ten].The number of components are the size of this array

func numberOfComponents(in pickerView: UIPickerView) -&gt; Int {

&nbsp;&nbsp;&nbsp;&nbsp;return components.count

&nbsp;&nbsp;}

For HH:MM, this would return 5. I’ll set the number of components in my datasource, based on the component type:

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    switch components[component]{
        case .ten: return 10
&nbsp;&nbsp;&nbsp;&nbsp;    case .six: return 6
&nbsp;&nbsp;&nbsp;&nbsp;    case .colon,.decimal: return 1
&nbsp;&nbsp;  }
}

Since I’m not using a string array to give me the title for a row in a component, I use the row number. The digit I want to display is equal to its row number. Make a formatted string of the row number. The exceptions are colons and decimal points, which I’ll explicitly return. I display the correct digit like this:

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
&nbsp;&nbsp;&nbsp;&nbsp;switch components[component]{
&nbsp;&nbsp;&nbsp;&nbsp;    case .ten: return String(format: "%01i", row)
    &nbsp;&nbsp;&nbsp;&nbsp;case .six: return String(format: "%01i", row)
    &nbsp;&nbsp;&nbsp;&nbsp;case .colon: return ":"
    &nbsp;&nbsp;&nbsp;&nbsp;case .decimal: return "."
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}&nbsp;&nbsp;

This all depends on the correct values in the components array. I’ll add another enum, listing input formats. I have three formats for that enumeration: a double with two dight decimal, MM:SS and HH:MM:SS.

enum PickerType {

&nbsp;&nbsp;&nbsp;&nbsp;case twoDecimal,minuteSecond,hourMinuteSecond

&nbsp;&nbsp;}

I’ll write a function to set the components.

&nbsp;func setPickerComponents(for pickerType:PickerType){
&nbsp;&nbsp;&nbsp;&nbsp;self.pickerType = pickerType

&nbsp;&nbsp;&nbsp;&nbsp;switch pickerType{
&nbsp;&nbsp;&nbsp;&nbsp;    case .twoDecimal:<span class="hljs-comment">//999.99 max value
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;     components = [.ten,.ten,.ten,.decimal,.ten,.ten]&nbsp;&nbsp;&nbsp;&nbsp;
        case .minuteSecond: <span class="hljs-comment">//59:59 max value
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;     components = [.six,.ten,.colon,.six,.ten]
&nbsp;&nbsp;&nbsp;&nbsp;    case .hourMinuteSecond: <span class="hljs-comment">//99:59:59 max value
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;components = [.ten,.ten,.colon,.six,.ten,.colon,.six,.ten] &nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;}

The class has two data properties. When I present the view controller, I’ll set these two properties.

var units = Units.meters

&nbsp;var value = 0.0&nbsp;&nbsp;

One is the units of measure and the current value. Units is another enumeration which lists all the app’s units of measure:

enum Units {
&nbsp;&nbsp;&nbsp;&nbsp;case 
       meters,kilometers,miles,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;milesPerHour,minutesSecondsPerMile,kilometersPerHour,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hoursMinutesSecond
&nbsp;&nbsp;}

There’s a lot of cosmetic changes I’ll make depending on the units, such as displaying the units and if this is a speed, distance or time variable. I wrote a method configure to handle all this, leaving some methods to do a lot of the repetitive configuration.

func configure<(for units:Units){

&nbsp;&nbsp;&nbsp;&nbsp;switch units{
&nbsp;&nbsp;&nbsp;&nbsp;case Units.meters:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Meters"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;distance()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.kilometers:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Kilometers"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;distance()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.miles:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Miles"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;distance()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.milesPerHour:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Miles per Hour"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paceOrSpeed()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.minutesSecondsPerMile:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setMinutesSeconds()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Minutes per Mile"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paceOrSpeed()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.kilometersPerHour:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Kilometers per Hour "
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paceOrSpeed()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.hoursMinutesSecond:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setHoursMinutesSeconds()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Time"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;time()
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}

I’ll add three methods for configuring the types.

func setTwoDecimal<span class="hljs-params">(){
&nbsp;&nbsp;setPickerComponents(for: .twoDecimal)
}

func setHoursMinutesSeconds<span class="hljs-params">(){
&nbsp;&nbsp;setPickerComponents(for: .hourMinuteSecond)&nbsp;&nbsp;
} &nbsp;

func setMinutesSeconds<span class="hljs-params">(){
&nbsp;&nbsp;setPickerComponents(for: .minuteSecond)
}


For the user interface side of things, I set the background color and title depending on the units so users have visual cues:

func distance(){
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "Distance"
&nbsp;&nbsp;&nbsp;&nbsp;backgroundColor = UIColor.cyan
&nbsp;&nbsp;}

&nbsp;&nbsp;func paceOrSpeed(){
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "Pace / Speed"
&nbsp;&nbsp;&nbsp;&nbsp;backgroundColor = UIColor.yellow
&nbsp;&nbsp;}

&nbsp;&nbsp;func time(){
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "Time"
&nbsp;&nbsp;&nbsp;&nbsp;backgroundColor = UIColor.green
&nbsp;&nbsp;}&nbsp;&nbsp;

The First Test

I have enough code to test the picker now. I’ll set up a button on my test storyboard and add some code to configure the picker. I’ll add a button Picker.

I’ll make an action for this button in RootViewController.swift. This input controller is intended for modal controllers. Modals launching for a storyboard identifier makes sense in this context. I use it multiple times. It clears all that segue spaghetti I had in version 1.0. I’ll launch the controller in a modal view using the default values.

@IBAction func pickerButton<span class="hljs-params">(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;let vc = storyboard?.instantiateViewController(withIdentifier: "InputController") as! PickerViewController
&nbsp;&nbsp;&nbsp;&nbsp;present(vc, animated: <span class="hljs-literal">true, completion: <span class="hljs-literal">nil)
&nbsp;&nbsp;}

I’ll go back to the PickerViewController.swift file and add dismissal code in my actions.

@IBAction func didPressDone(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;self.dismiss(animated: true, completion: nil)
&nbsp;&nbsp;}

@IBAction func didPressBack(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;self.dismiss(animated: true, completion: nil)
&nbsp;&nbsp;}

One thing I forgot – to call configure. I’ll do that on the viewDidLoad of PickerViewController.swift

override func viewDidLoad() {
&nbsp;&nbsp;&nbsp;&nbsp;super.viewDidLoad()
&nbsp;&nbsp;&nbsp;&nbsp;picker.dataSource = self
&nbsp;&nbsp;&nbsp;&nbsp;picker.delegate = self
&nbsp;&nbsp;&nbsp;&nbsp;configure(for: self.units)
&nbsp;&nbsp;}

I’m ready to run. When I build and run, then press the picker button, I get a beautiful interface, where I can roll the individual digits.

I notice a potential bug. I ran a quarter of a marathon or 10.56 Km last weekend. That’s 10557.30 meters, which I could not enter into the app since I don’t have enough digits. I could add another two digits for 99999.99 for meters, but before I do, I’ll ask myself the question: will the user enter anything in meters?

This app is for distance runners. Distances are in kilometers or miles, never meters. That would be a change that has no use. I only care about what the user wants here, not my internal measurement system of meters. I’ll skip that change.

I do another test. I add the unit of measure in the action in RootViewController for a pace.

@IBAction func pickerButton(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;let vc = storyboard?.instantiateViewController(withIdentifier: "InputController") as! PickerViewController
&nbsp;&nbsp;&nbsp;&nbsp;vc.units = .minutesSecondsPerMile
&nbsp;&nbsp;&nbsp;&nbsp;present(vc, animated: true, completion: >nil)
&nbsp;&nbsp;}

Building and running again, I can spin the wheels there too.

Setting Values

I want to set the picker to an initial value. Inside the picker’s code I‘ll change value to a series of row indices representing the number, one digit per component. I’ll convert value to a string, then use the characters to represent rows.

If I had a 11:23 minute/mile setting, I set the string to "11:23" The code looks at a character from the left to the right as the row index for the component. The functions Int(String(character)) get me this index used in a for loop. I use the selectRow method to select the row with that number.

picker.selectRow(row, inComponent: component, animated: animated)

The Int constructor produces a nil value for any character that isn’t a digit. Those are my separators, and they always have a row value of 0. The code looks like this:

func setComponentValues(from str:String){&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;var component = 0
&nbsp;&nbsp;&nbsp;&nbsp;for character in str.characters{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if let row = Int(String(character)){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;picker.selectRow(row, inComponent: component, animated: animated)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;picker.selectRow(0, inComponent: component, animated: animated)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;component += 1
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}

I made three functions to set the value depending on the PickerType. If their sparse code didn’t make sense before, they will now. Each converts the value to a string for that PickerType, then converts that string to the correct rows in the picker components

func setTwoDecimal(){
&nbsp;&nbsp;&nbsp;&nbsp;setPickerComponents(for: .twoDecimal)
&nbsp;&nbsp;&nbsp;&nbsp;let str = String(format:"%06.2f",value)
&nbsp;&nbsp;&nbsp;&nbsp;setComponentValues(from: str)
&nbsp;&nbsp;}

&nbsp;&nbsp;func setHoursMinutesSeconds(){
&nbsp;&nbsp;&nbsp;&nbsp;setPickerComponents(for: .hourMinuteSecond)
&nbsp;&nbsp;&nbsp;&nbsp;let hours = Int(value + 0.5) / 3600
&nbsp;&nbsp;&nbsp;&nbsp;let hoursRemainder = Int(value + 0.5) % 3600
&nbsp;&nbsp;&nbsp;&nbsp;let minutes = hoursRemainder / 60
&nbsp;&nbsp;&nbsp;&nbsp;let seconds = hoursRemainder % 60
&nbsp;&nbsp;&nbsp;&nbsp;let str =&nbsp;String(format:"%02i:%02i:%02i",hours,minutes,seconds)
&nbsp;&nbsp;&nbsp;&nbsp;setComponentValues(from: str)
&nbsp;&nbsp;}

&nbsp;&nbsp;func setMinutesSeconds(){
&nbsp;&nbsp;&nbsp;&nbsp;setPickerComponents(for: .minuteSecond)
&nbsp;&nbsp;&nbsp;&nbsp;let minutes = Int(value) / 60
&nbsp;&nbsp;&nbsp;&nbsp;let seconds = Int(value) % 60
&nbsp;&nbsp;&nbsp;&nbsp;let str = String(format:"%02i:%02i",minutes,seconds)
&nbsp;&nbsp;&nbsp;&nbsp;setComponentValues(from: str)
&nbsp;&nbsp;}

&nbsp;&nbsp;

To save myself some time I copied and pasted some code from my model to make the time strings.

In RootViewController, Add a value to the action:

@IBAction func pickerButton(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;let vc = storyboard?.instantiateViewController(withIdentifier: "InputController") as! PickerViewController
&nbsp;&nbsp;&nbsp;&nbsp;vc.units = .minutesSecondsPerMile
&nbsp;&nbsp;&nbsp;&nbsp;vc.units = 683.0 // in seconds 600 + 60 + 20 + 3 = 11min 23sec
&nbsp;&nbsp;&nbsp;&nbsp;present(vc, animated: <span class="hljs-literal">true, completion: <span class="hljs-literal">nil)
&nbsp;&nbsp;}

I’ll test this and see 11:23

Getting Values

The purpose of a input picker is the get a value from the user. To do that I’ll update value every time the picker changes. I’ll set up the picker’s delegate function to update the value in a function

&nbsp;func pickerView<span class="hljs-params">(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
&nbsp;&nbsp;&nbsp;&nbsp;updatePickerValue()
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "<span class="hljs-subst">\(value)" //for testing only
&nbsp;&nbsp;}

I added a extra line for testing the value, sending the value to the title. In the app we’ll use a delegate, but that’s for the next phase, so I’ll display it here for now.

The the updatePickerValue function converts the components into a value. I built this into another switch pickerType control structure. This is the opposite of the setting the value. Take the row and convert it to a value. If it is part of a bigger number, add it with the other digits of the number. For example, for the .twoDecimal, I did this:

case .twoDecimal:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;picker.selectedRow(inComponent: 5)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let hundredth = Double(picker.selectedRow(inComponent: 5))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let tenth = Double(picker.selectedRow(inComponent: 4))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let fraction = tenth * 0.1 + hundredth * 0.01
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let ones = Double(picker.selectedRow(inComponent: 2))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let tens = Double(picker.selectedRow(inComponent: 1))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let hundreds = Double(picker.selectedRow(inComponent: 0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let wholeNumber = hundreds * 100 + tens * 10 + ones
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;value = wholeNumber + fraction

I did this explicitly since it is easier to debug than an algorithm for it. I do the same for the time-based picker types, grabbing a digit and adding it all together.

&nbsp;case .minuteSecond: //00:00 returning seconds
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var tens = Double(picker.selectedRow(inComponent: 0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var ones = Double(picker.selectedRow(inComponent: 1))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let minutes = (tens * 10 + ones) * 60
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tens = Double(picker.selectedRow(inComponent: 3))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ones = Double(picker.selectedRow(inComponent: 4))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let seconds = (tens * 10 + ones)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;value = minutes + seconds&nbsp;&nbsp;&nbsp;

Run again and I’ll change the pace to 10:23. As expected, 623.0 is in the title.

 

I’ve built a decent picker that follows Get Good, Get Fast. All data in and out of the picker is a single value of double. What that value represents is a unit of measure. My unit of measure configures my picker for the only legitimate values. I call the picker from a modal view programmatically without a lot of confusion in the storyboard. My next step is the user interface, and hooking this up to it. In the next installment, I’ll build that UI and find I made a few bad assumptions with the picker and find some other interesting dilemmas.

Swift Swift: Formatting a UIPickerView

[Updated to Swift 2.0/iOS9.0 9/15/15]

I’ve posted on UIPickerViews before, but in the comments for that post, I got a question that needed an answer so big, it was worth a new post.

UIPickerViews are those spinning wheel slot machine type controls. For a variety of reasons I went into in the earlier post they are not as common as other  controls in iOS. The reason I’ll focus on here is the lack of properties for a UIPickerView. It relies heavily on delegates. In this lesson we’ll look at some of those delegate methods.

Setting Up a Demo

I debated just having people go to the UIPickerView lesson, and start with that, but decided it would be simpler to start with a single-component picker view instead of the double component in that lesson. I will go through the setup quickly and if you  are not understanding something, I’d suggest clicking here and doing that lesson first.

Make a new project with Command-Shift-N, under iOS make a single view project named SwiftUIPickerFormatted. Drag a label and a picker view onto the view controller on the storyboard.
Screenshot 2014-10-20 12.11.21

Open the Assistant editor, and control-drag from the label to make an outlet named myLabel. Control-drag from the picker view and make an outlet named myPicker. Close the assistant editor and go to the ViewController.swift file. Clean up the code, then add the data for the picker view there so it looks like this:

class ViewController: UIViewController {

    @IBOutlet weak var myPicker: UIPickerView!
    @IBOutlet weak var myLabel: UILabel!
    let pickerData = ["Mozzarella","Gorgonzola","Provolone","Brie","Maytag Blue","Sharp Cheddar","Monterrey Jack","Stilton","Gouda","Goat Cheese", "Asiago"]
    override func viewDidLoad() {
        super.viewDidLoad()
        myPicker.dataSource = self
        myPicker.delegate = self
    }

}

UIPickerView needs a delegate and a data source.  In the viewDidLoad above we set both the delegate and dataSource to to self so we can add the required methods here. Change the class declaration to this:

class ViewController: UIViewController,UIPickerViewDataSource,UIPickerViewDelegate {

You will get the classic

Type 'ViewController' does not conform to protocol 'UIPickerViewDataSource'

error.  Xcode is whining at you to add some required methods for the protocol. The two methods are in the data source. Add these towards the bottom of your code:

//MARK: - Delegates and data sources
    //MARK: Data Sources
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }
    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return pickerData.count
    }

We have only one component for the picker view so we return a literal 1. Using .count we get the number of rows from the data. We have some optional methods to use in UIPickerViewDelegate. Add these below the Data Source methods:

 //MARK: Delegates
    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return pickerData[row]
    }

    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        myLabel.text = pickerData[row]
    }

The first method places the data into the picker and the second selects and displays it in the label. You can now build and run and spin the wheel a bit:
Screenshot 2014-10-21 07.38.20

Formatting Picker Views with Attributed Strings

Now that we have set up a picker view, we might want a different font or color. There is a method which uses attributed strings.  Add this under the code we already have:


    func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
        let titleData = pickerData[row]
        let myTitle = NSAttributedString(string: titleData, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 15.0)!,NSForegroundColorAttributeName:UIColor.blueColor()])
        return myTitle
    }

If you are not familiar with attributed Strings,  click here and check out my earlier post on how to use them. This should change the font to blue 15 point Georgia given the attributed string we have. Build and run.
Screenshot 2014-10-21 07.39.05
We do get blue, but no font change. You can change many attributes of a string. You cannot change the font in  attributed text  for picker views.

Using UIViews to Format the Picker

There is one more very powerful method for displaying something on a UIPickerView. There is a method in the delegate to display a UIView. One subclass of UIView is a UILabel. So we could make a label and have the delegate method return a formatted UILabel. Add this example using attributed strings to the code we have so far:

    func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {
        let pickerLabel = UILabel()
        let titleData = pickerData[row]
        let myTitle = NSAttributedString(string: titleData, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 26.0)!,NSForegroundColorAttributeName:UIColor.blackColor()])
        pickerLabel.attributedText = myTitle
       return pickerLabel
    }

Build and run.

Screenshot 2014-10-21 07.40.24

We get a change of font, but it is left justified. I went back to a black font for our next addition. With a UILabel in this delegate method, you can use all the properties of a UILabel instead of attributed text. I could use text and font property instead of attributed text. I’ll change textAlignment to .Center and I’ll add a colored background to the label by adding this to the delegate method.

         //color  and center the label's background
        let hue = CGFloat(row)/CGFloat(pickerData.count)
        pickerLabel.backgroundColor = UIColor(hue: hue, saturation: 1.0, brightness:1.0, alpha: 1.0)
        pickerLabel.textAlignment = .Center
        return pickerLabel

As I discussed in my color post on hues, this is a simple example to do multiple colors. Line two takes the current element and divides it by the number of elements in our array. Since both are type Int, I cast them to CGFloat before I do the division. This will be a  CGFloat between 0 and 1, which I use as my hue in assigning a color. Build and run:
Screenshot 2014-10-21 07.41.19

You should be a bit more conservative with memory using this. A better way to write this is:

func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {
        var pickerLabel = view as! UILabel!
        if view == nil {  //if no label there yet
            pickerLabel = UILabel()
            //color the label's background
            let hue = CGFloat(row)/CGFloat(pickerData.count)
            pickerLabel.backgroundColor = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)
        }
        let titleData = pickerData[row]
        let myTitle = NSAttributedString(string: titleData, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 26.0)!,NSForegroundColorAttributeName:UIColor.blackColor()])
        pickerLabel!.attributedText = myTitle
        pickerLabel!.textAlignment = .Center
        return pickerLabel

    }

This way we check if the label is already created before creating a new one.

Sizing the UIPickerView

You’ll notice that things seem a little squeezed in using the label. One way of fixing this is two more delegate methods: rowHeightForComponent and widthForComponent. Add this to increase the spacing between cells in the picker view:

 func pickerView(pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        return 36.0
    }

If you build and run, you get this:

Screenshot 2014-10-21 07.42.04

While in a single component, it is not so important, if you have multiple components you can change the width of each spinner as well. Let’s make the component smaller. Add this code:

 func pickerView(pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
        return 200
    }

We are making all the components the same here, since we only have one. We could use the component parameter to identify the component and have different lengths for each one. Build and run.

Screenshot 2014-10-21 07.43.00

That is a few of the things you could do with a UIPickerView. There are a lot more, and since it accepts a UIView anything is possible for a user interface. Apple does recommend in the Human Interface Guide that UITableViews are a better option if you want to get complex. But the UIPickerView does have some very good uses, limited by your creativity.

The Whole Code

//
//  ViewController.swift
//  SwiftUIPickerFormatted
//
//  Created by Steven Lipton on 10/20/14.
//  Copyright (c) 2014 MakeAppPie.Com. All rights reserved.
//  Updated to Swift 2.0  9/15/15 SJL

import UIKit

class ViewController: UIViewController,UIPickerViewDataSource,UIPickerViewDelegate {
    
    @IBOutlet weak var myPicker: UIPickerView!
    @IBOutlet weak var myLabel: UILabel!
    let pickerData = ["Mozzarella","Gorgonzola","Provolone","Brie","Maytag Blue","Sharp Cheddar","Monterrey Jack","Stilton","Gouda","Goat Cheese", "Asiago"]
    override func viewDidLoad() {
        super.viewDidLoad()
        myPicker.delegate = self
        myPicker.dataSource = self
        
    }
    //MARK: - Delegates and data sources
    //MARK: Data Sources
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }
    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return pickerData.count
    }
    //MARK: Delegates
    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return pickerData[row]
    }
    
    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        myLabel.text = pickerData[row]
    }
    
    func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
        let titleData = pickerData[row]
        let myTitle = NSAttributedString(string: titleData, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 26.0)!,NSForegroundColorAttributeName:UIColor.blueColor()])
        return myTitle
    }
 /* less conservative memory version
    func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {
        let pickerLabel = UILabel()
        let titleData = pickerData[row]
        let myTitle = NSAttributedString(string: titleData, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 26.0)!,NSForegroundColorAttributeName:UIColor.blackColor()])
        pickerLabel.attributedText = myTitle
        //color  and center the label's background
        let hue = CGFloat(row)/CGFloat(pickerData.count)
        pickerLabel.backgroundColor = UIColor(hue: hue, saturation: 1.0, brightness:1.0, alpha: 1.0)
        pickerLabel.textAlignment = .Center
        return pickerLabel
    }
  */
    
/* better memory management version */
    func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {
        var pickerLabel = view as! UILabel!
        if view == nil {  //if no label there yet
            pickerLabel = UILabel()
            //color the label's background
            let hue = CGFloat(row)/CGFloat(pickerData.count)
            pickerLabel.backgroundColor = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)
        }
        let titleData = pickerData[row]
        let myTitle = NSAttributedString(string: titleData, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 26.0)!,NSForegroundColorAttributeName:UIColor.blackColor()])
        pickerLabel!.attributedText = myTitle
        pickerLabel!.textAlignment = .Center
        
        return pickerLabel
        
    }
    
    func pickerView(pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        return 36.0
    }
    // for best use with multitasking , dont use a constant here.
    //this is for demonstration purposes only.
    func pickerView(pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
        return 200
    }
    
    
    
}

Swift Swift: Implementing Picker Views

[Updated to Swift3.0 10/4/16 SJL]

Screenshot 2014-09-17 06.45.53
Example of A UiPickerView In action

This week, we’ll look at UIPickerView. Picker views are the spin-wheel like controls that often remind me of slot machines. Picker views, like table and collection views, are delegate based. You need to adopt a delegate and data source to get them to work properly. Picker views also take a lot of screen real estate that is not under developer control, which means your view can get crushed quickly. These two probably account for its lack of popularity. Most of what can be done in a picker view can be done far better in a table view. For those three reasons the fourth reason is also true: Nothing yells “newbie” like over-using picker views.

Professional developers do not use them as often as table views, but they work well for static multi-selection situations. When you have a few items to select from that will never change a picker view works well. We’ll set up a pizza order with a picker view today.

Setting Up the Basic Layout

Start with a new Swift single-view project in Xcode named SwiftPickerViewPizzaDemo. Go into the storyboard and make the following layout with two labels and a picker view:

Screenshot 2014-09-17 06.47.04

You can also download the starter file swiftpickerviewpizzademo_star.t.zip which has the layout done for you.

We’ll need outlets for both the picker and the label. Open the assistant editor and control drag the lower label and the picker to the code. Name them myLabel and myPicker. I’m also adding //MARK comments to my code to keep things organized. //MARK is the objective-C equivalent of #pragma mark. After cleaning up your code, it should look like this:

class ViewController: UIViewController {
//MARK -Outlets and Properties
@IBOutlet weak var myLabel: UILabel!
@IBOutlet weak var myPicker: UIPickerView!

//MARK - Instance Methods

//MARK - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()

}
//MARK - Delgates and Data Source
}

Add Our Picker Data

The UIPickerView uses an array of arrays to store the titles for the wheels. Each wheel is called a component. Each component gets it own array. I want to use both size and topping information so add the following just above the Outlets mark.

let pickerData = [
    ["10\"","14\"","18\"","24\""],
    ["Cheese","Pepperoni","Sausage","Veggie","BBQ Chicken"]
]

Adopt the Delegates and Data Source

Now we adopt the protocols UIPickerViewDataSource and UIPickerViewDelegate by listing them in the class declaration

class ViewController: UIViewController,UIPickerViewDataSource,UIPickerViewDelegate {

Xcode will give us an error about required methods we need to implement. Ignore it for now. Go down to viewDidLoad and make sure we assign ourselves to the delegate:

//MARK - Life Cycle
override func viewDidLoad() {
    super.viewDidLoad()
    myPicker.delegate = self
    myPicker.dataSource = self
}

Implement the Picker Data Source Methods

Xcode is still complaining

Type 'ViewController' does not conform to protocol 'UIPickerViewDataSource'

Let’s get rid of this.  Add the data source required methods. These methods tell the picker the size of the array we set up in pickerData. There are two methods. Add this below the Delegate and Data Source mark.

func numberOfComponents(in pickerView: UIPickerView) -> Int {
return pickerData.count
}

Line 2 specifies the number of components in the pickerData array in the method. The second method looks at each component and gives the number of rows in the component:

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

The method give us the specified component in component. We access that element of the array, which is another array. We use the count property on the second array, and return that value.

Implement the Required Delegate

Anyone who has ever used a UITableView may find this vaguely familiar. In UITableView we use sections and rows instead of components and rows of UIPickerView, but it is the same two required methods. There is a presenting method similar to a table view’s cellForRowAtIndexPath in a picker’s delegate:

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

Instead of a cell like its table view equivalent, the method returns the title string that goes in that row and component of the spinner.

Selecting a Row

We set up the required methods for the protocol. We have one more step. We need to get the selection. Add this to the delegate section of your code:

func pickerView(
    pickerView: UIPickerView,
    didSelectRow row: Int,
    inComponent component: Int)
{
    updateLabel()
}

In our case all we want to do is to print out the results of the selection. We’ll need that twice once here and once in initialization. A method might be a good idea then. Add a method to update the label.

//MARK - Instance Methods
func updateLabel(){
let size = pickerData[0][myPicker.selectedRowInComponent(0)]
let topping = pickerData[1][myPicker.selectedRowInComponent(1)]
pizzaLabel.text = "Pizza: " + size + " " + topping
}

Line 2 and 3 use the selectedRowInComponent method to access the selected array row.

Build and run. We start with this:

Screenshot 2014-09-18 05.40.01

Dial an 18″ sausage pizza and you get this:

Screenshot 2014-09-18 05.39.49

Selection and Cleaning up

While I wouldn’t call it a bug, there are three issues with his code I don’t like. It’s bad coding and documentation to use literal numbers for component. What is better is to use constants. Add this under the pickerData declaration:

let sizeComponent = 0
let toppingComponent = 1

In this enum, we make the values literal integers that represent the components. This way if we change around our components while using them in multiple methods, we can control things from the one enum. In Swift, the enumerations will need a little help though to get the value out. For a enum to give its raw value use the rawValue property. Change updateLabel to this:

 func updateLabel(){
      
        let size = pickerData[sizeComponent][myPicker.selectedRow(inComponent:sizeComponent)]
        let topping = pickerData[toppingComponent][myPicker.selectedRow(inComponent:toppingComponent)]
        myLabel.text = "Pizza: " + size + " " + topping
    }

While longer, this is much more readable and flexible code.

When we start, the picker  does not reflect in the label. I’d like that to happen. I also want to start with a 18″ pizza, since a large pizza is the most ordered size. In viewDidLoad, add the following code:

myPicker.selectRow(2,
    inComponent: sizeComponent,
    animated: false)
updateLabel()

Line 2 uses another UIPickerView method selectRow to set the size component to 18″. I added the updateLabel method after this. What shows on the picker at initialization shows up in the label.
Build and run, and we get the behavior we want.

Making the UI Pretty With Translucency.

That is all the code for making a UIPickerView, however the storyboard still doesn’t look like much. I’m going to do this with Auto Layout off, but you can certainly leave it on. If you want it off, go to the file inspector and turn it off. For the background I used this graphic:

Photo Sep 14, 7 40 59 PM_small
Click the thumbnail to get the full size image.

Click the image above and when you have the full size one, right-click or save the image. Drag the saved file into your project file list. Make sure copy item is checked in the dialog box. Once you add the image to your project, in the lower right of the utilities pane you will find a little film clip icon, which is the media library. Click the film clip icon, and then drag the pizza image onto the storyboard view. It will cover everything else.  Look at the document outline. If it is not visible click the small icon on the lower left of the storyboard.  You should see this:

The bottom of the outline is the top of the view stack.
The bottom of the outline is the top of the view stack.

Drag the photo to the top of the outline in the view like this, so it is at the bottom of the view stack:

Screenshot 2014-09-18 06.44.44

Now we can see the controls — sort of. We need a background behind the labels, and need to see through the picker.

Screenshot 2014-09-18 06.44.53

Change the background of the picker controller to a white with a 50% alpha. I use the crayon mode and use Snow at 50%

Screenshot 2014-09-18 06.45.27

We’ll make a background layer for the two labels. Drag out  a view (not a view controller) onto the bottom of the photo and size it so it fits across the view. Set the background  on the blank view once again to Snow at 50% alpha.

Screenshot 2014-09-18 06.47.37

Drag the Labels into the view on the bottom and place the labels in the right spots. When you drop a view into another view it becomes a subview of that view.

Screenshot 2014-09-18 06.49.50

Drag the view, not the labels, up above the picker.  Everything moves up to the new place.

Screenshot 2014-09-18 06.50.34

Build and Run:

Screenshot 2014-09-18 06.52.13

You have a better looking UI.

Picker Views: Fastest Way to a One Star Review

Picker views have one purpose: When you have a few items which never change to pick from. For the demo above with a few sizes and pizza toppings, they work fine. For more than 15 items per component, use a table view. For more than three components, these get ugly since there is not enough space. Picker views above these limits are bad user experiences. They take too long to select items quickly for users. They are not good at dynamic selection, and although there is some formatting you can do, they have severe limitations in formatting. My suggestion is for most places above the limits I just mentioned, use a table view. Nothing says you don’t know what you are doing quite like over using picker views. The few who buy your app will give you a one-star review, Everyone else will stay away.

The Whole Code:

You can find the finished project, where I did use auto layout, here:  swiftpickerviewpizzademo below you can find the code.

//
//  ViewController.swift
//  SwiftPickerViewPizzaDemo
//
//  Created by Steven Lipton on 10/4/16.
//  An update of the earlier  9/18/14 article to Swift 3.0
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit

class ViewController: UIViewController,UIPickerViewDataSource,UIPickerViewDelegate {

    @IBOutlet weak var myPicker: UIPickerView!
    @IBOutlet weak var pizzaLabel: UILabel!
    let pickerData = [
        ["10\"","14\"","18\"","24\""],
        ["Cheese","Pepperoni","Sausage","Veggie","BBQ Chicken"]
    ]
    
    let sizeComponent = 0
    let toppingComponent = 1
   
    
    
    //MARK: - Picker View Data Sources and Delegates
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return pickerData.count
    }
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
         return pickerData[component].count
    }
    
    func pickerView(_
        pickerView: UIPickerView,
                    titleForRow row: Int,
                    forComponent component: Int
        ) -> String? {
        return pickerData[component][row]
    }
    
    func pickerView(_
        pickerView: UIPickerView,
        didSelectRow row: Int,
        inComponent component: Int)
    {
        updateLabel()
    }
    
    //MARK: - Instance Methods
    func updateLabel(){
        let size = pickerData[sizeComponent][myPicker.selectedRow(inComponent: sizeComponent)]
        let topping = pickerData[toppingComponent][myPicker.selectedRow(inComponent: toppingComponent)]
        pizzaLabel.text = "Pizza: " + size + " " + topping
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        myPicker.delegate = self
        myPicker.dataSource = self
        myPicker.selectRow(2, inComponent:sizeComponent, animated: false)
        updateLabel()
    }
    
    
}