Aleksandar • Vacić

iOS bits and pieces

RTFlyoutMenu - drop-down menu component for iOS

I’ve long had a fascination with fly-out or drop-down menus. One of my most successful web components was ADxMenu which I continually developed for years, as things and technologies evolved in web space.

On iOS though, screens were smaller in the beginning and different UI concepts were dominant thus I mostly forgot about this. Until recently, when I had the need to implement a multi-part filtering component for an iPad app I was creating.

So, RTFlyoutMenu was born.

How it works

Basic idea is this:

  • Show a list of main items, which are filter categories.
  • When user taps any of them, show a list of sub items (filter options).
  • When sub-item is tapped, replace main item with it and close the submenu.
  • Main item is shown at the bottom of the submenu, so you can reset back to default.

Thus better name is maybe RTFilterMenu, but I started with fly-out and left it as such. How it works in the end depends a lot on how you implement delegate methods, so it can be both filter and/or a menu.

What I needed for this particular instance is to quickly filter a very large catalog. In practice, this works extremely well to narrow down to just items of particular materials, colors, seasons etc.

Implementation

I modeled it per UITableView and its friends. So you have RTFlyoutMenuDataSource and RTFlyoutMenuDelegate.

Data source is self-explanatory. You define main-level items and then sub-menu items for each of them.

1
2
3
4
5
6
7
8
@protocol RTFlyoutMenuDataSource <NSObject>

- (NSUInteger)numberOfMainItemsInFlyoutMenu:(RTFlyoutMenu *)flyoutMenu;
- (NSString *)flyoutMenu:(RTFlyoutMenu *)flyoutMenu titleForMainItem:(NSUInteger)mainItemIndex;
- (NSUInteger)flyoutMenu:(RTFlyoutMenu *)flyoutMenu numberOfItemsInSubmenu:(NSUInteger)mainItemIndex;
- (NSString *)flyoutMenu:(RTFlyoutMenu *)flyoutMenu titleForSubItem:(NSUInteger)subItemIndex inMainItem:(NSUInteger)mainItemIndex;

@end

Delegate methods tell you what user has tapped and thus you can respond to those changes.

1
2
3
4
5
6
7
8
@protocol RTFlyoutMenuDelegate <NSObject>

@optional
- (void)flyoutMenu:(RTFlyoutMenu *)flyoutMenu didSelectMainItemWithIndex:(NSInteger)index;
- (void)flyoutMenu:(RTFlyoutMenu *)flyoutMenu didSelectSubItemWithIndex:(NSInteger)subIndex mainMenuItemIndex:(NSInteger)mainIndex;
- (void)didReloadFlyoutMenu:(RTFlyoutMenu *)flyoutMenu;

@end

Initial setup

RTFlyoutMenu is UIView and thus you can place it anywhere you want and it will automatically try to center itself in it. By default, I place in a container view. You must have the container and pass it as canvasView to the menu itself. Here’s a crude setup from the demo app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];

  NSDictionary *options = @{
      RTFlyoutMenuUIOptionInnerItemSize: [NSValue valueWithCGSize:CGSizeMake(22, 22)],
      RTFlyoutMenuUIOptionSubItemPaddings: [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(10, 15, 10, 15)]
  };
  RTFlyoutMenu *m = [[RTFlyoutMenu alloc] initWithDelegate:self dataSource:self position:kRTFlyoutMenuPositionTop options:options];
  m.canvasView = self.view;

  CGRect mf = m.frame;
  CGRect cf = self.menuContainerView.bounds;

  // center menu in container view
  CGFloat newOriginX = (cf.size.width - mf.size.width) / 2;
  CGFloat newOriginY = (cf.size.height - mf.size.height) / 2;
  if (newOriginX > 0) mf.origin.x = newOriginX;
  if (newOriginY > 0) mf.origin.y = newOriginY;
  m.frame = mf;

//   m.backgroundColor = [UIColor redColor];
  [self.menuContainerView addSubview:m];
  self.flyoutMenu = m;

  // look & feel
  [[RTFlyoutItem appearanceWhenContainedIn:[self.menuContainerView class], nil] setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
  [[RTFlyoutItem appearanceWhenContainedIn:[self.menuContainerView class], nil] setTitleShadowColor:[UIColor whiteColor] forState:UIControlStateNormal];

}

For the full set of options you can pass, take a look at the header file.

Improvements

There’s room for a lot more customization options (like colors, fonts etc), but this is what I needed for this particular project.

As it is, component will lightly try to fit whatever content you give it. So if your submenu has over 10 items, it will break them into columns. This is as far it goes though and will not check if all columns can fit.

On interface rotation it will recenter main menu, but any open submenus will remain where they were, which is not optimal. Again, in my particular use-case this was not an issue (all submenus were more or less the same size and placement) so I did not work on it.

And so on and on. There is always room for improvement, so fork away. :)