iPad for iPhone Developers 101 in iOS 6: UIPopoverController Tutorial
This is the second part of a four-part series to help get iPhone Developers up-to-speed with iPad development by first focusing on three of the most useful classes: UISplitView, UIPopoverController, and Custom Input Views. By Ellen Shapiro.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iPad for iPhone Developers 101 in iOS 6: UIPopoverController Tutorial
10 mins
Update 3/7/2013: Fully updated for iOS 6 (original post by Ray Wenderlich, update by Ellen Shapiro).
This is the second part of a four-part series to help get iPhone Developers up-to-speed with iPad development by first focusing on three of the most useful classes: UISplitView, UIPopoverController, and Custom Input Views.
In the first part of the series, you made an app with a UISplitViewController which displays a list of monsters on the left side, and details for the selected monster on the right side.
In this second installment, you’re going to try out Popover views with a simple example: you’ll add a popover to let the user select from a list of colors to change the color of the UILabel displaying the monster’s name. (Or you can jump to Part 3) in the series on Custom Input Views.)
You’ll start out with where you left off the project after part 1, so grab a copy if you don’t have it already.
Creating Your Color Picker
Let’s start by creating the view that you’ll use to let the user pick from a list of colors. You’ll make this a simple table view with a list of color names.
Go to File\New\File… and select the iOS\Cocoa Touch\Objective-C class template. Enter ColorPickerViewController for the name, UITableViewController for the subclass, and leave both checkboxes unchecked. Click Next, and then Create.
Then replace ColorPickerViewController.h with the following:
#import <UIKit/UIKit.h>
@protocol ColorPickerDelegate <NSObject>
@required
-(void)selectedColor:(UIColor *)newColor;
@end
@interface ColorPickerViewController : UITableViewController
@property (nonatomic, strong) NSMutableArray *colorNames;
@property (nonatomic, weak) id<ColorPickerDelegate> delegate;
@end
Here you declare a delegate so that this class can notify another class when a user selects a color. Note that you are not presently breaking this delegate out into a separate file because anything that would need to know about a color being selected would be presenting this view controller. In the future, you can move it out to a separate file if you find you need to.
You then declare two properties: one for the list of colors to display, and one to store the delegate itself.
Open ColorPickerController.m and replace initWithStyle: with the following:
-(id)initWithStyle:(UITableViewStyle)style
{
if ([super initWithStyle:style] != nil) {
//Initialize the array
_colorNames = [NSMutableArray array];
//Set up the array of colors.
[_colorNames addObject:@"Red"];
[_colorNames addObject:@"Green"];
[_colorNames addObject:@"Blue"];
//Make row selections persist.
self.clearsSelectionOnViewWillAppear = NO;
}
return self;
}
In the previous version of this tutorial, there was also another line:
self.contentSizeForViewInPopover = CGSizeMake(150, 140);
This line sets the size of how large the popover container should be when it is displayed. The problem with doing this with hard-coded numbers is that if you add more options or change the names of any of the items, your content is not going to fit.
Instead, you’re going to do a little bit of simple math so you can add more items or change the names of your colors so that they will always fit.
Directly below self.clearsSelectionOnViewWillAppear = NO, add the following code to the init method:
//Calculate how tall the view should be by multiplying
//the individual row height by the total number of rows.
NSInteger rowsCount = [_colorNames count];
NSInteger singleRowHeight = [self.tableView.delegate tableView:self.tableView
heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
NSInteger totalRowsHeight = rowsCount * singleRowHeight;
//Calculate how wide the view should be by finding how
//wide each string is expected to be
CGFloat largestLabelWidth = 0;
for (NSString *colorName in _colorNames) {
//Checks size of text using the default font for UITableViewCell's textLabel.
CGSize labelSize = [colorName sizeWithFont:[UIFont boldSystemFontOfSize:20.0f]];
if (labelSize.width > largestLabelWidth) {
largestLabelWidth = labelSize.width;
}
}
//Add a little padding to the width
CGFloat popoverWidth = largestLabelWidth + 100;
//Set the property to tell the popover container how big this view will be.
self.contentSizeForViewInPopover = CGSizeMake(popoverWidth, totalRowsHeight);
Now, you’ll always be able to make sure that your items fit properly into the popover. Now that you’re done with that portion, scroll down until you get to the Table View Data Source section and update the Data Source methods:
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [_colorNames count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell...
cell.textLabel.text = [_colorNames objectAtIndex:indexPath.row];
return cell;
}
Note that unlike in RightViewController, this class is not included in the Storyboard, so the reuse identifier is not referenced within it. Because of that, you’re going to need to use a different method to get a reusable UITableView cell – you simply use dequeueReusableCellWithIdentifier: without referencing the index path, and then if that returns nil, you create a new cell using the default style.
Now add the UITableViewDelegate method for row selection, where you’ll notify the ColorSelectionDelegate that a new color has been selected:
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *selectedColorName = [_colorNames objectAtIndex:indexPath.row];
//Create a variable to hold the color, making its default
//color something annoying and obvious so you can see if
//you've missed a case here.
UIColor *color = [UIColor orangeColor];
//Set the color object based on the selected color name.
if ([selectedColorName isEqualToString:@"Red"]) {
color = [UIColor redColor];
} else if ([selectedColorName isEqualToString:@"Green"]){
color = [UIColor greenColor];
} else if ([selectedColorName isEqualToString:@"Blue"]) {
color = [UIColor blueColor];
}
//Notify the delegate if it exists.
if (_delegate != nil) {
[_delegate selectedColor:color];
}
}
Now, your class for the picker is complete. Time to display it!
Displaying the Picker
Believe it or not, creating that TableViewController subclass was the hardest part. Now to display the picker, all you need to do is add a button to your Navigation bar, and a little bit of code to display it and handle the selection.
So first, let’s add the button. Open MainStoryboard_iPad.storyboard, find the Right View Controller, and add a UIBarButtonItem to its Navigation bar’s NavigationItem on the right side.
Remember, you don’t want to put it on the left since that’s where the button to trigger the popover for the UISplitViewController is going. Make the title of the button Choose Color.
Now open RightViewController.h to set up a few things. First, import the ColorPickerViewController class to access both it and the ColorPickerDelegate protocol:
#import "ColorPickerViewController.h"
Then modify RightViewController’s class declaration to declare that it conforms to the ColorPickerDelegate protocol:
@interface RightViewController : UIViewController <MonsterSelectionDelegate, UISplitViewControllerDelegate, ColorPickerDelegate>
Then add two new properties and a new IBAction method:
@property (nonatomic, strong) ColorPickerViewController *colorPicker;
@property (nonatomic, strong) UIPopoverController *colorPickerPopover;
-(IBAction)chooseColorButtonTapped:(id)sender;
Go ahead and connect the action method to the Bar Button Item in MainStoryboard_iPad.storyboard by control-clicking on the RightViewController object and dragging from the chooseColorButtonTapped: Received action over to the Choose Color UIBarButtonItem.
Then let’s finish by adding the following methods to RightViewController.m, at the bottom just above @end:
#pragma mark - IBActions
-(IBAction)chooseColorButtonTapped:(id)sender
{
if (_colorPicker == nil) {
//Create the ColorPickerViewController.
_colorPicker = [[ColorPickerViewController alloc] initWithStyle:UITableViewStylePlain];
//Set this VC as the delegate.
_colorPicker.delegate = self;
}
if (_colorPickerPopover == nil) {
//The color picker popover is not showing. Show it.
_colorPickerPopover = [[UIPopoverController alloc] initWithContentViewController:_colorPicker];
[_colorPickerPopover presentPopoverFromBarButtonItem:(UIBarButtonItem *)sender
permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
} else {
//The color picker popover is showing. Hide it.
[_colorPickerPopover dismissPopoverAnimated:YES];
_colorPickerPopover = nil;
}
}
#pragma mark - ColorPickerDelegate method
-(void)selectedColor:(UIColor *)newColor
{
_nameLabel.textColor = newColor;
//Dismiss the popover if it's showing.
if (_colorPickerPopover) {
[_colorPickerPopover dismissPopoverAnimated:YES];
_colorPickerPopover = nil;
}
}
Ok let’s explain this a bit. All popovers are is a “wrapper” around an existing view controller that “floats” it in a certain spot and possibly displays an arrow showing what the popover is related to. You can see this in chooseColorButtonTapped – you create your color picker, and then wrap it with a popover controller.
Then you call a method on the popover controller to display it in the view. You use the helper function presentPopoverFromBarButtonItem to display the popover.
When the user is done, they can tap anywhere outside the popover to dismiss it automatically. However if they select a color, you also want it to be dismissed, so you call the dismissPopoverAnimated method to get rid of the popover on-demand (as well as setting the color appropriately).
And that’s it! Compile and run and when you tap the “Choose Color” bar button item, you should see a popover like the following that changes the label color:
You’ll also note that if you go in to ColorPickerViewController.h and change the name of the blue color from “Blue” to “Delightful Sky Blue” and re-run the application, the size of the popover changes to accommodate it without having to do any additional work:
This also works if you add more colors (although you will also need to add code in the tableView:didSelectRowAtIndexPath: method in ColorPickerViewController.m that will handle selection of any additional colors):
You will find yourself using popovers quite a bit in places where users need to edit a field or toggle a setting, rather than the iPhone style where you navigate to the next level in a UINavigationController – it makes it much easier and faster to make selections, and it makes it much more pleasant for the user.