If have you have ever wanted to make the arrows of your popovers that little bit more ‘pointy’, or you have hankered after a co-respondent popover, for example
then help is at hand courtesy of the UIPopoverBackgroundView
class.
The UIPopoverBackgroundView Class
The UIPopoverBackgroundView
class defines two properties.
The arrowDirection Property
var arrowDirection: UIPopoverArrowDirection
The documentation for this reads
The direction in which the popover arrow is pointing.
which is entirely obvious unless it is not.
A picture sometimes helps.
Arrows And Their Directions
The arrowOffset Property
var arrowOffset: CGFloat
The documentation uses a lot of words to explain this.
Here are some pictures instead
Offset For Vertical Arrows
Offset For Horizontal Arrows
The UIPopoverBackgroundViewMethods Protocol
The UIPopoverBackgroundView
class implements the UIPopoverBackgroundViewMethods
protocol
The arrowBase Method
static func arrowBase() -> CGFloat
Note that this is a static, i.e., class method
The base of an arrow (see below) must be the same for all directions and it must not change.
The arrowHeight Method
static func arrowHeight() -> CGFloat
Note that this is a static, i.e., class method
The height of an arrow (see below) must be the same for all directions and it must not change.
The contentViewInsets Method
static func contentViewInsets() -> UIEdgeInsets
Note that this is a static, i.e., class method
This method specifies the distances between the edges of the popover’s content and the edges of the background view’s frame. exclusive of the arrow.
Arrow Bases And Heights
Subclassing The UIPopoverBackgroundView Class
The only way to make use of the UIPopoverBackgroundView
class is to sub-class it and override both it properties and the three methods defined by the UIPopoverBackgroundViewMethods
protocol.
The arrowDirection Property
It is not possible to override the arrowDirection
property with a stored property so we need to define a computed
property.
To start with we don’t really know what we should be doing when getting or setting the value but the pseudo-code looks like this
override var arrowDirection : UIPopoverArrowDirection
{
get
{
return ????
}
set
{
???? = newValue
}
}
The arrowOffset Property
The same holds true for the arrowOffset
property. It needs to be overridden by a computed property and we don’t really know what we will need to do.
override var arrowOffset : CGFloat
{
get
{
return ????
}
set
{
???? = newValue
}
}
The arrowBase Method
This is a ‘class’ method so there is not a lot of room for manoeuvre, We will will need to return some kind of constant value obtained from somewhere.
override static func arrowBase() -> CGFloat
{
return ????
}
The arrowHeight Method
The same is true for the arrowHeight method.
override static func arrowHeight() -> CGFloat
{
return ????
}
The contentViewInsets Method
This one we can do straight away
override static func contentViewInsets() -> UIEdgeInsets
{
return UIEdgeInsets(top: 10.0, left: 10.0, bottom:10.0, right: 10.0)
}
but its really not that exciting.
A Direction Digression Or When Is An Enum Not An Enum ?
For our purposes the arrow direction can be one of
This can be represented by the Swift enum
enum Direction
{
case UP
case DOWN
case LEFT
case RIGHT
}
which results in switch statements that look like this
switch direction
{
case .UP:
...
case .DOWN:
...
case .LEFT:
...
case .RIGHT:
...
}
What the API uses to represent the direction of an arrow is the type
UIPopoverArrowDirection
which results in switch statements that look like this
switch direction
{
case UIPopoverArrowDirection.Up:
...
case .UIPopoverArrowDirection.Down:
...
case .UIPopoverArrowDirection.Left:
...
case .UIPopoverArrowDirection.Right:
...
default:
...
}
because in Swift UIPopoverArrowDirection
is not an enum at all.
In Objective-C UIPopoverArrowDirection
is defined like this
typedef NSUInteger UIPopoverArrowDirection;
There are some associated constants defined using an anonymous enum
enum {
UIPopoverArrowDirectionUp = 1UL << 0,
UIPopoverArrowDirectionDown = 1UL << 1,
UIPopoverArrowDirectionLeft = 1UL << 2,
UIPopoverArrowDirectionRight = 1UL << 3,
UIPopoverArrowDirectionAny = UIPopoverArrowDirectionUp | UIPopoverArrowDirectionDown |
UIPopoverArrowDirectionLeft | UIPopoverArrowDirectionRight,
UIPopoverArrowDirectionUnknown = NSUIntegerMax
};
This is a standard ‘C’ idiom which makes it possible to specify both an arrow direction and a set of arrow directions as values of the ‘type’ UIPopoverArrowDirection
.
This translates to something like the following in Swift
struct UIPopoverArrowDirection : RawOptionSetType {
init(_ rawValue: UInt)
init(rawValue rawValue: UInt)
static var Up: UIPopoverArrowDirection { get }
static var Down: UIPopoverArrowDirection { get }
static var Left: UIPopoverArrowDirection { get }
static var Right: UIPopoverArrowDirection { get }
static var Any: UIPopoverArrowDirection { get }
static var Unknown: UIPopoverArrowDirection { get }
}
which provides a bit of syntactic sugar, and a little more type safety in that you cannot accidentally pass any old UInt
to something expecting a UIPopoverArrowDirection
value. Now you have to wrap it in a struct first !
In our case, semantically at least, the API shouldn’t be passing a value that is not equal to one of the static values Up, Down, Left, or Right but as the compiler is making clear there is nothing to stop it doing so programatically, hence the need for a ‘default’ case.
The problem with having to deal with a default case is that increases the complexity of the code for no gain whatsoever.
Each time we switch on a value of ‘type’ UIPopoverArrowDirection
we have to decide what the ‘right thing’ to do is in the default case, even if that thing is nothing.
If we change the code containing the switch then the decision needs to be taken once again.
If we need to add another switch you need to make the decision again.
Its much easier and safer to work with values of type Direction
internally, converting from the UIPopoverArrowDirection
value at the point it is set and converting back again when the value is accessed
We can extend Direction
quite simply to do the conversions for us
extension Direction
{
static func fromPopoverArrowDirection(direction:UIPopoverArrowDirection) -> Direction?
{
switch direction
{
case UIPopoverArrowDirection.Up:
return .UP
case UIPopoverArrowDirection.Down:
return .DOWN
case UIPopoverArrowDirection.Left:
return .LEFT
case UIPopoverArrowDirection.Right:
return .RIGHT
default:
return nil
}
}
func toPopoverArrowDirection() -> UIPopoverArrowDirection
{
switch self
{
case .UP:
return UIPopoverArrowDirection.Up
case .DOWN:
return UIPopoverArrowDirection.Down
case .LEFT:
return UIPopoverArrowDirection.Left
case .RIGHT:
return UIPopoverArrowDirection.Right
}
}
}
We’re Going To Need An ‘Arrow’
So far its been all arrows all the time so let’s define a nifty structure to hold all the arrow related stuff
private struct Arrow
{
let height : CGFloat = 30.0
let base : CGFloat = 20.0
var direction : Direction = .UP
var offset : CGFloat = 0.0
}
a variable for storing one
private var arrow : Arrow = Arrow()
and a ‘prototype’ arrow for the static methods to access
private static let PROTO_ARROW = Arrow()
New And Improved
We can now flesh out the implementations of the overridden methods and properties like so
override static func arrowBase() -> CGFloat
{
return PROTO_ARROW.base
}
override static func arrowHeight() -> CGFloat
{
return PROTO_ARROW.height
}
For the arrowDirection
property, if we are ever handed an ‘invalid’ UIPopoverArrowDirection
value we simply drop it on the floor. Its not clear what else we could do.
override var arrowDirection : UIPopoverArrowDirection
{
get
{
return arrow.direction.toPopoverArrowDirection()
}
set
{
if let direction = Direction.fromPopoverArrowDirection(newValue)
{
arrow.direction = direction
}
}
}
override var arrowOffset: CGFloat
{
get
{
return arrow.offset
}
set
{
arrow.offset = newValue
}
}
Now What ?
Now we have to provide the ‘background’ for the popover.
By default the background is simply an arrow plus a rectangle.
In theory we could draw it but the documentation states that we should use images.
To do this we need to add two instances of UIImageView
as subviews, one for the arrow and one for the ‘rest’
The basic task is to work out where the two components of the background should go.
We need to set the frames of the two subviews on the basis of the arrow base and height, and the current values for the arrow’s direction and offset.
We can define a method on the Arrow
struct which returns its frame given the bounds of its super view
func frame(container:CGRect) -> CGRect
{
let containerMidX = CGRectGetMidX(container)
let containerMidY = CGRectGetMidY(container)
let containerWidth = container.size.width
let containerHeight = container.size.height
let halfBase = base/2.0
let x : CGFloat
let y : CGFloat
let size : CGSize = frameSize()
switch direction
{
case .UP:
x = containerMidX + offset - halfBase
y = 0.0
case .DOWN:
x = containerMidX + offset - halfBase
y = containerHeight - height
case .LEFT:
x = 0.0
y = containerMidY + offset - halfBase
case .RIGHT:
x = containerWidth - height
y = containerMidY + offset - halfBase
}
return CGRect(x: x, y: y, width:size.width, height:size.height)
}
When the arrow is ‘vertical’ the x coordinate of the origin is the same, and the arrow is either at the ‘top’ if the direction is .UP, or at the bottom if the direction is .DOWN.
When the arrow is ‘horizontal’ the y coordinate of the origin is the same and the arrow is on the left if the direction is .LEFT or on the right if the direction is .RIGHT.
This method calls the frameSize method
private func frameSize() -> CGSize
{
switch direction
{
case .UP,
.DOWN:
return CGSize(width: base, height: height)
case .LEFT,
.RIGHT:
return CGSize(width: height, height: base)
}
}
Since the layout is a function of the current values of the arrow’s direction and offset we need to add calls to the setNeedLayout
method each time either of these values change.
override var arrowDirection : UIPopoverArrowDirection
{
get
{
return arrow.direction.toPopoverArrowDirection()
}
set
{
if let direction = Direction.fromPopoverArrowDirection(newValue)
{
arrow.direction = direction
setNeedsLayout()
}
}
}
override var arrowOffset: CGFloat
{
get
{
return arrow.offset
}
set
{
arrow.offset = newValue
setNeedsLayout()
}
}
This ensures that the layoutSubviews method will be called when necessary.
override func layoutSubviews()
{
let arrowFrame = arrow.frame(self.bounds)
var backgroundFrame = self.bounds
switch arrow.direction
{
case .UP:
backgroundFrame.origin.y += arrowFrame.height
backgroundFrame.size.height -= arrowFrame.height
case .DOWN:
backgroundFrame.size.height -= arrowFrame.height
case .LEFT:
backgroundFrame.origin.x += arrowFrame.width
backgroundFrame.size.width -= arrowFrame.width
case .RIGHT:
backgroundFrame.size.width -= arrowFrame.width
}
backgroundView?.frame = backgroundFrame
arrowView?.image = arrowImages[arrow.direction]
arrowView?.frame = arrowFrame
}
Wiring It All Up
To get our UIPopoverBackgroundView
sub-class to be used for a popover being presented via a segue we need to set the property
var popoverBackgroundViewClass: AnyObject.Type?
of the UIPopoverPresentationController
instance, managing the popover’s display.
This can be done in the appropriate prepareForSegue
method
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
if let segueId = segue.identifier
{
switch segueId
{
case "CustomPopoverSegue":
let dvc = segue.destinationViewController as? UIViewController
let ppc = dvc?.popoverPresentationController
ppc?.popoverBackgroundViewClass = CustomPopoverBackgroundView.self
default:
break
}
}
}
Note that it is the class itself not an instance of the class which is assigned to the property.
About That Arrow Drawing …
It turns out that no actual drawing of arrows is required at runtime which is a bit disappointing.
This is not strictly true, you can draw them, it does appear to work, but you are supposed to use images.
You can, if you wish, use a single image and rotate it as necessary.
Alternatively, for those of us who do not entirely trust affine transformations, after all how do you know what they are really doing ?, you simply have one image for each direction, which you can of course draw, so all is not lost.
Here’s some very simple code for drawing a triangle on Mac OS X.
func drawArrow(width:Int, height:Int, vertices:(CGPoint, CGPoint, CGPoint), colour:NSColor) -> NSData?
{
let bitmap = NSBitmapImageRep(
bitmapDataPlanes:
nil,
pixelsWide:
width,
pixelsHigh:
height,
bitsPerSample:
8,
samplesPerPixel:
4,
hasAlpha:
true,
isPlanar:
false,
colorSpaceName:
NSCalibratedRGBColorSpace,
bytesPerRow:
(4 * width),
bitsPerPixel:
32)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.setCurrentContext(NSGraphicsContext(bitmapImageRep:bitmap!))
let (v0, v1, v2) = vertices
let path = NSBezierPath()
path.moveToPoint(v0)
path.lineToPoint(v1)
path.lineToPoint(v2)
path.lineToPoint(v0)
path.closePath()
colour.setFill()
path.fill()
NSGraphicsContext.restoreGraphicsState()
return bitmap?.representationUsingType(.NSPNGFileType, properties: [NSObject: AnyObject]())
}
Copyright (c) 2015 By Simon Lewis. All Rights Reserved.
Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.
Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.