
Source Code
The full source code for the project can be found on github.com at https://github.com/tobins/PathMenuExample. Feel free to use the code provided in the example as you wish. Just say hi on twitter (@tobins) if you found this post useful.
Problem
The Path 2.0 iPhone app (http://path.com) uses an expandable menu to free up some extra real estate. The Path app places a button in the lower left corner of the screen. When the user presses the button, menu items expand out from behind the button in a circular pattern at a fixed distance from the main button. To close the menu the user either selects one of the options presented or presses the main button again.

The following is a quick overview of how I implemented a navigation system similar to the Path iPhone App.
Setup
To keep things simple I set up a single class named ExpandableNavigation that will handle all the work. It has an init method that takes in an array of menu item buttons (UIViews), a main button, and distance the menu item buttons should travel from the center of the main button when expanded. I did this because I wanted developers to set up and configure buttons either programmatically or using interface builder.
For example here is the code for ViewController.m’s viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
// initialize ExpandableNavigation object with an array of buttons.
NSArray* buttons = [NSArray arrayWithObjects:button1, button2, button3, button4, button5, nil];
self.navigation = [[[ExpandableNavigation alloc] initWithMenuItems:buttons
mainButton:self.main
radius:120.0] autorelease];
}
The menu item buttons (1-5) were all added to the ViewController.xib and wired to UIButtons defined in ViewController.h. The main button is wired to the “main” UIButton. Passing in the array & the main button to the init method sets up the initial positioning of the menu item buttons and attaches a touch event to the main UIButton for handling the menu expanding/collapsing.
IMPORTANT NOTE: Make sure the main menu button is in front of all of the menu items you want controlled by the class.
As you can see below, the init method is pretty basic. It sets up some variables, aligns the menu items buttons to the center of the main button, and attaches a touch event to the main button.
- (id)initWithMenuItems:(NSArray*) menuItems mainButton:(UIButton*) mainButton radius:(CGFloat) radius {
if( self = [super init] ) {
self.menuItems = menuItems;
self.mainButton = mainButton;
self.radius = radius;
self.speed = 0.15;
self.bounce = 0.225;
self.bounceSpeed = 0.1;
expanded = NO;
transition = NO;
if( self.mainButton != nil ) {
for (UIView* view in self.menuItems) {
view.center = self.mainButton.center;
}
[self.mainButton addTarget:self action:@selector(press:) forControlEvents:UIControlEventTouchUpInside];
}
}
return self;
}
Expanding
When the main button is pressed the menu item buttons associated to it will expand from the center of the main button to the distance given in the init method. The menu items will be animated to bounce out and be equally spaced along the 90 degree edge of a circle.
The expand method uses block-based UIView animations and CGAffineTransformMakeRotation to handle the expanding of the menu.
- (void) expand {
transition = YES;
[UIView animateWithDuration:self.speed animations:^{
self.mainButton.transform = CGAffineTransformMakeRotation( 45.0 * M_PI/180 );
}];
for (UIView* view in self.menuItems) {
int index = [self.menuItems indexOfObject:view];
CGFloat oneOverCount = self.menuItems.count<=1?1.0:(1.0/(self.menuItems.count-1));
CGFloat indexOverCount = index * oneOverCount;
CGFloat rad =(1.0 - indexOverCount) * 90.0 * M_PI/180;
CGAffineTransform rotation = CGAffineTransformMakeRotation( rad );
CGFloat x = (self.radius + self.bounce * self.radius) * rotation.a;
CGFloat y = (self.radius + self.bounce * self.radius) * rotation.c;
CGPoint center = CGPointMake( view.center.x + x , view.center.y + y);
[UIView animateWithDuration: self.speed
delay: self.speed * indexOverCount
options: UIViewAnimationOptionCurveEaseIn
animations:^{
view.center = center;
}
completion:^(BOOL finished){
[UIView animateWithDuration:self.bounceSpeed
animations:^{
CGFloat x = self.bounce * self.radius * rotation.a;
CGFloat y = self.bounce * self.radius * rotation.c;
CGPoint center = CGPointMake( view.center.x - x , view.center.y - y);
view.center = center;
}];
if( view == self.menuItems.lastObject ) {
expanded = YES;
transition = NO;
}
}];
}
}
Note that the end point for the initial animation is farther than the radius specified in the init method. I did this so I could set up a second animation to do the “bounce” to the desired distance. You’ll see that in the completion block that I set up a second animation that returns the UIView back to the desired distance.
Collapse
The collapse method returns the menu item buttons back behind the main menu button.
- (void) collapse {
transition = YES;
[UIView animateWithDuration:self.speed animations:^{
self.mainButton.transform = CGAffineTransformMakeRotation( 0 );
}];
for (UIView* view in self.menuItems) {
int index = [self.menuItems indexOfObject:view];
CGFloat oneOverCount = self.menuItems.count<=1?1.0:(1.0/(self.menuItems.count-1));
CGFloat indexOverCount = index * oneOverCount;
[UIView animateWithDuration:self.speed
delay:(1.0 - indexOverCount) * self.speed
options: UIViewAnimationOptionCurveEaseIn
animations:^{
view.center = self.mainButton.center;
}
completion:^(BOOL finished){
if( view == self.menuItems.lastObject ) {
expanded = NO;
transition = NO;
}
}];
}
}
The collapse method is a little simpler than the expand as it doesn’t do a bounce when returning the menu items to behind the main button.
Extra Credit
I didn’t implement the Path 2.0 exactly as it is in the app, however this gives you a good starting point. Incase you’re interested, fork the repository and try your hand at one of the following:
* Calculate the ideal radius based on number of menu item buttons (and their sizes) passed to the init method and size of those buttons.
* Spin the menu items like the Path 2.0 App using UIView or Core Animation.
* Auto adjust the z-index of the views in the init method so that the main menu button is always on top.
Enjoy and don’t forget to say hi on twitter!
@tobins