[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.
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:
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.
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.
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:
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:
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.
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 } }
Leave a Reply