This tutorial will teach you how to create a custom Accordion Menu. This animated menu will allow you to collect input from the user in an engaging and streamlined fashion. Read on to learn more!
Tutorial Preview
About the Accordion Menu
The accordion menu's initial position will be at the center of the target view it appears on. When the menu appears, half of it will move towards the top of the target view, while the other half will move towards the bottom of the view, expanding to its full allowed height. During use, both the Y origin point and the height are going to be modified so that the desired effect can take place. The menu itself will consist of a tableview. It will provide us with great flexibility regarding the number of options added to the menu. The tableview is going to exist as a subview on a view and will appear on a target view. The main view of the accordion menu's view controller will work as a cover to the subviews existing at the back, so the user is unable to tap on anything else except for our menu options.
Let's go ahead and bring this idea to life. Here's an image of what the final product will look like.
1. Create the Accordion Menu View Controller
Step 1
The first thing we must do is create a new view controller for our accordion menu. Prior to that, we'll create a new group in the Xcode to keep everything neat and tidy.
In the Project Navigator pane on the left side of the Xcode, Control + Click (right click) on the CustomViewsDemo group. Select the New Group option from the popup menu.
Set Accordion Menu as the name.
Step 2
Let's create the view controller now. Control + Click (right click) on the Accordion Menu group. Select the New File... option from the popup menu.
Select the Objective-C class as the template for the new file and click next.
Use the name AccordionMenuViewController in the class field and make sure that in the Subclass of field the UIViewController value is selected. Don't forget to leave the "With XIB for user interface" option checked.
Finally, click on the create button. Make sure that the Accordion Menu is the selected group, as shown in the next image.
2. Configure the interface
The first thing we need to do with our new view controller is setup the interface in the Interface Builder, which should be pretty simple. Most of the work will be done in code.
Step 1
Click on the AccordionMenuViewController.xib
file to turn the Interface Builder on. Click on the default view and turn the Autolayout feature off so it works on versions of iOS prior to 6.
- Click on the Utilities button at the Xcode toolbar to show the Utilites pane if it is not visible.
- Click on File Inspector.
- Scroll towards the bottom a litte bit and click on the "Use Autolayout" option to turn it off.
Next, go to the Attributes Inspector. In the Simulated Metrics section, set the Size value to None so that it will work on a 3.5" screen as well.
Step 2
Go ahead and add a new view, but make sure that you don't add it as a subview to the default view. Do the following:
- Go to the Simulated Metrics section of the Attributes Inspector and set the Size to None.
- Change the background color to dark grey.
Step 3
Grab a tableview from the Object Library and add it as a subview to the view we added in the previous step. Complete the next configuration in the Attributes Inspector section.
- Set Shows Horizontal Scrollers to off.
- Set Shows Vertical Scrollers to off.
- Set Scrolling Enabled to off.
- Change the background color to clear.
This is what your interface should look like at this point.
3. Set the IBOutlet Properties
Step 1
Next, we're going to create two IBOutlet properties for the extra view and the tableview we added to the project earlier. First, we should make the Assistant Editor appear. Click on the middle button in the Editor section on the Xcode toolbar to reveal it.
Step 2
To connect the view to a new IBOutlet property, go to Show Document Outline > Control + Click (right click) > New Referencing Outlet. Drag and drop into the Assistant Editor.
I named the property viewAccordionMenu and I recommend that you use the same name to avoid any problems while coding. Set the Storage option to Strong instead of the default Weak value.
Step 3
Now let's add an IBOutlet property for the tableview. Just as we did above, create a new property named tblAccordionMenu. Set the Storage option to Strong as well.
Here are the two IBOutlet properties.
@property (strong, nonatomic) IBOutlet UIView *viewAccordionMenu; @property (strong, nonatomic) IBOutlet UITableView *tblAccordionMenu;
4. Do Some Code Level Setup
So far so good! We've created the view controller for the accordion menu, we've setup the interface, and we've created the two required IBOutlet properties for the views we added to the builder. Now it's time to begin writing some code.
Step 1
First of all, we should set our view controller as the delegate and datasource of the tableview we created in the Interface Builder. Our view controller should adopt the respective protocols. Click on the AccordionMenuViewController.h file and right next to the @interface
header add the following.
@interface AccordionMenuViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
Step 2
Click on the AccordionMenuViewController.m
file and implement a simple init
method. Set the view controller as the delegate and datasource of the tableview.
-(id)init{ self = [super init]; if (self) { [_tblAccordionMenu setDelegate:self]; [_tblAccordionMenu setDataSource:self]; } return self; }
To make the menu look better, let's add a border. In the init
, add the highlighted code.
... if (self) { [_tblAccordionMenu setDelegate:self]; [_tblAccordionMenu setDataSource:self]; // Add some border around the tableview. [[_tblAccordionMenu layer] setBorderColor:[UIColor whiteColor].CGColor]; [[_tblAccordionMenu layer] setBorderWidth:2.0]; } ...
Make sure to add this to the top of the file.
#import <QuartzCore/QuartzCore.h>
Set the background color of the default view to light grey with transparency.
... if (self) { ... [self.view setBackgroundColor:[UIColor colorWithRed:0.33 green:0.33 blue:0.33 alpha:0.75]]; } ...
Step 3
It's a good idea to define the row height of the tableview as a constant. Apart from the tableView:heightForRowAtIndexPath
method, we are going to use it in other methods. Right after the #import
commands, add the following.
#define ROW_HEIGHT 40.0 // The height of each option of the accordion menu.
The accordion menu will use animation to appear and disappear. We can set the duration of the animation as a constant.
#define ANIMATION_DURATION 0.25 // The duration of the animation.
From now on, if we want to change the row height or the animation duration we'll do it once without changing the values in each method.
Step 4
We should declare two private variables and an NSArray. The variables regard the accordion menu width and height, meaning the width and height of the parent view of our tableview. The NSArray is the array that will keep the menu options, and it will be the datasource of the tableview.
At the top of the AccordionMenuViewController.m
file add the next lines in the @interface
section.
@interface AccordionMenuViewController (){ CGFloat menuViewWidth; CGFloat menuViewHeight; } @property (nonatomic, strong) NSArray *optionsArray; @end
Make sure you don't forget the curly brackets!
5. Implement the Public Methods
There are at least three public methods that should be implemented in order for the accordion menu to work properly. The first method will be used to set the options of the menu, the second method will make the accordion menu appear, and the third method will make it disappear.
Step 1
First of all, we should declare the methods I mentioned above into the .h file. So, click on the AccordionMenuViewController.h
file and add the following.
-(void)setMenuOptions:(NSArray *)options; -(void)showAccordionMenuInView:(UIView *)targetView; -(void)closeAccordionMenuAnimated:(BOOL)animated;
Step 2
Let's implement the first method. Click on the AccordionMenuViewController.m
file and write or copy/paste the following code.
-(void)setMenuOptions:(NSArray *)options{ NSMutableArray *tempArray = [NSMutableArray arrayWithArray:options]; [tempArray addObject:@"Close"]; _optionsArray = [[NSArray alloc] initWithArray:(NSArray *)tempArray copyItems:YES]; }
Even though this is a really simple method, let me explain it a little bit. I find it easier for the user and the programmer to provide an array with menu options only. You don't need to be concerned about the so-called close menu option. That's really only useful when someone is going to use the accordion menu in more than one case. I use the tempArray
mutable array to put together both the user options and the close option. If you know that the _optionsArray
is not mutable, you'll understand that it cannot accept new objects after creation. I initialize the _optionsArray
array. It's your choice to avoid using this logic or change the title of the close menu option.
Step 3
Let's move on to the next method. It'll concern the way the accordion menu appears on screen. A lot of different steps take place in it, so I'll present and discuss it in parts.
First of all, we need to take the status bar under consideration when the accordion menu is about to appear. This is because we'll use the target view where the menu will appear and its frame as a base to configure our views. Thus, it's really important to handle the status bar correctly. If the status bar is hidden, there's no problem at all. If it's visible, however, an empty space equal to the status bar height will be created when we make our view appear. So, as a first step, we need to check if the status bar is visible and store its height to fix the offset created by this.
-(void)showAccordionMenuInView:(UIView *)targetView{ // STEP 1: THE STATUS BAR OFFSET CGFloat statusBarOffset; if (![[UIApplication sharedApplication] isStatusBarHidden]) { CGSize statusBarSize = [[UIApplication sharedApplication] statusBarFrame].size; if (statusBarSize.width < statusBarSize.height) { statusBarOffset = statusBarSize.width; } else{ statusBarOffset = statusBarSize.height; } } else{ statusBarOffset = 0.0; } ...
This is the approach we followed during the previous tutorial in the custom text input view.
Next, we should specify what the width and height of the target view is depending on the orientation. Depending again on the orientation, we should "tell" whether the offset that should be moved by our default view is on the X or the Y axis.
-(void)showAccordionMenuInView:(UIView *)targetView{ ... CGFloat width, height, offsetX, offsetY; if ([[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationLandscapeLeft || [[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationLandscapeRight) { // If the orientation is landscape then the width // gets the targetView's height value and the height gets // the targetView's width value. width = targetView.frame.size.height; height = targetView.frame.size.width; offsetX = -statusBarOffset; offsetY = 0.0; } else{ // Otherwise the width is width and the height is height. width = targetView.frame.size.width; height = targetView.frame.size.height; offsetX = 0.0; offsetY = -statusBarOffset; } ...
The next step is easy. We'll simply set up the default view, self.view, by setting its frame and the appropriate offset, its alpha value, and finally adding it as a subview to the target view.
-(void)showAccordionMenuInView:(UIView *)targetView{ ... // STEP 3 : SETUP THE SELF.VIEW [self.view setFrame:CGRectMake(targetView.frame.origin.x, targetView.frame.origin.y, width, height)]; [self.view setFrame:CGRectOffset(self.view.frame, offsetX, offsetY)]; [self.view setAlpha:0.0]; [targetView addSubview:self.view]; ...
Now it's time to configure the view that contains the tableview. We have to specify its size here, keeping in mind that it will occupy a portion of the screen. I set its width to 260.0 px, but you can change it according to your desires. Its height will be calculated based on the total options and the height of each row. That means that the height will be equal to the total rows multiplied by the height of each row. In the case of too many options, and a menu height greater than the target view height in either orientation, we should manually shrink the menu and enable scrolling on the tableview. To achieve that, we'll use a temporary variable that will keep the height in each case.
In order to achieve the accordion effect, we need to set its frame twice. At first, we'll give it its normal frame and we'll center it according to the target view center point. Then we'll store the Y origin point and set the frame again. We'll do this by setting the Y origin point to the Y origin point of its center and setting its height to 0.0. When the Y origin point and the height get restored to their original values, we'll have a great accordion effect.
-(void)showAccordionMenuInView:(UIView *)targetView{ ... // STEP 4: SETUP THE MENU VIEW menuViewWidth = 260.0; // The height is the height of each row multiplied by the number // of options. menuViewHeight = ROW_HEIGHT * [_optionsArray count]; // Declare and use a local, temporary variable for the height of the menu view. CGFloat tempMenuHeight; if (menuViewHeight > height) { // If the menuViewHeight as set above is greater than the height of the target view // then set the menu view's height to targetview's height - 50.0. // Also enable scrolling on tableview. tempMenuHeight = height - 50.0; [_tblAccordionMenu setScrollEnabled:YES]; } else{ // Otherwise if the menu view height is not greater than the targetView's height // then the tempMenuHeight simply equals to the menuViewHeight. // The scrolling doesn't have to be enabled. tempMenuHeight = menuViewHeight; [_tblAccordionMenu setScrollEnabled:NO]; } // Set the initial frame of the menu view. Note that we're not // interested in the x and y origin points because they'll be automatically // set right after, so set it to 0.0. [_viewAccordionMenu setFrame:CGRectMake(0.0, 0.0, menuViewWidth, tempMenuHeight)]; // Set its center to the self.view's center. [_viewAccordionMenu setCenter:self.view.center]; // Store temporarily the current y origin point of the menu view. CGFloat yPoint = _viewAccordionMenu.frame.origin.y; // Now set the center.y point as the y origin point of the menu view and its height to 0.0. [_viewAccordionMenu setFrame:CGRectMake(_viewAccordionMenu.frame.origin.x, _viewAccordionMenu.center.y, _viewAccordionMenu.frame.size.width, 0.0)]; // Add the menu view to the targetView view. [targetView addSubview:_viewAccordionMenu]; ...
It's time to animate the menu. There's really nothing special to discuss here. We simply change the alpha value of the self.view and set the final frame to the menu view.
-(void)showAccordionMenuInView:(UIView *)targetView{ ... // STEP 5: ANIMATE [UIView beginAnimations:@"" context:nil]; [UIView setAnimationDuration:ANIMATION_DURATION]; [UIView setAnimationCurve:UIViewAnimationCurveLinear]; [self.view setAlpha:0.5]; // Set the yPoint value as the y origin point of the menu view // and the tempMenuHeight value as its height. [_viewAccordionMenu setFrame:CGRectMake(_viewAccordionMenu.frame.origin.x, yPoint, _viewAccordionMenu.frame.size.width, tempMenuHeight)]; [UIView commitAnimations]; ...
Finally, reload the table data so the menu options appear on the tableview. Note that the method ends here.
-(void)showAccordionMenuInView:(UIView *)targetView{ ... // STEP 6: RELOAD THE TABLEVIEW DATA [_tblAccordionMenu reloadData]; ... }
Step 4
Let's write the next method regarding the menu closing. There's nothing I really need to mention here. I'll only emphasize that it uses a boolean parameter that specifies whether the closing should be animated or not.
-(void)closeAccordionMenuAnimated:(BOOL)animated{ if (animated) { [UIView beginAnimations:@"" context:nil]; [UIView setAnimationDuration:ANIMATION_DURATION]; [UIView setAnimationCurve:UIViewAnimationCurveLinear]; [self.view setAlpha:0.0]; [_viewAccordionMenu setFrame:CGRectMake(_viewAccordionMenu.frame.origin.x, _viewAccordionMenu.center.y, _viewAccordionMenu.frame.size.width, 0.0)]; [UIView commitAnimations]; [_viewAccordionMenu performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:ANIMATION_DURATION + 0.5]; [self.view performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:ANIMATION_DURATION + 0.5]; } else{ [_viewAccordionMenu removeFromSuperview]; [self.view removeFromSuperview]; } }
6. Consider Orientation Changes
We've succeeded in making our menu appear correctly when called. What happens when the user rotates the device, though? Nothing! This is because we haven't written anything concerning this, so let's do it now. We'll implement the viewWillLayoutSubviews
method, which gets called every time the orientation is changed. You can read more about it at Apple's developer website.
Here's the short version of what we're going to do. First, we'll set the menu view's frame, based on the menuViewWidth
and the menuViewHeight
variables we set earlier. We'll center it according to the self.view center point. Next, depending on the device orientation, we'll calculate the height of the superview. Finally, we'll check whether the view's height is greater than the superview's height. If that's true, we'll manually shrink it and make scrolling enabled, just like we did in the -(void)showAccordionMenuInView:(UIView *)targetView
method. Otherwise, we'll simply turn scrolling off.
-(void)viewWillLayoutSubviews{ // Get the current orientation. UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; // Set the menu view frame and center it. [_viewAccordionMenu setFrame:CGRectMake(_viewAccordionMenu.frame.origin.x, _viewAccordionMenu.frame.origin.y, menuViewWidth, menuViewHeight)]; [_viewAccordionMenu setCenter:self.view.center]; // Get the superview's height. In landscape mode the height is the width. CGFloat height; if (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) { height = self.view.superview.frame.size.width; } else{ height = self.view.superview.frame.size.height; } // Check if the menu view's height is greater than the superview's height. if (_viewAccordionMenu.frame.size.height > height) { // If that's true then set the menu view's frame again by setting its height // to superview's height minus 50.0. [_viewAccordionMenu setFrame:CGRectMake(_viewAccordionMenu.frame.origin.x, _viewAccordionMenu.frame.origin.y, menuViewWidth, height - 50.0)]; // Center again. [_viewAccordionMenu setCenter:self.view.center]; // Also allow scrolling. [_tblAccordionMenu setScrollEnabled:YES]; } else{ // In that case the menu view's height is not greater than the superview's height // so set scrolling to NO. [_tblAccordionMenu setScrollEnabled:NO]; } }
7. Implement the Tableview Methods
Step 1
Here are the minimum required methods needed to make the tableview work. Note that in the -(UITableViewCell *)tableView:cellForRowAtIndexPath:
method, we'll check to see whether the current row is the last one or not.
-(int)numberOfSectionsInTableView:(UITableView *)tableView{ return 1; } -(int)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return [_optionsArray count]; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } if ([indexPath row] < [_optionsArray count] - 1) { [[cell contentView] setBackgroundColor:[UIColor colorWithRed:204.0/255.0 green:204.0/255.0 blue:204.0/255.0 alpha:1.0]]; [[cell textLabel] setTextColor:[UIColor blackColor]]; [[cell textLabel] setShadowColor:[UIColor whiteColor]]; [[cell textLabel] setShadowOffset:CGSizeMake(1.0, 1.0)]; } else{ [[cell contentView] setBackgroundColor:[UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:104.0/255.0 alpha:1.0]]; [[cell textLabel] setTextColor:[UIColor whiteColor]]; [[cell textLabel] setShadowColor:[UIColor blackColor]]; [[cell textLabel] setShadowOffset:CGSizeMake(1.0, 1.0)]; } [[cell textLabel] setFont:[UIFont fontWithName:@"Georgia" size:17.0]]; [cell setSelectionStyle:UITableViewCellSelectionStyleGray]; CGRect rect = CGRectMake(0.0, 0.0, self.view.bounds.size.width, [self tableView:tableView heightForRowAtIndexPath:indexPath]); [[cell textLabel] setFrame:rect]; [[cell textLabel] setTextAlignment:NSTextAlignmentCenter]; [[cell textLabel] setText:[_optionsArray objectAtIndex:[indexPath row]]]; return cell; } -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return ROW_HEIGHT; }
Step 2
We also need to handle the tapping on the rows of the tableview. Note in the following method that we remove the selection from the tapped row.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ [[tableView cellForRowAtIndexPath:indexPath] setSelected:NO]; }
We'll get back to this method pretty soon.
8. Protocol Definition
When the user taps on a row, or in other words a menu option, we want the caller view controller to be notified about the selected choice.
Step 1
Click on the AccordionMenuViewController.h
file and write the following code, right before the @interface
header.
@protocol AccordionMenuViewControllerDelegate -(void)userSelectedOptionWithIndex:(NSUInteger)index; @end
Step 2
Now, declare a delegate property.
@interface AccordionMenuViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> @property (nonatomic, strong) id<AccordionMenuViewControllerDelegate> delegate; @property (strong, nonatomic) IBOutlet UIView *viewAccordionMenu; @property (strong, nonatomic) IBOutlet UITableView *tblAccordionMenu; -(void)setMenuOptions:(NSArray *)options; -(void)showAccordionMenuInView:(UIView *)targetView; -(void)closeAccordionMenuAnimated:(BOOL)animated; @end
Step 3
When should the userSelectedOptionWithIndex
delegate method be used? Every time a menu option gets selected. Go back in the -(void)tableView:didSelectRowAtIndexPath:
method and add the following lines.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ [[tableView cellForRowAtIndexPath:indexPath] setSelected:NO]; [self.delegate userSelectedOptionWithIndex:[indexPath row]]; }
9. The Accordion Menu in Action
The accordion menu is now ready. It's time to see it in action. Make the required preparations in the ViewController class.
Step 1
First of all, the ViewController class should adopt the AccordionMenuViewControllerDelegate
protocol. Open the ViewController.h
file, import the AccordionMenuViewController.h
class and add the protocol in the @interface
header.
#import <UIKit/UIKit.h> #import "CustomTextInputViewController.h" #import "AccordionMenuViewController.h" @interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, CustomTextInputViewControllerDelegate, AccordionMenuViewControllerDelegate>
Step 2
Open the ViewController.m
file and go to the private part of the @interface
at the top of the file. In there, add an NSArray that will be used for the options we'll provide the accordion menu with, as well as an AccordionMenuViewController
object.
@interface ViewController (){ ... @property (nonatomic, strong) NSArray *menuOptionsArray; @property (nonatomic, strong) AccordionMenuViewController *accordionMenu; @end
Step 3
Inside the viewDidLoad
method, we need to initialize both the array and the object we declared in the previous step.
- (void)viewDidLoad { [super viewDidLoad]; ... // Set the options that will appear in the accordion menu. _menuOptionsArray = [[NSArray alloc] initWithObjects:@"Edit", @"Delete", @"Option 1", @"Option 2", @"Option 3", @"Option 4", @"Option 5", nil]; // Init the accordion menu view controller. _accordionMenu = [[AccordionMenuViewController alloc] init]; // Set self as its delegate. [_accordionMenu setDelegate:self]; // Set the menu options. [_accordionMenu setMenuOptions:_menuOptionsArray]; }
We'll only use two options from the list above. For the time being, the rest are for demonstration purposes only.
Step 4
Go to the -(void)tableView:didSelectRowAtIndexPath:
method and add the following.
// Make the accordion menu appear. [_accordionMenu showAccordionMenuInView:self.view];
If you're continuing the project from the previous tutorial, delete or comment out any existing content.
Step 5
Finally, we just need to implement the -(void)userSelectedOptionWithIndex:(NSUInteger)index
delegate method. This is where any actions are taken when the user taps on the menu options.
-(void)userSelectedOptionWithIndex:(NSUInteger)index{ if (index != [_menuOptionsArray count]) { NSIndexPath *indexPath = [_table indexPathForSelectedRow]; switch (index) { case 0: [_textInput showCustomTextInputViewInView:self.view withText:[_sampleDataArray objectAtIndex:[indexPath row]] andWithTitle:@"Edit item"]; // Set the isEditingItem flag value to YES, indicating that // we are editing an item. isEditingItem = YES; break; case 1: [_sampleDataArray removeObjectAtIndex:[indexPath row]]; [_table reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic]; break; default: break; } } [_accordionMenu closeAccordionMenuAnimated:YES]; }
We're finished! Run the app on the simulator or on a device and check out the menu. Play around with it, and don't hesitate to improve it or change it according to your needs.
Conclusion
Providing users with menu options that are different than the usual predefined ones is always a great challenge for a programmer. As you now know, we can achieve a nice result without using any difficult or extreme techniques. The accordion menu presented in this tutorial is a pretty nice way to display options to the user and, most importantly, it is reusable. I hope it will become a useful tool to everyone who uses it!
Comments