본문 바로가기
모바일개발(Mobile Dev)/IOS개발(ObjectC)

SLIDING MENU DRAWER WITH CUSTOM SEGUES

by 테크한스 2015. 3. 6.
반응형

SLIDING MENU DRAWER WITH CUSTOM SEGUES

This post is not related to 10InARow but to an up and coming project that I’ve been working on that needed a sliding menu drawer. These seem all the rage at the moment and there are a number of open source components available to help you. My problem with these are that you need to maintain a lot of code when you want to add or remove a new menu item. What I wanted was something that I could manage entirely from Interface Builder. So I decided to have a crack at writing my own.

I went through a few permutations but in the end I got pretty much what I wanted. The secret is a neat little custom segue that allows you to decouple the view controllers for content from the menu draw’s view controller itself.

Setup

Let’s kick off by creating a new empty iOS project in Xcode. You’ll need to target at least iOS 6. Now add a storyboard and select it in the Main Interface of Deployment Info section on the General tab in the project properties. Open your new storyboard and drop a View Controller onto the canvas. It should be the initial view controller by default.

Next, open the AppDelegate class and make sure that application:didFnishLauchingWithOptions: just returns YES, like this:

1
2
3
4
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    return YES;
}

Run your app in the simulator and make sure you can see an empty white window.

MenuDrawViewController

The first job is to create a new class that extends UIViewController called MenuDrawViewController. This where all the code is going to go that will manage all your other content based view controllers. It’s key that at no point will this class will import any of your other view controllers. It will only see them as view controllers constructed entirely by the framework from the storyboard.

Go back to your storyboard and select the view controller that you created during the setup. Make this view controller an instance of MenuDrawViewController by setting the class name in it’s Custom Class field.

Custom class

Add a public property to your MenuDrawViewController interface of type UIViewController. This is going to be used to tell the MenuDrawViewController about content to display.

1
2
3
4
5
@interface DDMenuDrawerViewController : UIViewController
 
@property(nonatomic, weak) UIViewController* content;
 
@end

MenuViewController

The MenuViewController is the view controller that will contain your menu UI. In my example I’m using a UITableViewController with static cells but it could easily be any kind of view controller with any kind of layout.

Grab a UITableViewController and drop it onto your storyboard canvas somewhere close to your MenuDrawerViewController.

Create another new class, this time called MenuViewController. If you’re using a table for your menu like me then you’ll need to make it extend UITableViewController.

This class is going to be super simple and super clean. It’s going to have one public property of type MenuDrawerViewController. Go ahead and add this to your interface definition now. It should look something like this:

1
2
3
4
5
6
7
@class DDMenuDrawerViewController;
 
@interface DDMenuViewController : UITableViewController
 
@property(nonatomic, weak) DDMenuDrawerViewController* menuDrawerViewController;
 
@end

In fact, as I’m using static cells in my menu, I gutted the class implementation too. If you don’t do this your static layout won’t be loaded. The only reason we have this class at all is so that custom segue (to be created) has a way back to the MenuDrawViewController instance – this should become clear later.

No go back to the storyboard and set the custom class of the menu view controller to be MenuViewController. Now is also a good time to setup your menu UI just how you want it. My menu is going to have just two options called Text and Image. Selecting Text will show a view controller with some text on it whilst selecting Image will show a view controller with an image on it.

Screen Shot 2013-12-19 at 20.20.05

Embed the MenuViewController

Now we need to tell the MenuDrawerViewController about the MenuViewController. We do this by embedding the MenuViewController into the MenuDrawViewController.

Take a Container View and drop it onto the MenuDrawerViewController. Make sure it fills the entire view. IB tries to be helpful by also adding a new view controller onto the canvas that is automatically connected to the Container View by an embed segue. You can delete this view controller right away as you don’t need it.

Next control click on the Container View, drag over to you MenuViewController and select embed. Click on the segue itself and set it’s Storyboard Embed Segue Identifier to “embedMenu”.

