In this installment, we’ll finish off the main functionality of the STKMenuViewController by implementing the STKMenuView and hooking it up to a gesture recognizer.

Make sure to check out the previous articles in this series here and here.

STKMenuView

The menu view will appear when the user taps three fingers on the screen. It will display a ring in the center of the screen. For each view controller that the STKMenuViewController contains, an icon will appear on this ring. Tapping the icon associated with a view controller will select that view controller and display its view.

MenuHookup

Thus, an STKMenuView needs to know how to do a few things.

  1. Appear and disappear.
  2. Get the icons it needs to display for each view controller.
  3. Alert the STKMenuViewController when one of the icons is tapped.
  4. For the best user experience, be able to ‘highlight’ the icon of the selected view controller.

We’ll then define the STKMenuView public interface like so:

@class STKMenuView;

@protocol STKMenuViewDelegate <NSObject>

- (NSArray *)itemsForMenuView:(STKMenuView *)menuView;
- (int)selectedIndexForMenuView:(STKMenuView *)sv;
- (void)menuView:(STKMenuView *)menuView didSelectItemAtIndex:(int)idx;

@end

@interface STKMenuView : UIControl

@property (nonatomic, weak) id <STKMenuViewDelegate> delegate;
@property (nonatomic, strong) NSArray *items;

- (void)setVisible:(BOOL)menuVisible animated:(BOOL)animated;

@end

Before we even write a line of code in STKMenuView.m, let’s work off the assumption that our public interface is reasonable and plug it into the STKMenuViewController.

It should be clear from the STKMenuViewDelegate protocol that the way the STKMenuView broadcasts changes to the selected index is through its delegate. It also uses its delegate to determine which items should be shown in its ring and which one is selected. The delegate of the menu view will naturally be the STKMenuViewController, because it knows of and can act on this information.

But, what exactly is one of these ‘items’ that is shown in the ring? Each item should be a model object that holds onto a two images: one when its associated view controller is selected and one when it isn’t. For sake of argument, let’s call each of these items an STKMenuItem.

To pull this off, let’s look at the existing pattern in UITabBarController. Every UIViewController has a tabBarItem that it creates and manages. When a view controller is added to a UITabBarController, the tab bar controller asks for the view controller’s tabBarItem. That item is drawn in the tab bar controller’s UITabBar.

One of the most valuable things you can do when you are writing any class is stick to the conventions in the rest of the SDK. The conventions exist for a reason – they are good design choices. Perhaps more important, a user of the class will be more likely to understand how to use it if they are already familiar with the pattern. So, it makes sense that every view controller that is added to a STKMenuViewController has a property to hang onto an STKMenuItem.

There are a few ways to accomplish this.

First, we could add a category to UIViewController and import that category’s header file into our project’s PCH file so we never had to think about it again.

@interface UIViewController (STKMenuViewController)

@property (nonatomic, strong) STKMenuItem *menuItem;

@end

This is not the most optimal solution. Extending a class with a category doesn’t allow you to add instance variables to the class. That is, there is no way to store an STKMenuItem in a view controller. This would require us to then use associated objects or some other ‘magic’. And while every Objective-C programmer at some points goes through their phase of isa swizzling, associating objects and automatic serialization with the runtime library, they eventually figure out there is a reason why those things aren’t common.

So, the second option would be to create a shim class for UIViewController: a UIViewController subclass that adds an STKMenuItem as a property. Then, all view controllers that are meant to be in the menu view controller inherit from this new shim class.

@interface STKBaseViewController : UIViewController

@property (nonatomic, strong) STKMenuItem *menuItem;

@end

Unfortunately, this is a really brittle solution in a language where only single inheritance is possible. What happens when you want to add a UITableViewController to the menu? OK, another shim class. Now what about a UINavigationController in the menu view controller? You see what I’m saying. You could still use some runtime magic tricks to accomplish this with only a minimal amount of disaster, but it still isn’t optimal.

The third option is to create a new protocol and leave it up to each view controller to conform to and handle storage for the STKMenuItem.

@protocol STKMenuController <NSObject>

@property (nonatomic, strong, readonly) STKMenuItem *menuItem;

@end

This is probably the safest bet. It’s clear, it’s obvious, and you only end up writing a line of extra code per view controller. Also, if you screw up and forget to have one of your view controllers conform to this protocol, you blow up really early and then it is an easy fix. (Bonus points for understanding why the protocol can have readonly in its property declaration while the other two should really use readwrite.)

So those are your options. But really, there is a fourth option that ends up working out more often than you might think: just use the existing UITabBarItem as the ‘menu item’. It has a selectedImage and image property. You don’t have to do anything special to your view controllers and you’re using an existing pattern. An unintended consequence of greatness: the client changes their mind and wants to use a basic tab bar controller, you don’t have to change any code!

So, our life is a little easier. In STKMenuViewController.m, we add the protocol declaration and the three delegate method implementations.

