iPad for iPhone Developers 101 in iOS 6: UIPopoverController Tutorial

Ellen Shapiro

This post is also available in: Chinese (Simplified)

Finished Popover Changing Text Color

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.

Choose Color Button

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:

Finished Full View

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:

Popover Resizing to accomodate text

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):

Adjusting For More 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.

Show Me the Code!

Here’s a copy of all of the code you’ve developed so far.

Check out the next part of the series, where you’ll learn how to use custom input views on the iPad!

designatednerd

Ellen Shapiro is a mobile developer in Chicago, Illinois who builds iOS and Android apps for Vokal Interactive, and is working in her spare time to help bring Hum to life. She’s also developed several independent applications through her personal company, Designated Nerd Software.

When she's not writing code, she's usually tweeting about it.

User Comments

27 Comments

[ 1 , 2 ]
  • Yes, though it's not entirely obvious. If you throw a breakpoint in prepareForSegue:, you can print out the segue passed in as a parameter's destinationViewController - you have to use a bracket method rather than a dot property for this to work, but when I type in:

    Code: Select all

    po [segue destinationViewController]


    ...the debugger prints out:
    Code: Select all

    $0 = 0x075694e0 <UINavigationController: 0x75694e0>


    And you'd have seen that the destination VC is a nav controller.

    One thing to keep in mind when using a Storyboard is that the Storyboard can make it a little easier to see when you've got a nav controller you've forgotten about in the controller hierarchy since it's clearly shown in the Storyboard as a separate controller. If things aren't working the way they think you should, you should a) double-check the storyboard to make sure you're not forgetting about a nav controller and b) start printing out properties in the debugger.
    designatednerd
  • I don't want to tell you how long I've been struggling with trying to replace a SplitViewController with real popovers. I'm not sure I would really do this...maybe it wouldn't satisfy apple, but I just wanted to know how. I'm not at my computer now to test everything you've taught me, but I'm pretty sure I can get it to work now.

    Thank you!
    csann
  • Glad I was able to help!

    I don't think Apple will much care about you not using a UISplitViewController - there are legit reasons for you to put something just in a straight-up UIPopoverController, mostly surrounding how often your user will be using the things in the RightViewController vs. constantly needing to see the left. It's just not the most common use case, which is why the UISplitViewController exists.
    designatednerd
  • I didn't know you could type "po something" to get debug to tell you about the item. I found you actually can use dot notation ie "po segue.destinationViewController". I wonder if there are other commands I can enter? I'll add "learn more about the debugger" to my TTD list.
    csann
  • I'd advise taking a look at the beginner debugging tutorials, they can help you wrap your head around some of the tools available to help debug your app.

    Beginner tuts:
    http://www.raywenderlich.com/10209/my-app-crashed-now-what-part-1
    http://www.raywenderlich.com/10505/my-app-crashed-now-what-part-2

    There's also an intermediate tutorial that has more specifically about the po command, but I'll warn you that some of the techniques in there fall under the category of "killing flies with a sledgehammer" if you're just getting into iOS development: http://www.raywenderlich.com/28289/debugging-ios-apps-in-xcode-4-5
    designatednerd
  • Thanks for the tutorial. I am an experienced iPhone dev but for my first iPad conversion I came here first. Great stuff, as always. This is "iOS - Apple's missing developer documentation". I mean all this info is in the documentation but it's in a format that's unusable, at least to me. In Apple's docs, the things that you want to actually know are hidden away in long text passages full of things you don't want to know.

    Also on a general note the UISplitViewController concept is weak IMO. There might be use cases for it, but they're pretty rare. Mail.app is the prime example for this UI and to me the prime example of why it's no good. Why would I ever *not* want to see my inbox? That's just weird.

    Popovers, on the other hand - very cool.
    nikster
  • HI thanks for this wonderful tutorial.

    I would like to ask one doubt.When i touched outside the popover (ie on the view) popover is dismissing.Then when i press the tabbar button popover is not showing up.

    Could you please help me in solving this issue.
    Hoping for your reply.
    iSuj
  • Hi,Thanks for this wonderful tutorial.

    I would like to ask one doubt.When i touch outside the popover, it is dismissing.Then when i tap on the bar button popover is not showing.(ie only on the second touch popover is showing).

    Can you please help me how to solve this issue.
    iSuj
  • I just compiled this using iOS7 and got an error message (2 actually)

    'sizeWithFont:" is deprecated in iOS7 use sizeWithAttributes.

    And other message that is very similar.

    I would like to fix this, but for the life of me it seems I can't.

    Anybody know what to put in there.

    Bryan
    bryanschmiedeler
  • Hi,
    Running on iOS 7 if I tap Choose Color and then click away from the popover to hide it without selecting a colour the next time I tap Choose Color it doesn't show the popover until I tap for a second time!

    I've used this example in my app and would like to know the best fix for it.
    Thanks,
    Ed
    EdUK
  • Same problem as EdUK and iSuj. Any workarounds?
    lucasgondim
  • For anyone who is having the issue of needing to tap the "Choose Color" button twice after dismissing the popover via tapping outside of its bounds, here is a solution.

    Code: Select all

    if (!_colorPickerPopover.popoverVisible && _colorPickerPopover != nil) {
        _colorPickerPopover = nil;
    }


    Add the above code below the first if statement in the chooseColorButtonTapped IBAction method. What this effectively does is to set the value of _colorPickerPopover to nil if the popover isn't visible and it's not nil. Because this case occurs only when the popover is dismissed by tapping outside of its bounds, we can set _colorPickerPopover to nil ensuring that the next if statement is executed to display the popover again.

    Also, to fix one of the two deprecation warnings, replace the following code:

    Code: Select all

    self.contentSizeForViewInPopover = CGSizeMake(popoverWidth, totalRowsHeight);


    with

    Code: Select all

    self.preferredContentSize = CGSizeMake(popoverWidth, totalRowsHeight);


    Hope the above helps and makes sense!
    aliclubb
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Tammy Coron
  • Kirill Muzykov

... 49 total!

Update Team

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Heejun Han
  • Jesus Guerra
  • David Xie

... 33 total!

Subject Matter Experts

... 4 total!