Embed segue

Run your app and you should now see your menu. If you don’t then go back and debug now – fixing it later will be a hassle.

Menu

The next thing to do is tell the MenuViewController about the MenuDrawViewController. We do this in the prepareForSegue:sender method of MenuDrawViewController. You’ll need to create this method yourself.

1
2
3
4
5
6
7
8
9
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if([segue.identifier isEqualToString:@"embedMenu"])
    {
        DDMenuViewController* menuViewController = segue.destinationViewController;
        menuViewController.menuDrawerViewController = self;
        self.menuDrawViewController = menuViewController;
    }
}

Don’t forget you’ll need to import the MenuViewController class. Note how we’re using identifier of our embed segue to trap a reference to the MenuViewController.

Also note how we’re storing a reference to the MenuViewController instance. You’ll need to create a property in the MenuDrawerViewController private interface to store this in. This is used later as a way to display the initial content.

Creating the content view controllers

Now we need to create the content. So go back to the storyboard and drop two UINavigationControllers onto the canvas to the right of the MenuViewController you’ve added previously. I’m not going to into too much detail here as this should be bread and butter stuff. Suffice to say that the root view controllers for each UINavigationController will the content that you’re going to display when selecting each menu item, so you’ll potentially need some content view controllers for every menu option that you have.

Here’s what my storyboard looks like after adding the content.

ContentStoryBoard

See how there’s a large gap between the menu and content view controllers. We’re going to bridge that gap next.

Creating a custom segue

Add a new class to your project that extends UIStoryboardSegue and call it DisplayContentSegue. Now go back to the storyboard and control click one of the menu options and drag to the corresponding UINavigationController that you want to be displayed when that item is selected. You’ll see a menu pop up in which should be listed your custom segue class (DisplaySegue). Choose the custom segue from section labeled Selection Segue.

Click on the segue and set it’s identifier to “displayText” or something that makes sense for the menu item. Do the same for the other menu item and corresponding UINavigationController – don’t forget to set the segue’s identifier with something sensible, like “displayImage”.

Completed storyboard

Now when you tap on the menu item, the framework is going to create a new instance a UINavigationController. It’s then going to embed your content in the root of UINavigationController and finally, it’s going to invoke the custom segue with the constructed UINavigationController set as the destination view controller and menu as the source view controller. All we need to is tell the MenuDrawViewController about the new content and that’s what we’re going to do next.

Go to the DisplaySegue class Override the perform method so it contains the following:

1
2
3
4
5
6
7
8
9
10
11
12
#import "DDMenuViewController.h"
#import "DDMenuDrawerViewController.h"
 
@implementation DDDisplayContentSegue
 
-(void)perform
{
    DDMenuDrawerViewController* menuDrawerViewController = ((DDMenuViewController*)self.sourceViewController).menuDrawerViewController;
    menuDrawerViewController.content = self.destinationViewController;
}
 
@end

This tells the MenuDrawerViewController about the new content that we want to display upon execution of the segue. It’s then just a matter of rearranging the content and animating as required in the MenuDrawerViewController.

Displaying the content

Open up your MenuDrawViewController implementation and override the setter for the content property so it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
-(void)setContent:(UIViewController *)content
{
    if(_content)
    {
        [_content.view removeFromSuperview];
        [_content removeFromParentViewController];
    }
 
    _content = content;
    [self addChildViewController:_content];
    [_content didMoveToParentViewController:self];
    [self.view addSubview:_content.view];
}

So what’s going on here? The first if statement checks for existing content. If it exists then the view is removed from the super view and the content view controller is removed from the parent view controller into which it’s embedded, i.e. the MenuDrawerViewController.

Next the reference to the incoming view controller is saved. We then embed the new view controller into the MenuDrawerViewController and add it’s view.

We can test this now. Modify the viewDidLoad method to invoke one of the custom segues using the identifier you specified earlier. Choose the identifier for the segue that’s connected to the content view controller you want to display initially.

