UIPickerView (resizing)

One of the prettiest built-in views on the iPhone is the UIPickerView, which presents a slot-machine-like list of options. Unfortunately, this view isn’t particularly easy to use, as its default presentation takes up quite a bit of screen real-estate, and it’s not obvious how to resize it. Today, I’d like to talk about how to tame this view.

Things That Just Ain’t So

When I was Googling for tips on how to control the size of UIPickerViews, I repeatedly came across the following pieces of advice:

  • Pass in a small CGRect to initWithFrame
  • Make the UIPickerView a subview of another UIView, and then resize the outer view

Neither technique worked for me. (OS 2.2.1)

Affine Transformations

Semi-buried in the UIView class is the astonishingly useful transform property, which performs arbitrary affine transformations on the view. This lets you do a lot of neat tricks, including scaling the UIPickerView.

Example

Here are the header and implementation files for a PickerDemo class, which is a view controller that manages a view containing a UIPickerView. (I nest the UIPickerView inside another UIView s.t. the PickerDemo controller can be pushed onto a Navigation Controller, with which I was playing when I pulled together this demo.)

#import 

@interface PickerDemo : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource> {
	UIPickerView* picker;
}

@end
#import "PickerDemo.h"

@implementation PickerDemo

- (void)loadView
{
	picker = [[UIPickerView alloc] initWithFrame:CGRectZero];
	picker.delegate = self;
	picker.dataSource = self;
	picker.showsSelectionIndicator = YES;

	self.view = [[UIView alloc] initWithFrame:CGRectZero];
	[self.view addSubview:picker];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}

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

#pragma mark UIPickerViewDelegate methods

- (NSString*)pickerView:(UIPickerView*)pv titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
	return [NSString stringWithFormat:@"%d",row];
}

#pragma mark UIPickerViewDataSource methods

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView*)pv
{
	return 3;
}

- (NSInteger)pickerView:(UIPickerView*)pv numberOfRowsInComponent:(NSInteger)component
{
	return 10;
}

@end

Big reelsThis yields a simple, 3-reel UIPickerView, where each reel contains choices from 0 to 9. No (meaningful) CGRect is given for either the UIPickerView or its enclosing UIView; the former always assumes its default size, while the latter receives its geometry from the (not shown here) Navigation Controller. The result may be seen to the right.

Scaling

Lets try adding a simple scaling CGAffineTransform, by recoding the loadView function as follows:

- (void)loadView
{
	picker = [[UIPickerView alloc] initWithFrame:CGRectZero];
	picker.delegate = self;
	picker.dataSource = self;
	picker.showsSelectionIndicator = YES;
	picker.transform = CGAffineTransformMakeScale(0.5, 0.5);

	self.view = [[UIView alloc] initWithFrame:CGRectZero];
	[self.view addSubview:picker];
}

Big reelsThis nicely scales down the UIPickerView. Unfortunately, the transform is applied to the center of the view, not the upper-left corner, which probably isn’t what one wants. (At least, it’s not what I want.)

Composites

To do a scale relative to the UL corner, we’ll need to execute three transforms:

  • Move the UL corner to the origin
  • Scale relative to the origin
  • Move the origin to the UL corner

We can call these three transformations T0, S0, and T1, and since Cocoa/Core Graphics uses row vectors, we will compose them into a single transformation C with this formula:

  • C = T0*S0*T1

A re-written loadView incorporating this transform would look like this:

- (void)loadView
{
	picker = [[UIPickerView alloc] initWithFrame:CGRectZero];
	picker.delegate = self;
	picker.dataSource = self;
	picker.showsSelectionIndicator = YES;

	CGAffineTransform t0 = CGAffineTransformMakeTranslation(picker.bounds.size.width/2, picker.bounds.size.height/2);
	CGAffineTransform s0 = CGAffineTransformMakeScale(0.5, 0.5);
	CGAffineTransform t1 = CGAffineTransformMakeTranslation(-picker.bounds.size.width/2, -picker.bounds.size.height/2);
	picker.transform = CGAffineTransformConcat(t0, CGAffineTransformConcat(s0, t1));

	self.view = [[UIView alloc] initWithFrame:CGRectZero];
	[self.view addSubview:picker];
}

Big reelsThis yields the desired result.

Digression

To be honest, I’m not really happy with the technique I used above to compute the translation offsets; the documentation states that:

The origin of the transform is the value of the center property, or the layer’s anchorPoint property if it was changed. (Use the layer property to get the underlying Core Animation layer object.)

The center property is given in the co-ordinate system of the view’s superview; I’m not familiar with the details of the anchorPoint property. In the code above, I sort of hand-wave over all of this, and just assume that the origin of the transform will end up in the middle of the bounds rectangle, which is given in the view’s coordinate system. That seems to be true in this case, and might be true in the general case, but it is a hand-wave, and I thought it only fair to point it out.

Reading over that, I’m not sure it clarified anything, but let’s move on.

Brevity

You can compress the transformation composition code quite a bit:

- (void)loadView
{
	picker = [[UIPickerView alloc] initWithFrame:CGRectZero];
	picker.delegate = self;
	picker.dataSource = self;
	picker.showsSelectionIndicator = YES;

	CGFloat dX=picker.bounds.size.width/2, dY=picker.bounds.size.height/2;
	picker.transform = CGAffineTransformTranslate(CGAffineTransformScale(CGAffineTransformMakeTranslation(-dX, -dY), 0.5, 0.5), dX, dY);

	self.view = [[UIView alloc] initWithFrame:CGRectZero];
	[self.view addSubview:picker];
}

Note that the transformations are assembled “backwards” in this idiom; the “innermost” or “first” transformation appears on the “outside” of the expression (its arguments appear last, on the right). Essentially, these functions take an existing transform and prepend a newly defined one to its left.

You can download the final header and implementation files, if you’re interested in them.

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.