@interface STKMenuViewController () <STKMenuViewDelegate>

...

@implementation STKMenuViewController

- (NSArray *)itemsForMenuView:(STKMenuView *)menuView
{
    return [[self viewControllers] valueForKey:@"tabBarItem"];
}

- (int)selectedIndexForMenuView:(STKMenuView *)sv
{
    return [self selectedViewControllerIndex];
}

- (void)menuView:(STKMenuView *)menuView didSelectItemAtIndex:(int)idx
{
    if(idx != [self selectedViewControllerIndex])
        [self setSelectedViewController:[[self viewControllers] objectAtIndex:idx]];

    [self setMenuVisible:NO animated:YES];
}

By the way, notice that the protocol declaration is on the private interface. No other object needs to know that a STKMenuViewController conforms to this protocol, so there is no reason to advertise it. After all, if a STKMenuViewController starts publicly advertising that it conforms to STKMenuViewDelegate, maybe some other rogue menu views might come along and start sending it those types of messages. Probably not, but hey, if it does happen, the compiler will throw a loud error to reinforce that it can’t. Never ‘underestupid’ what someone else might do with your code.

No, underestupid isn’t a word, but neither is performant, so we’ll call it even.

Modifying STKMenuViewController

Now, we still aren’t going to write the code for the STKMenuView yet – it’s mostly layout and drawing stuff which isn’t the focus here. Instead, we’ll implement the two public methods on STKMenuViewController.m that toggle the menu appearing. It is important that these methods are in the public interface, because a child view controller may want to throw the menu up in response to some other user action.

@dynamic menuVisible;

- (void)setMenuVisible:(BOOL)menuVisible
{
    [self setMenuVisible:menuVisible animated:NO];
}

- (void)setMenuVisible:(BOOL)menuVisible animated:(BOOL)animated
{
    if(menuVisible) {
        [[self view] endEditing:YES];
    }
    [[self menuView] setVisible:menuVisible animated:animated];
}

- (BOOL)isMenuVisible
{
    return [[self menuView] isVisible];
}

Notice the common pattern of having an animated setter method, setMenuVisible: and setMenuVisible:animated:. You’ll see this all over the place in the iOS SDK. Like the designated initializer pattern, you want all of the real work to happen in only one method (the animated version) and have all other versions call that version. The default setter should always pass NO as the default.

Another thing to notice here is that the STKMenuView has its own setVisible:animated: method. This leaves it up to the STKMenuView to determine what visible and not visible mean and also how its own animation occurs. This also means that the STKMenuViewController should not maintain an instance variable that keeps the state of whether or not the menu is visible. If an object asks the menu view controller whether or not its menu is visible, it defers that answer to its menu view – who holds the true answer.

The important point here is that we don’t have two variables that refer to the same state, because that opens us up to those two values getting out of sync. By the way, this line of code,

@dynamic menuVisible;

is optional – the compiler can figure out that if a property has both the setter and getter overridden in the implementation that the instance variable is not automatically generated. I like to err on the side of being explicit, especially because the ‘default’ for property behaviors has been subject to change in recent years.

There is no built-in way to show the menu yet. In the previous installment, we wrote the code that would instantiate the STKMenuView and add it to the menu view controller’s view hierarchy, but we defaulted the menu view to not visible.

We decided early on that the menu would appear in response to a three-finger tap, so we must add a tap gesture recognizer to the menu view controller’s view hierarchy. First, let’s make the gesture recognizer a public property for STKMenuViewController. This allows child view controllers and other objects to see this gesture recognizer. This becomes important when a child view controller has its own gesture recognizers and must get those to interact appropriately with the STKMenuViewController’s gesture recognizer.

@interface STKMenuViewController : UIViewController

@property (nonatomic, strong, readonly) UITapGestureRecognizer *menuGestureRecognizer;

...

Then, in STKMenuViewController’s loadView method, we create this gesture recognizer and add it to the root view of the hierarchy.

- (void)loadView
{
    UIView *layoutView = [[UIView alloc] init];

    _menuGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenTap:)];

    [_menuGestureRecognizer setNumberOfTouchesRequired:3];
    [_menuGestureRecognizer setDelegate:self];
    [layoutView addGestureRecognizer:_menuGestureRecognizer];

    ...

The screenTap: action message should toggle the menu:

- (void)screenTap:(UITapGestureRecognizer *)gr
{
    if([gr state] == UIGestureRecognizerStateEnded) {
        [self setMenuVisible:YES animated:YES];
    }
}

And finally, the gesture recognizer should default to working well with other gesture recognizers:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

Now, that finishes off the STKMenuViewController. The code for the STKMenuView is a lot of layout and drawing code, so we won’t go over every bit of it. Instead, you can check it out on GitHub here. However, we do want to talk about the methods that interface with the menu view’s delegate – the STKMenuViewController that owns it.

When a menu view is told to become visible, it starts firing off its delegate messages to determine what items it should show:

- (void)setVisible:(BOOL)menuVisible animated:(BOOL)animated
{
    _visible = menuVisible;
    if(menuVisible) {
        [self setHidden:NO];
        [self setItems:[[self delegate] itemsForMenuView:self]];
        [self setNeedsLayout];          

        int selectedIndex = [[self delegate] selectedIndexForMenuView:self];
        [[self itemViews] enumerateObjectsUsingBlock:
        ^(UIControl *ctl, NSUInteger idx, BOOL *stop) {
            [ctl setSelected:(selectedIndex == idx)];
        }];

        if(animated) {
            [self setAlpha:0];
            [UIView animateWithDuration:0.2 delay:0.0 options:0 animations:^{
                [self setAlpha:1];
            } completion:^(BOOL finished) {

            }];
        }
    } else {
        if(animated) {
            [UIView animateWithDuration:0.2
                                  delay:0.0
                                options:0
                             animations:^{
                                 [self setAlpha:0];
                             } completion:^(BOOL finished) {
                                 if(finished)
                                     [self setHidden:YES];
                             }];
        } else {
            [self setHidden:YES];
        }
    }
}

This method will ask the delegate for all of the UITabBarItems it needs to display, and shuttles those off to its own setItems: method. That method creates a UIControl for each item:

- (void)setItems:(NSArray *)items
{
    [[self itemViews] makeObjectsPerformSelector:@selector(removeFromSuperview)];

    _items = items;

    NSMutableArray *itemViews = [NSMutableArray array];
    for(UITabBarItem *i in _items) {
        UIControl *ctl = [self controlForItem:i];
        [itemViews addObject:ctl];
        [self addSubview:ctl];
    }
    _itemViews = [itemViews copy];
}

By using UIControl here, we buy ourselves the ability to hook up target-action pairs. (UIControls also have a selected state and can display a different image based on the selection state, so we have that going for us, too.) When one of these controls is tapped, the STKMenuView receives this message, which it then forwards on to its delegate.

- (void)itemViewTapped:(id)sender
{
    int idx = (int)[[self itemViews] indexOfObject:sender];
    [[self delegate] menuView:self didSelectItemAtIndex:idx];
}

We can now see the full process emerge.

MenuViewFlow

When the user taps three fingers on the screen, the STKMenuView is told to become visible. As part of that process, it asks the menu view controller for the items it should display, lays them out, and ‘selects’ the one that is currently active. When the user taps on one of the controls derived from the tab bar item, the menu view controller is told about that change, triggering the swapping of view controllers.

One other neat piece of information about the STKMenuView: it is also a subclass of UIControl. In its own init method, it establishes a target-action pair for itself:

- (id)init
{
    self = [super initWithFrame:CGRectZero];
    if (self) {

        [self addTarget:self action:@selector(dismiss:) forControlEvents:UIControlEventTouchUpInside];
     ...
    }
    return self;
}        

- (void)dismiss:(id)sender
{
    [self setVisible:NO animated:YES];
}

Thus, if the user taps anywhere on the menu view other than one of the items, the menu view is dismissed.

Finally, there is one other thing we are missing. View controller containers like UINavigationController and UITabBarController allow their children to send them messages. For example, a child of a UINavigationController must send a message to the navigation controller in order to pop itself off the stack or push a view controller onto the stack.

[[self navigationController] pushViewController:newVC animated:YES];

A child of a STKMenuViewController should be able to do the same. By using view controller containers, a child view controller’s parentViewController property is automatically set to its parent. However, if a view controller is contained in a navigation controller, which is contained in a menu view controller, the parent property of that view controller will be the navigation controller.

ParentRelationship

Therefore, it makes sense to add a category to every UIViewController to derive its STKMenuViewControler when sent menuViewController. Add that category declaration to STKMenuViewController.h.

@interface UIViewController (STKMenuAdditions)
@property (nonatomic, readonly) STKMenuViewController *menuController;
@end

Then, in STKMenuViewController.m, you can implement the menuController getter method.

@implementation UIViewController (STKMenuAdditions)
- (STKMenuViewController *)menuController
{
    UIViewController *parent = [self parentViewController];
    while(parent) {
        if([parent isKindOfClass:[STKMenuViewController class]]) {
            return (STKMenuViewController *)parent;
        }
        parent = [parent parentViewController];
    }
    return nil;
}
@end

We can use a category here because the menuController of a UIViewController is a derived value and doesn’t need to be stored in an instance variable. If a view controller is not a child of an STKMenuViewController, this method will return nil. Otherwise, it will find the first parent of type STKMenuViewController and return that.

 


In the next installment, we’ll talk about view controller animated transitions and how to implement them for existing view controller containers. This will help us for the final installation where we add the ability for an STKMenuViewController to have animated transitions.

Joe Conway

Founder at Stable Kernel

Leave a Reply

Your email address will not be published. Required fields are marked *