1
2
3
4
5
- (void)viewDidLoad
{
   [super viewDidLoad];
   [self.menuViewController performSegueWithIdentifier:@"displayText" sender:self.menuViewController];
}

Run your code and you should see the content view controller you elected to be displayed by default. If you don’t see this then stop and debug now.

InitialContent

Opening the drawer

The next job is to provide someway to open the menu drawer. The convention for this is to use a button in the navigation bar that looks a bit like a hamburger. Here is the image I’m using for this:

Burger

Add the image to the image assets of your project and apply the image to a UIBarButtonItem that you need to add to the navigation bar of your content views.

Now you need something to hook these buttons up to that will open the menu drawer. Add a new class to your project that is designed just to handle these button events. Call it ButtonHandler and have it extend NSObject.

In the implementation file create a method that will be called when the button is pressed that looks like this:

1
2
3
4
-(IBAction)handleButton:(id)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"notifyButtonPressed" object:self];
}

What this is going to do is post a notification back to the MenuDrawViewController to tell it open (or close) the menu drawer – simple.

Now go back your storyboard and drop an object onto bar at the bottom of one of your content view controllers. Set the custom class of your object to the ButtonHandler class that you just created. Now control click on the hamburger button in the navigation bar and drag down to the ButtonHandler object. From the resulting menu select “handle button” from the Sent Actions section. Repeat this for your other content view controllers.

Button handler

Now we need to get the MenuDrawViewController to listen for the notification so open the MenuDrawerViewController class and add the following line somewhere in the viewDidLoad method.

1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slideDrawer:) name:@"notifyButtonPressed" object:nil];

As we’re good developers we’re going to make sure we tidy up by removing the observer in the dealloc method, like this:

