In the clocks application, the timer screen shows a picker (probably a UIPicker
in UIDatePickerModeCountDownTimer
mode) with some text in the selection bar ("hours" and "mins" in this case).
(edit) Note that these labels are fixed: They don't move when the picker wheel is rolling.
Is there a way to show such fixed labels in the selection bar of a standard UIPickerView
component?
I did not find any API that would help with that. A suggestion was to add a UILabel
as a subview of the picker, but that didn't work.
Answer
I followed Ed Marty's advice (answer below), and it works! Not perfect but it should fool people. For reference, here's my implementation, feel free to make it better...
- (void)viewDidLoad {
// Add pickerView
self.pickerView = [[UIPickerView alloc] initWithFrame:CGRectZero];
[pickerView release];
CGSize pickerSize = [pickerView sizeThatFits:CGSizeZero];
CGRect screenRect = [[UIScreen mainScreen] applicationFrame];
#define toolbarHeight 40.0
CGFloat pickerTop = screenRect.size.height - toolbarHeight - pickerSize.height;
CGRect pickerRect = CGRectMake(0.0, pickerTop, pickerSize.width, pickerSize.height);
pickerView.frame = pickerRect;
// Add label on top of pickerView
CGFloat top = pickerTop + 2;
CGFloat height = pickerSize.height - 2;
[self addPickerLabel:@"x" rightX:123.0 top:top height:height];
[self addPickerLabel:@"y" rightX:183.0 top:top height:height];
//...
}
- (void)addPickerLabel:(NSString *)labelString rightX:(CGFloat)rightX top:(CGFloat)top height:(CGFloat)height {
#define PICKER_LABEL_FONT_SIZE 18
#define PICKER_LABEL_ALPHA 0.7
UIFont *font = [UIFont boldSystemFontOfSize:PICKER_LABEL_FONT_SIZE];
CGFloat x = rightX - [labelString sizeWithFont:font].width;
// White label 1 pixel below, to simulate embossing.
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(x, top + 1, rightX, height)];
label.text = labelString;
label.font = font;
label.textColor = [UIColor whiteColor];
label.backgroundColor = [UIColor clearColor];
label.opaque = NO;
label.alpha = PICKER_LABEL_ALPHA;
[self.view addSubview:label];
[label release];
// Actual label.
label = [[UILabel alloc] initWithFrame:CGRectMake(x, top, rightX, height)];
label.text = labelString;
label.font = font;
label.backgroundColor = [UIColor clearColor];
label.opaque = NO;
label.alpha = PICKER_LABEL_ALPHA;
[self.view addSubview:label];
[label release];
}
Create your picker, create a label with a shadow, and push it to a picker's subview below the selectionIndicator view.
It would look something like this
UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(135, 93, 80, 30)] autorelease];
label.text = @"Label";
label.font = [UIFont boldSystemFontOfSize:20];
label.backgroundColor = [UIColor clearColor];
label.shadowColor = [UIColor whiteColor];
label.shadowOffset = CGSizeMake (0,1);
[picker insertSubview:label aboveSubview:[picker.subviews objectAtIndex:5]];
//When you have multiple components (sections)...
//you will need to find which subview you need to actually get under
//so experiment with that 'objectAtIndex:5'
//
//you can do something like the following to find the view to get on top of
// define @class UIPickerTable;
// NSMutableArray *tables = [[NSMutableArray alloc] init];
// for (id i in picker.subviews) if([i isKindOfClass:[UIPickerTable class]]) [tables addObject:i];
// etc...
-- Pay it forward
I've turned dizy's answer into a category on UIPickerView
a couple years ago. Just verified that it still works with iOS SDK 4.3, and posting it here. It allows you to add a label (XX hours) and animate changes to this label (e.g. 1 hour -> 3 hours) just like UIDatePicker
does.
// UIPickerView_SelectionBarLabelSupport.h
//
// This file adds a new API to UIPickerView that allows to easily recreate
// the look and feel of UIDatePicker labeled components.
//
// Copyright (c) 2009, Andrey Tarantsov <[email protected]>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#import <Foundation/Foundation.h>
// useful constants for your font size-related code
#define kPickerViewDefaultTitleFontSize 20.0f
#define kDatePickerTitleFontSize 25.0f
#define kDatePickerLabelFontSize 21.0f
@interface UIPickerView (SelectionBarLabelSupport)
// The primary API to add a label to the given component.
// If you want to match the look of UIDatePicker, use 21pt as pointSize and 25pt as the font size of your content views (titlePointSize).
// (Note that UIPickerView defaults to 20pt items, so you need to use custom views. See a helper method below.)
// Repeated calls will change the label with an animation effect similar to UIDatePicker's one.
//
// To call this method on viewDidLoad, please call [pickerView layoutSubviews] first so that all subviews
// get created.
- (void)addLabel:(NSString *)label ofSize:(CGFloat)pointSize toComponent:(NSInteger)component leftAlignedAt:(CGFloat)offset baselineAlignedWithFontOfSize:(CGFloat)titlePointSize;
// A helper method for your delegate's "pickerView:viewForRow:forComponent:reusingView:".
// Creates a propertly positioned right-aligned label of the given size, and also handles reuse.
// The actual UILabel is a child of the returned view, use [returnedView viewWithTag:1] to retrieve the label.
- (UIView *)viewForShadedLabelWithText:(NSString *)label ofSize:(CGFloat)pointSize forComponent:(NSInteger)component rightAlignedAt:(CGFloat)offset reusingView:(UIView *)view;
// Creates a shaded label of the given size, looking similar to the labels used by UIPickerView/UIDatePicker.
- (UILabel *)shadedLabelWithText:(NSString *)label ofSize:(CGFloat)pointSize;
@end
And the implementation:
// UIPickerView_SelectionBarLabelSupport.m
//
// This file adds a new API to UIPickerView that allows to easily recreate
// the look and feel of UIDatePicker labeled components.
//
// Copyright (c) 2009, Andrey Tarantsov <[email protected]>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#import "UIPickerView_SelectionBarLabelSupport.h"
// used to find existing component labels among UIPicker's children
#define kMagicTag 89464534
// a private UIKit implementation detail, but we do degrade gracefully in case it stops working
#define kSelectionBarClassName @"_UIPickerViewSelectionBar"
// used to sort per-component selection bars in a left-to-right order
static NSInteger compareViews(UIView *a, UIView *b, void *context) {
CGFloat ax = a.frame.origin.x, bx = b.frame.origin.x;
if (ax < bx)
return -1;
else if (ax > bx)
return 1;
else
return 0;
}
@implementation UIPickerView (SelectionBarLabelSupport)
- (UILabel *)shadedLabelWithText:(NSString *)label ofSize:(CGFloat)pointSize {
UIFont *font = [UIFont boldSystemFontOfSize:pointSize];
CGSize size = [label sizeWithFont:font];
UILabel *labelView = [[[UILabel alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)] autorelease];
labelView.font = font;
labelView.adjustsFontSizeToFitWidth = NO;
labelView.shadowOffset = CGSizeMake(1, 1);
labelView.textColor = [UIColor blackColor];
labelView.shadowColor = [UIColor whiteColor];
labelView.opaque = NO;
labelView.backgroundColor = [UIColor clearColor];
labelView.text = label;
labelView.userInteractionEnabled = NO;
return labelView;
}
- (UIView *)viewForShadedLabelWithText:(NSString *)title ofSize:(CGFloat)pointSize forComponent:(NSInteger)component rightAlignedAt:(CGFloat)offset reusingView:(UIView *)view {
UILabel *label;
UIView *wrapper;
if (view != nil) {
wrapper = view;
label = (UILabel *)[wrapper viewWithTag:1];
} else {
CGFloat width = [self.delegate pickerView:self widthForComponent:component];
label = [self shadedLabelWithText:title ofSize:pointSize];
CGSize size = label.frame.size;
label.frame = CGRectMake(0, 0, offset, size.height);
label.tag = 1;
label.textAlignment = UITextAlignmentRight;
label.autoresizingMask = UIViewAutoresizingFlexibleHeight;
wrapper = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, width, size.height)] autorelease];
wrapper.autoresizesSubviews = NO;
wrapper.userInteractionEnabled = NO;
[wrapper addSubview:label];
}
label.text = title;
return wrapper;
}
- (void)addLabel:(NSString *)label ofSize:(CGFloat)pointSize toComponent:(NSInteger)component leftAlignedAt:(CGFloat)offset baselineAlignedWithFontOfSize:(CGFloat)titlePointSize {
NSParameterAssert(component < [self numberOfComponents]);
NSInteger tag = kMagicTag + component;
UILabel *oldLabel = (UILabel *) [self viewWithTag:tag];
if (oldLabel != nil && [oldLabel.text isEqualToString:label])
return;
NSInteger n = [self numberOfComponents];
CGFloat total = 0.0;
for (int c = 0; c < component; c++)
offset += [self.delegate pickerView:self widthForComponent:c];
for (int c = 0; c < n; c++)
total += [self.delegate pickerView:self widthForComponent:c];
offset += (self.bounds.size.width - total) / 2;
offset += 2 * component; // internal UIPicker metrics, measured on a screenshot
offset += 4; // add a gap
CGFloat baselineHeight = [@"X" sizeWithFont:[UIFont boldSystemFontOfSize:titlePointSize]].height;
CGFloat labelHeight = [@"X" sizeWithFont:[UIFont boldSystemFontOfSize:pointSize]].height;
UILabel *labelView = [self shadedLabelWithText:label ofSize:pointSize];
labelView.frame = CGRectMake(offset,
(self.bounds.size.height - baselineHeight) / 2 + (baselineHeight - labelHeight) - 1,
labelView.frame.size.width,
labelView.frame.size.height);
labelView.tag = tag;
UIView *selectionBarView = nil;
NSMutableArray *selectionBars = [NSMutableArray array];
for (UIView *subview in self.subviews) {
if ([[[subview class] description] isEqualToString:kSelectionBarClassName])
[selectionBars addObject:subview];
}
if ([selectionBars count] == n) {
[selectionBars sortUsingFunction:compareViews context:NULL];
selectionBarView = [selectionBars objectAtIndex:component];
}
if (oldLabel != nil) {
[UIView beginAnimations:nil context:oldLabel];
[UIView setAnimationDuration:0.25];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(YS_barLabelHideAnimationDidStop:finished:context:)];
oldLabel.alpha = 0.0f;
[UIView commitAnimations];
}
// if the selection bar hack stops working, degrade to using 60% alpha
CGFloat normalAlpha = (selectionBarView == nil ? 0.6f : 1.0f);
if (selectionBarView != nil)
[self insertSubview:labelView aboveSubview:selectionBarView];
else
[self addSubview:labelView];
if (oldLabel != nil) {
labelView.alpha = 0.0f;
[UIView beginAnimations:nil context:oldLabel];
[UIView setAnimationDuration:0.25];
[UIView setAnimationDelay:0.25];
labelView.alpha = normalAlpha;
[UIView commitAnimations];
} else {
labelView.alpha = normalAlpha;
}
}
- (void)YS_barLabelHideAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(UIView *)oldLabel {
[oldLabel removeFromSuperview];
}
@end
Usage example (in a view controller):
- (void)updateFloorLabel {
NSInteger floor = [self.pickerView numberOfRowsInComponent:0] - [self.pickerView selectedRowInComponent:0];
NSString *suffix = @"th";
if (((floor % 100) / 10) != 1) {
switch (floor % 10) {
case 1: suffix = @"st"; break;
case 2: suffix = @"nd"; break;
case 3: suffix = @"rd"; break;
}
}
[self.pickerView addLabel:[NSString stringWithFormat:@"%@ Floor", suffix]
ofSize:21
toComponent:0
leftAlignedAt:50
baselineAlignedWithFontOfSize:25];
}
- (void)viewDidLoad {
...
[self.pickerView layoutSubviews];
[self updateFloorLabel];
...
}
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
NSString *s = [NSString stringWithFormat:@"%d", [pickerView numberOfRowsInComponent:0] - row];
return [pickerView viewForShadedLabelWithText:s ofSize:25 forComponent:0 rightAlignedAt:46 reusingView:view];
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
[self updateFloorLabel];
}
Enjoy!
Let's say we want to implement a picker view for selecting distance, there are 2 columns, one for distance, one for unit, which is km. Then we want the second column to be fixed. We can make it through some delegate methods.
- (NSString*)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
if (component == 0) {
return self.distanceItems[row];
}
else {
return @"km";
}
}
-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{
return 2;
}
-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component{
if (component == 0) {
return [self.distanceItems count];
}
else {
// when it comes to the second column, only one row.
return 1;
}
}
Now we have this:
I guess this is the simplest way.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With