A UIPickerView with labels

I recently needed a picker view with labels (like the one in the timer tab in the Clock app) to select minutes and seconds for a time interval. So I made the following subclass of UIPickerView:

#import

/**
A picker view with labels under the selection indicator.
Similar to the one in the timer tab in the Clock app.
NB: has only been tested with less than four wheels.
*/
@interface LabeledPickerView : UIPickerView {
NSMutableDictionary *labels;
}

/** Adds the label for the given component. */
- (void) addLabel:(NSString *)labeltext forComponent:(NSUInteger)component;

@end

… and the implementation:

/*******************************************************************************
* Copyright (c) 2009 Kåre Morstøl (NotTooBad Software).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*    Kåre Morstøl (NotTooBad Software) - initial API and implementation
*******************************************************************************/

// http://stackoverflow.com/questions/367471/fixed-labels-in-the-selection-bar-of-a-uipickerview/616517

#import "LabeledPickerView.h"

@implementation LabeledPickerView

/** loading programmatically */
- (id)initWithFrame:(CGRect)aRect {
if (self = [super initWithFrame:aRect]) {
labels = [[NSMutableDictionary alloc] initWithCapacity:3];
}
return self;
}

/** loading from nib */
- (id)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
labels = [[NSMutableDictionary alloc] initWithCapacity:3];
}
return self;
}

- (void) dealloc
{
[labels release];
[super dealloc];
}

#pragma mark Labels

- (void) addLabel:(NSString *)labeltext forComponent:(NSUInteger)component {
[labels setObject:labeltext forKey:[NSNumber numberWithInt:component]];
}

/**
Adds the labels to the view, below the selection indicator glass-thingy.
The labels are aligned to the right side of the wheel.
The delegate is responsible for providing enough width for both the value and the label.
*/
- (void)didMoveToWindow {
// exit if view is removed from the window or there are no labels.
if (!self.window || [labels count] == 0)
return;

UIFont *labelfont = [UIFont boldSystemFontOfSize:20];

// find the width of all the wheels combined
CGFloat widthofwheels = 0;
for (int i=0; i
widthofwheels += [self rowSizeForComponent:i].width;
}

// find the left side of the first wheel.
// seems like a misnomer, but that will soon be corrected.
CGFloat rightsideofwheel = (self.frame.size.width - widthofwheels) / 2;

// cycle through all wheels
for (int component=0; component
// find the right side of the wheel
rightsideofwheel += [self rowSizeForComponent:component].width;

// get the text for the label.
// move on to the next if there is no label for this wheel.
NSString *text = [labels objectForKey:[NSNumber numberWithInt:component]];
if (text) {

// set up the frame for the label
CGRect frame;
frame.size = [text sizeWithFont:labelfont];
// center it vertically
frame.origin.y = (self.frame.size.height / 2) - (frame.size.height / 2) - 0.5;
// align it to the right side of the wheel, with a margin.
// use a smaller margin for the rightmost wheel.
frame.origin.x = rightsideofwheel - frame.size.width -
(component == self.numberOfComponents - 1 ? 5 : 7);

// set up the label
UILabel *label = [[[UILabel alloc] initWithFrame:frame] autorelease];
label.text = text;
label.font = labelfont;
label.backgroundColor = [UIColor clearColor];
label.shadowColor = [UIColor whiteColor];
label.shadowOffset = CGSizeMake(0,1);

/*
and now for the tricky bit: adding the label to the view.
kind of a hack to be honest, might stop working if Apple decides to
change the inner workings of the UIPickerView.
*/
if (self.showsSelectionIndicator) {
// if this is the last wheel, add label as the third view from the top
if (component==self.numberOfComponents-1)
[self insertSubview:label atIndex:[self.subviews count]-3];
// otherwise add label as the 5th, 10th, 15th etc view from the top
else
[self insertSubview:label aboveSubview:[self.subviews objectAtIndex:5*(component+1)]];
} else
// there is no selection indicator, so just add it to the top
[self addSubview:label];
}
}
}

@end

A big thanks to dizy from stackoverflow.com for showing how to add the labels below the selection indicator.

If anyone knows of a better place to put the label-adding code than didMoveToWindow then please let me know. It seems out of place where it is now.