1
2
3
4
-(void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

The observant amongst you (no pun intended) will have noticed that we now have warning. When we registered the MenuDrawViewController as an observer of the “notifyButtonPressed” event we asked it invoke a method that doesn’t yet exist when the event is fired; slideDrawer.

This method is going to open the menu drawer if it’s closed and close it if it’s open.

1
2
3
4
5
6
7
8
9
10
11
-(void)slideDrawer:(id)sender
{
    if(self.content.view.frame.origin.x > 0)
    {
        [self closeDrawer];
    }
    else
    {
        [self openDrawer];
    }
}

Now we need to create the methods that do the heavy lifting of opening and closing the menu drawer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(void)openDrawer
{
    CGRect fm = self.content.view.frame;
    fm.origin.x = 240.0;
 
    [UIView animateWithDuration:0.3 animations:^{
        self.content.view.frame = fm;
    }];
}
 
-(void)closeDrawer
{
    CGRect fm = self.content.view.frame;
    fm.origin.x = 0;
 
    [UIView animateWithDuration:0.3 animations:^{
        self.content.view.frame = fm;
    }];
}

The code should be self explanatory, but the general gist is that we animate the origin of the content view controller’s frame to an x position of 240 when opening and to 0 when closing. I found that 240 is a good number given the size of the hamburger button I’m using. You might need to tweak this to suit.

If you don’t like the magic number (240) you could change the width of the container view in the MenuDrawerViewController that is used to embed the MenuViewController to 240. Then use self.menuDrawViewController.view.frame.size.width to control how much the content view is moved by and not magic number. This means that InterfaceBuilder is defining the amount by which the content viewed and not the code – as it should be!

Now you need to revisit your setContentMethod. You need to tell the new incoming view controller to assume the position of the previous one. You also need to tell the drawer to close once the view controllers have been swapped.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(void)setContent:(UIViewController *)content
{
    if(_content)
    {
        [_content.view removeFromSuperview];
        [_content removeFromParentViewController];
 
        content.view.frame = _content.view.frame;
    }
 
    _content = content;
    [self addChildViewController:_content];
    [_content didMoveToParentViewController:self];
    [self.view addSubview:_content.view];
 
    [self closeDrawer];
}

Testing

You now should be in a position to test your code. When you tap on the button the drawer should open revealing the menu underneath. Tap the button again and the drawer should close. Open the drawer again and this time select a menu option and the corresponding content view controller should be shown.

Drawer open

And there you have it. What’s great about this solution is that once you have the code in place, all you need to do to add a new menu option is:

  1. Add the content view controller for the new option
  2. Add an item to the menu for content view controller
  3. Connect the menu item to the content view controller using the custom segue
  4. Add an object to the content view controller to handle the hamburger button press

The are zero code changes to make!

You can develop it further so that when the view controller is set the in MenuDrawerViewController a UIPanGestureRecognizer is added to the new content view controller.  You could also add a drop shadow to content view controller view so that there is clearer separation between the menu and content. Both of these issues are covered in Tammy Coron’s article on Ray Wenderlich which can be found here:

http://www.raywenderlich.com/32054/how-to-create-a-slide-out-navigation-like-facebook-and-path

Thanks for reading and I hope you’ve found it useful. If you like this post then please do check out our current release in the Apple App Store, 10InARow

Icon-200

25 responses to “Sliding menu drawer with custom segues

  1. Hi ddiostips,

    Cloud you send this dome code to me?I’m a junior iOS developer and want to investigate this demo.If you can give this code to me maybe helpful.

    Thanks a lot!
    Javen

      • Thanks DD, I’m currently stuck here,

        – (void)viewDidLoad
        {
        [super viewDidLoad];
        [self.menuViewController performSegueWithIdentifier:@"displayText" sender:self.menuViewController];
        }

        Where should I put above code? Also self.menuViewController is not right?

      • -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
        {
        if([segue.identifier isEqualToString:@"embedMenu"])
        {
        DDMenuViewController* menuViewController = segue.destinationViewController;
        menuViewController.menuDrawerViewController = self;
        “”””” self.menuDrawViewController “”””””= menuViewController;
        }
        }

        Quoted bit has mistake? Thanks.

    • Thanks very much, but I did find “”””” self.menuDrawViewController “”””””= menuViewController; should be self.menuViewController = menuViewController.

  2. Awesome! It’s great to see these UI features being handled in IB.

    You’ve got some character formatting issues in your code snippets you might wanna fix for confused beginners.

    • Thanks for the feedback :)

      As for the code formatting issues are referring to the html encoded characters? Yeah I’d noticed that before… Thanks, I’ll see what I can do.

    • Yeah that’s odd, it wasn’t like that when I posted it. Anyway I think I’ve fixed it now. Thanks for pointing it out.

  3. It all works for me except one thing. When I run my app the menu remains on top of the initial displayed view. The custom segue is happening and I can see the content peaking through under the status bar, but the menu remains on top.

  4. Calling the selector performSegueWithIdentifier:@”ShowProfile” with a 1 second delay makes it work. So it seems like there is a race condition somewhere.

    • Have you tried the demo project? There wasn’t an issue when I last tested it. I haven’t tried it on ios8 though, there might be an issue there? I’ll try it out.

      • I did try the demo and it behaved correctly, but I do see a flash of the menu before “displayText” was displayed. Why that flash would happen in the demo app but not my app is mysterious. I am running it in the iPhone6 simulator using XCode6.

  5. I just rebuilt the demo in Xcode 6 and it seems to be working fine. I don’t see any flashing at all. Did you update the project settings when prompted by Xcode?

    If you want to share your project perhaps I can take a look?

  6. OK I worked out what it is. IB is arcane and mysterious! Select the “View Container” and open the “Attributes Inspector”. Scroll to the bottom and make sure that all the different aspect ratios are ticked. Alternatively just remove them all so you just have:
    + [*] Installed

  7. Finally thanks for the great pattern. It really is a great way to keep everything together and maintain control.


반응형