Popovers are very common within the iPad user interface but you were restricted to the design provided by Apple. With iOS 5 came a little known class called UIPopoverBackgroundView which allows you to provide a custom border and arrow for the popover.

The popover

The UIPopoverController is the class that facilitates the popover view. It takes a custom view controller and then displays it with a neat border and arrow, where the arrow points to its origin. It is ideal for displaying contextual information. The popover user interface is essentially made of three main parts: the border, content, and arrow.

UIPopoverController

Images

Before we look at the UIPopoverBackgroundView you will need two images to customize your popover. One image for the border and the other for the arrow.

Dissecting the background image

When designing the background image it is important to note that the image will be stretched. UIImage allows you to create a stretchable image by defining cap insets. These caps define portions of the image that will not be rescaled whereas the rest of the image is easily tiled when stretched. As seen in the image below the dark area is what will be tiled and the colored corners will not.

Subclassing UIPopoverBackgroundView

The UIPopoverBackgroundView is an abstract class which has no implementation. We need to subclass it and provide implementations for all its methods and properties. You can read the Apple documentation on all its properties and methods. What is not in the documentation is how to layout the border and arrow using the method layoutSubviews.

First let’s start by creating an Interface and subclassing the UIPopoverBackgroundView.

#import 

@interface CustomPopoverBackgroundView : UIPopoverBackgroundView {
    UIImageView *_borderImageView;
    UIImageView *_arrowView;
    CGFloat _arrowOffset;
    UIPopoverArrowDirection _arrowDirection;
}

@end

Make sure to specify your import statement or else the code will not compile. An explanation for each of the instance variables:

  • _borderImageView: contains the image for the border
  • _arrowView: contains the image for the arrow
  • _arrowOffset: used for the property arrowOffset specified in the Interface for UIPopoverBackgroundView. We will see later how this value is used to calculate the position for the arrow.
  • _arrowDirection: used for the property arrowDirection specified in the Interface for UIPopoverBackgroundView

Let’s fill out the implementation, starting with the designated initializer

#import "CustomPopoverBackgroundView.h"

#define CONTENT_INSET 10.0
#define CAP_INSET 25.0
#define ARROW_BASE 25.0
#define ARROW_HEIGHT 25.0

@implementation CustomPopoverBackgroundView


-(id)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        _borderImageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"popover-bg.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(CAP_INSET,CAP_INSET,CAP_INSET,CAP_INSET)]];
        
        _arrowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"arrow.png"]];
        
        [self addSubview:_borderImageView];
        [self addSubview:_arrowView];
        
    }
    return self;
}

Basically we are allocating and initializing the two views and adding them as subviews. Notice how the background image is defined with cap insets. Next, let’s implement all the required methods including the getters and setters for the properties.


- (CGFloat) arrowOffset {    
    return _arrowOffset;    
}

- (void) setArrowOffset:(CGFloat)arrowOffset {    
    _arrowOffset = arrowOffset;
}

- (UIPopoverArrowDirection)arrowDirection {    
    return _arrowDirection;    
}

- (void)setArrowDirection:(UIPopoverArrowDirection)arrowDirection {    
    _arrowDirection = arrowDirection;
}


+(UIEdgeInsets)contentViewInsets{
    return UIEdgeInsetsMake(CONTENT_INSET, CONTENT_INSET, CONTENT_INSET, CONTENT_INSET);
}

+(CGFloat)arrowHeight{
    return ARROW_HEIGHT;
}

+(CGFloat)arrowBase{
    return ARROW_BASE;
}

The above methods are fairly straightforward with the exception of contentViewInsets. This method determines the thickness of your border. The higher the number the thicker your border. Finally, the method that lays out our two subviews in their appropriate sizes and location.


-  (void)layoutSubviews {
    [super layoutSubviews];

    CGFloat _height = self.frame.size.height;
    CGFloat _width = self.frame.size.width;
    CGFloat _left = 0.0;
    CGFloat _top = 0.0;
    CGFloat _coordinate = 0.0;
    CGAffineTransform _rotation = CGAffineTransformIdentity;

    
    switch (self.arrowDirection) {
        case UIPopoverArrowDirectionUp:
            _top += ARROW_HEIGHT;
            _height -= ARROW_HEIGHT;
            _coordinate = ((self.frame.size.width / 2) + self.arrowOffset) - (ARROW_BASE/2);
            _arrowView.frame = CGRectMake(_coordinate, 0, ARROW_BASE, ARROW_HEIGHT);            
            break;
            
            
        case UIPopoverArrowDirectionDown:
            _height -= ARROW_HEIGHT;
            _coordinate = ((self.frame.size.width / 2) + self.arrowOffset) - (ARROW_BASE/2);
            _arrowView.frame = CGRectMake(_coordinate, _height, ARROW_BASE, ARROW_HEIGHT); 
            _rotation = CGAffineTransformMakeRotation( M_PI );
            break;
            
        case UIPopoverArrowDirectionLeft:
            _left += ARROW_BASE;
            _width -= ARROW_BASE;
            _coordinate = ((self.frame.size.height / 2) + self.arrowOffset) - (ARROW_HEIGHT/2);
            _arrowView.frame = CGRectMake(0, _coordinate, ARROW_BASE, ARROW_HEIGHT); 
            _rotation = CGAffineTransformMakeRotation( -M_PI_2 );
            break;
            
        case UIPopoverArrowDirectionRight:
            _width -= ARROW_BASE;
            _coordinate = ((self.frame.size.height / 2) + self.arrowOffset)- (ARROW_HEIGHT/2);
            _arrowView.frame = CGRectMake(_width, _coordinate, ARROW_BASE, ARROW_HEIGHT); 
            _rotation = CGAffineTransformMakeRotation( M_PI_2 );

            break;
            
    }
    
    _borderImageView.frame =  CGRectMake(_left, _top, _width, _height);

    
    [_arrowView setTransform:_rotation];

}

@end

The switch statement determines the direction of the arrow and then calculates the location of the arrow and its rotation. Our default arrow image points upwards so we need to change its rotation using an affine transform which takes in radians. The arrowOffset is calculated and set by the UIPopoverController which essentially tells us the distance of the arrow from the center of content view. We also have to adjust the height and width of our border view to account for the arrow.

Using the CustomPopoverBackgroundView

Now that we have created the CustomPopoverBackgroundView we need to set it when creating an instance of the UIPopoverController

UIPopoverController *popoverController = [[UIPopoverController alloc] initWithContentViewController:contentViewController] ;

popoverController.popoverBackgroundViewClass = [CustomPopoverBackgroundView class];

Note: the above code will work only in iOS 5

Now run your app and marvel at your newly designed popover.

Subclassing UIPopoverBackgroundView