This class is part of the mySettings project and the latest version can always be found here: LabeledPickerView.h, LabeledPickerView.m.

  • Very nice and very helpful. Thank you.

    However I have a need to update the labels on the fly. So for example changingthe picker from 2 to 1 will change my label from “minutes” to “minute” etc.

    I’m not 100% sure how I can do this. I’ve tried adding a tag to the label setup just below label.shadowoffset:

    label.tag = component;

    And then I’ve added an update routine to the class as follows:

    – (void) upDateLabel:(NSString *)labeltext forComponent:(NSUInteger)component {
    UILabel *theLabel = [self viewWithTag:component];
    theLabel.text = labeltext;
    }

    This returns a warning for starters (initialisation from distinct objective-c type) but when I run it, it does appear to update the value for the view (in the debugger). However the label in the pickerView never changes.

    Any ideas or pointers on how to do this correctly?

    Thanks, Michael.

  • I think you need to call setNeedsDisplay on the label. And be aware that the label is set to the smallest size possible so you might need to change the size too.

    Please let me know if you get this to work because I could really use this functionality in the mySettings project.

  • Hi Kare,

    Turns out it was a bug in another bit of code….so everything is working now (you don’t even need setNeedsDisplay).

    I still have the warning though….which I’m trying to sort now. When I do I’ll come back to you.

    Thanks, Michael.

  • Ok done…I needed to cast the viewWithTag to a UILabel of course,

    I’m sure there is a better way to do this but for the moment using the component as the tag is the easiest way because nothing else in your code needs to be modified. Also it makes it very easy to find the label view regardless of where it is added.

    So please go ahead and add this in.

    Thanks again.

    //add the following line

    – (void)didMoveToWindow {

    label.tag = component;

    }

    // Update method
    – (void) upDateLabel:(NSString *)labeltext forComponent:(NSUInteger)component {
    UILabel *theLabel = [self viewWithTag:component];
    theLabel.text = labeltext;
    //[theLabel setNeedsDisplay];
    }

    Obviously the best place to call the update method is in the pickerView didSelectRow method for the component being updated. i.e. [thePicker upDateLabel:NSLocalizedString(@”minute”, @””) forComponent:1];

    Thanks again for doing all the hard work.

    Regards, Michael

  • In fact Apple animates the label change so you could do this to replicate what apple does (not sure about the timings though!:

    – (void) upDateLabel:(NSString *)labeltext forComponent:(NSUInteger)component {
    UILabel *theLabel = (UILabel*)[self viewWithTag:component];

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.75];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    theLabel.alpha = 0.00;
    theLabel.text = labeltext;
    theLabel.alpha = 1.00;
    [UIView commitAnimations];
    }

  • sorry! In fact you only want to update the label if the label value has changed:

    – (void) upDateLabel:(NSString *)labeltext forComponent:(NSUInteger)component {
    UILabel *theLabel = (UILabel*)[self viewWithTag:component];

    // Update label if it doesn’t match current label
    if (theLabel.text != labeltext) {
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.75];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    theLabel.alpha = 0.00;
    theLabel.text = labeltext;
    theLabel.alpha = 1.00;
    [UIView commitAnimations];
    }

    }

    • I finally got around to updating mySettings with Michael’s changes. Now just add something similar to this to your delegate and the labels will update automatically when a new row is selected:
      [code lang=”objc”]
      // called when the wheel stops
      – (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
      if (component == 0) {
      [(LabeledPickerView *)pickerView updateLabel:(row==1 ? @"min" : @"mins") forComponent:0];
      } else if (component == 1) {
      [(LabeledPickerView *)pickerView updateLabel:(row==1 ? @"sec" : @"secs") forComponent:1];
      }
      }
      [/code]
      The latest version can be found here: LabeledPickerView.h, LabeledPickerView.m.

  • Cleaned up the code a bit, should have been done long time ago obviously. And removed some completely unnecessary reloading of views.

  • Michael

    Thanks for this useful little class! It’s strange that the iPhone HIG recommends doing exactly this, yet doesn’t suggest how to do it, nor do the APIs seem to support it easily!

    By the way, I noticed that this class always tries to send a pickerView:didSelectRow:inComponent: message to its delegate, which crashes the app if the delegate doesn’t implement that method. The regular UIPickerView doesn’t do this, and indeed that method is listed as “optional” in the UIPickerViewDelegate protocol reference.

    Otherwise, very nice work. Thanks again!

    • You’re right. I fixed it so that method is no longer required. Well spotted.

  • Jeff

    This is great stuff!

    I did notice, however, that its calling ‘didSelectRow’ upon creation (or at least when added to view). I haven’t narrowed it down exactly but has anyone else seen this?

  • JeffW

    I’m working on a iOS 4.0 app, when I try to use a LabledPickerView I get an EXE_BAD_ACCESS at this line:

    if ([self.delegate respondsToSelector:@selector(pickerView:didSelectRow:inComponent:)])
    [self.delegate pickerView:self didSelectRow:[self selectedRowInComponent:component] inComponent:component];

    Has this code been broken by an Apple update, or am I doing something wrong?

    Thanks!

    • Kareman

      Nice! I’ve never even seen C# iOS code before. Cool.