Accordion View in iOS

Swarnendu De February 5, 2014

We often come across the need for an accordion view while showing items, be it as simple as an expandable list of names to complex ones like names expanding to various no of pictures. A fully customisable Accordion view is here.

mobilePics

You can view the demo here and download the source code from the GitHub for Swift and Objective-C.

In a graphical user interface, an accordion is a vertically stacked list of items (e.g. labels or thumbnails). Each item can be “expanded” or “stretched” to reveal the content associated with that item. There can be zero or more items expanded at a time, depending on the configuration.

Here in this tutorial am showing you how to make a fully customisable accordion view.

Swift ::

To implement Accordion view in swift, you need add MKAccordionView.swift to your project. Then follow steps of “how to use it”. If you are new to iOS development start with “Create a New iOS Project”.

Create a New iOS Project ::

You can download the initial project from dropbox or create a new project along with the tutorial.

If you’re going with the latter, create a new single view project with name “AccordionView”. Tap on AccordionView folder and right click on it. Select “Add files to Accordion View”. Locate your project in window and create a new folder named “View” and add.

Now, right click on “View”. Then click on new file and select cocoa class. Write “MKAccordionView” as class name and click next. Select “View” folder and click on create.

Now delete the existing code in “MKAccordionView” and copy this code and paste it into the file.

Now download these images and drag & drop them into “Supporting Files”.

  1. open.png
  2. close.png
  3. grayBar.png
  4. grayBarSelected.png
  5. lightGrayBarWithBlueStripe.png

How to use it ::

MKAccordionView follows delegation patterns like UITableView. So, if you have worked with UITableView, it is very easy for you to implement it. All you need to do is implement a few delegates and datasource methods of MKAccordionView. When you are going to initialize the MKAccordionView, don’t forget to set delegate and datasource to self. Next add the object to view.

Open ViewController.swift . Now move to bottom of the file and extend MKAccordionViewDelegate as following.

// MARK: - Implemention of MKAccordionViewDelegate method
extension ViewController : MKAccordionViewDelegate {
    
}

Now we will implement three methods from MKAccordionViewDelegate

accordionView(_:heightForRowAtIndexPath:), accordionView(_:heightForHeaderInSection:) and accordionView(_:viewForHeaderInSection:isSectionOpen:)

Each section and row will have 50 pixels height.

// MARK: - Implemention of MKAccordionViewDelegate method
extension ViewController : MKAccordionViewDelegate {
    
    func accordionView(accordionView: MKAccordionView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return 50
    }
    
    func accordionView(accordionView: MKAccordionView, heightForHeaderInSection section: Int) -> CGFloat {
        return 50
    }

}

To represent the section we will send a custom view through delegate method. Whenever section will be clicked accordionView(_:viewForHeaderInSection:isSectionOpen:) will be called. Based on isSectionOpen bool we will change backgroundImage of section to indicate if it is opened or closed. Write this within the extension ViewController : MKAccordionViewDelegate

func accordionView(accordionView: MKAccordionView, viewForHeaderInSection section: Int, isSectionOpen sectionOpen: Bool) -> UIView? {
     
        var view : UIView! = UIView(frame: CGRectMake(0, 0, CGRectGetWidth(accordionView.bounds), 50))

        // Background Image
        var bgImageView : UIImageView = UIImageView(frame: view.bounds)
        bgImageView.image = UIImage(named: ( sectionOpen ? "grayBarSelected" : "grayBar"))!
        view.addSubview(bgImageView)
        
        // Arrow Image
        var arrowImageView : UIImageView = UIImageView(frame: CGRectMake(15, 15, 20, 20))
        arrowImageView.image = UIImage(named: ( sectionOpen ? "close" : "open"))!
        view.addSubview(arrowImageView)
        
        
        // Title Label
        var titleLabel : UILabel = UILabel(frame: CGRectMake(50, 0, CGRectGetWidth(view.bounds) - 120, CGRectGetHeight(view.bounds)))
        titleLabel.text = "Process no: (section)"
        titleLabel.textColor = UIColor.whiteColor()
        view.addSubview(titleLabel)
        
        return view
        
    }

Now, we will extend MKAccordionViewDatasource and implement

// MARK: - Implemention of MKAccordionViewDatasource method
extension ViewController : MKAccordionViewDatasource {
        
}

numberOfSectionsInAccordionView(_:)

accordionView(_: numberOfRowsInSection:)

accordionView(_:cellForRowAtIndexPath:)

You may return any integer value for numberOfSectionsInAccordionView(_:) and accordionView(_: numberOfRowsInSection:) . Here the return value is set to 5.

// MARK: - Implemention of MKAccordionViewDatasource method
extension ViewController : MKAccordionViewDatasource {
    
    func numberOfSectionsInAccordionView(accordionView: MKAccordionView) -> Int {
        return 5 
    }
    
    func accordionView(accordionView: MKAccordionView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }    
}

In accordionView(_:cellForRowAtIndexPath:) we have to return a UITableViewCell as we do for UITableView.

func accordionView(accordionView: MKAccordionView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell : UITableViewCell? = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: nil)
        //cell?.imageView = UIImageView(image: UIImage(named: "lightGrayBarWithBluestripe"))
        
        // Background view
        var bgView : UIView? = UIView(frame: CGRectMake(0, 0, CGRectGetWidth(accordionView.bounds), 50))
        var bgImageView : UIImageView! = UIImageView(image: UIImage(named: "lightGrayBarWithBluestripe"))
        bgImageView.frame = (bgView?.bounds)!
        bgImageView.contentMode = UIViewContentMode.ScaleToFill
        bgView?.addSubview(bgImageView)
        cell?.backgroundView = bgView
        
        // You can assign cell.selectedBackgroundView also for selected mode
        
        cell?.textLabel?.text = "          subProcess no: (indexPath.row)"
        return cell!
    }

Now, we are almost done. It’s time to initialize MKAccordianView and add it to view. We will write the following snippets in viewDidLoad.

var accordionView : MKAccordionView = MKAccordionView(frame: CGRectMake(0, 22, CGRectGetWidth(view.bounds), CGRectGetHeight(view.bounds)));
accordionView.delegate = self;
accordionView.dataSource = self;
view.addSubview(accordionView);

Now run (ctrl + R) the project.

Other useful methods ::

MKAccordionView works as UITableView does. So, there are other several delegate and datasource methods i.e.

Delegate Methods ::

accordionView(_:heightForRowAtIndexPath indexPath: )

accordionView(_:heightForHeaderInSection section:)

accordionView(_: heightForFooterInSection section:)

accordionView(_: shouldHighlightRowAtIndexPath indexPath:)

accordionView(_: didHighlightRowAtIndexPath indexPath:)

accordionView(_:didUnhighlightRowAtIndexPath indexPath:)

accordionView(_: willSelectRowAtIndexPath indexPath:)

accordionView(_:willDeselectRowAtIndexPath indexPath:)

accordionView(_:didSelectRowAtIndexPath indexPath:)

accordionView(_:didDeselectRowAtIndexPath indexPath:)

accordionView(_:viewForHeaderInSection section:isSectionOpen:)

accordionView(_:viewForFooterInSection section:isSectionOpen:)

Datasource Methods ::

accordionView(_:numberOfRowsInSection section:)

accordionView(_:cellForRowAtIndexPath indexPath:)

numberOfSectionsInAccordionView(_:)

accordionView(_:titleForHeaderInSection section:)

accordionView(_:titleForFooterInSection section:)

Objective C ::

Go through the initial set up like:
1. create a new project and save it
2. open stroyboard and insert a scrollview in the main view, such that it completely covers it.

3. open Assistant editor and draw an outlet of the scroll view as scrollViewForAccordi0n in the viewController.m file and synthesize it. it would look like :

@interface AR_ViewController ()
@property (weak, nonatomic) IBOutlet UIScrollView *scrollViewForAccordion;
@end

@implementation AR_ViewController
@synthesize scrollViewForAccordion;

4. Now create some extra properties and synthesize it. We would require all of them in due course of tutorial.

a. a BOOL (boolean ) property named isExpanded

b. a NSUInteger type property named tag Of View Expanded and synthesize both of them

5. Define a Macro Yvalueinitial with value 0 (zero)

Finally you code would look somewhat like this:

#define Yvalueinitial 0
@interface AR_ViewController ()
@property (weak, nonatomic) IBOutlet UIScrollView *scrollViewForAccordion;

@property (nonatomic) BOOL isExpanded;
@property (nonatomic) NSUInteger tagOfViewExpanded;

@end

@implementation AR_ViewController
@synthesize scrollViewForAccordion;
@synthesize isExpanded;
@synthesize tagOfViewExpanded;

In order to give support to different Devices like 3.5 inch iPhone or 4 inch iPhone we need to set the size of the scrollView accordingly . So simply use this code

-(void) settingDifferentDevices{
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    if (screenBounds.size.height == 480){
        NSLog(@"its 3.5 inch");
        scrollViewForAccordion.frame  = CGRectMake(scrollViewForAccordion.frame.origin.x, scrollViewForAccordion.frame.origin.y, scrollViewForAccordion.frame.size.width, scrollViewForAccordion.frame.size.height-88);
     }
    else{
        NSLog(@"its 4 inch");
    }
}

and call this method from the viewDidLoad using this code.

 [self settingDifferentDevices];

You may simply skip this part in case you are making it only for a specific device.

Design

Now for creating the accordion view we need to create one list View to show the outer list, another method to draw the inner list view dynamically and pushing the list view below downwards. We would also need a method to bring up the views . All this will be controlled on click of the button, so there will be one button buttonHandler method to control the actions and maintain the animation of accordion.

Code

1. Paste this Code for setting the outer table:

-(void)setTableOuter:(NSUInteger)noOfOuterRows
{
    int yValue=Yvalueinitial;
    for (UIView *views in scrollViewForAccordion.subviews)
    {
        [views removeFromSuperview];
    }
    for (int i=1 ; i<= noOfOuterRows; i++)
    {
        UIView *viewForOutertable = [[UIView alloc]initWithFrame:CGRectMake(0, yValue, 320, 44)];
        viewForOutertable.tag = i;
        UILabel *labelOnOuter = [[UILabel alloc]initWithFrame:CGRectMake(60, 7, 250, 30)];
        labelOnOuter.text = [NSString stringWithFormat:@"Process no: %i", i];
        labelOnOuter.textColor = [UIColor whiteColor];

        UIButton *buttonToEnlarge = [[UIButton alloc]initWithFrame:CGRectMake(20, 7, 30 , 30)];
        buttonToEnlarge.tag = i;
        [buttonToEnlarge setImage:[UIImage imageNamed:@"open.png"] forState:UIControlStateNormal];

        [buttonToEnlarge setImage:[UIImage imageNamed:@"close.png"] forState:UIControlStateSelected];
        [buttonToEnlarge addTarget:self action:@selector(expandButtonTapped:) forControlEvents: UIControlEventTouchUpInside];
        UIImageView *imageViewFOrOuterTable = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 44)];
        imageViewFOrOuterTable.image = [UIImage imageNamed:@"gray_bar_520X88.png"];
        [viewForOutertable addSubview:imageViewFOrOuterTable];
        [viewForOutertable addSubview:labelOnOuter];
        [viewForOutertable addSubview:buttonToEnlarge];
        [scrollViewForAccordion addSubview:viewForOutertable];
        yValue = yValue+viewForOutertable.frame.size.height;
    }
    [scrollViewForAccordion setContentSize:CGSizeMake(320, yValue)];
}

2. Paste this code for drawing the inner view and animate it down dynamically.

-(void)setTableInner:(NSUInteger)noOfInnerRows forTag:(NSUInteger)tagForView
{
    float yValue=Yvalueinitial, yValueForSubviews;
    tagOfViewExpanded = tagForView;
    UIView *viewBelowWhichItStarts;
    NSMutableArray *subviewsArray = [scrollViewForAccordion.subviews mutableCopy];
    NSSortDescriptor *aSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"tag" ascending:YES];
    [subviewsArray sortUsingDescriptors:[NSArray arrayWithObject:aSortDescriptor]];

    for (UIView * subviews in subviewsArray)
    {
        if (subviews.tag == 0 )
        {
            [subviews removeFromSuperview];
        }
        if (subviews.tag ==tagForView )
        {
            viewBelowWhichItStarts = subviews;
            yValueForSubviews=viewBelowWhichItStarts.frame.origin.y+viewBelowWhichItStarts.frame.size.height;
        }
    }
    yValue = viewBelowWhichItStarts.frame.origin.y+44;
    for (int i=0 ; i< noOfInnerRows; i++)
    {
        UIView *viewForInnerTable = [[UIView alloc]initWithFrame:CGRectMake(0, yValue, 320, 44)];
        viewForInnerTable.backgroundColor = [UIColor redColor];
        yValue = yValue+viewForInnerTable.frame.size.height;
        UIImageView *imageViewForInnerTable = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 44)];
        imageViewForInnerTable.image = [UIImage imageNamed:@"light_gray_bar_with_blue_stripe.png"];
        [viewForInnerTable addSubview:imageViewForInnerTable];

        UILabel *labelForInner = [[UILabel alloc]initWithFrame:CGRectMake(50, 7, 200, 30)];
        float i = arc4random_uniform(8);
        labelForInner.text= [NSString stringWithFormat:@"subProcess: %i", (int)i];
        [viewForInnerTable addSubview:labelForInner];
        [scrollViewForAccordion addSubview:viewForInnerTable];
    }
    yValueForSubviews = yValue;
    for (UIView * subviews in subviewsArray)
    {
        if (subviews.tag > tagForView)
        {
            [scrollViewForAccordion bringSubviewToFront:subviews];
            [UIView beginAnimations:nil context:NULL];
            [UIView setAnimationDuration:0.3];
            subviews.frame = CGRectMake(0, yValueForSubviews, 320, viewBelowWhichItStarts.frame.size.height);
            [UIView commitAnimations];

            yValueForSubviews = yValueForSubviews + viewBelowWhichItStarts.frame.size.height;
        }
    }
    [scrollViewForAccordion setContentSize:CGSizeMake(320, yValueForSubviews+2)];
}

3. Paste this code for bringing up the inner list and close it.

-(void)bringUpViews:(NSUInteger)tagForView
{
    UIView *viewBelowWhichItStarts;
    float yValueForSubviews;
    for (UIView * subviews in scrollViewForAccordion.subviews)
    {
        if (subviews.tag == tagForView)
        {
            viewBelowWhichItStarts =subviews;
            yValueForSubviews = viewBelowWhichItStarts.frame.origin.y+44;
        }
        if (subviews.tag > tagForView )
        {
            [UIView beginAnimations:nil context:NULL];
            [UIView setAnimationDuration:0.3];
            subviews.frame = CGRectMake(0, yValueForSubviews, 320, viewBelowWhichItStarts.frame.size.height);
            [UIView commitAnimations];
            yValueForSubviews = yValueForSubviews + viewBelowWhichItStarts.frame.size.height;
        }
    }
    [scrollViewForAccordion setContentSize:CGSizeMake(320, yValueForSubviews)];
    yValueForSubviews = viewBelowWhichItStarts.frame.origin.y;
    [scrollViewForAccordion bringSubviewToFront:viewBelowWhichItStarts];
    float yChek = yValueForSubviews;
    // if needed to remove the previosly cretaed views
    for (UIView * subviews in scrollViewForAccordion.subviews)
    {
        if (subviews.tag == 0)
        {
            if ((subviews.frame.origin.y - yChek) == 44)
            {
                [UIView beginAnimations:nil context:NULL];
                [UIView setAnimationDuration:0.3];
                subviews.frame = CGRectMake(0, yValueForSubviews, 320, viewBelowWhichItStarts.frame.size.height);
                [UIView commitAnimations];
                yChek +=44;

            }
        }
    }
}

4. Paste this code for controlling all the actions of button and create animation of accordion successfully.

-(void)bringUpViews:(NSUInteger)tagForView
{
    UIView *viewBelowWhichItStarts;
    float yValueForSubviews;
    for (UIView * subviews in scrollViewForAccordion.subviews)
    {
        if (subviews.tag == tagForView)
        {
            viewBelowWhichItStarts =subviews;
            yValueForSubviews = viewBelowWhichItStarts.frame.origin.y+44;
        }
        if (subviews.tag > tagForView )
        {
            [UIView beginAnimations:nil context:NULL];
            [UIView setAnimationDuration:0.3];
            subviews.frame = CGRectMake(0, yValueForSubviews, 320, viewBelowWhichItStarts.frame.size.height);
            [UIView commitAnimations];
            yValueForSubviews = yValueForSubviews + viewBelowWhichItStarts.frame.size.height;
        }
    }
    [scrollViewForAccordion setContentSize:CGSizeMake(320, yValueForSubviews)];
    yValueForSubviews = viewBelowWhichItStarts.frame.origin.y;
    [scrollViewForAccordion bringSubviewToFront:viewBelowWhichItStarts];
    float yChek = yValueForSubviews;
    // if needed to remove the previosly cretaed views
    for (UIView * subviews in scrollViewForAccordion.subviews)
    {
        if (subviews.tag == 0)
        {
            if ((subviews.frame.origin.y - yChek) == 44)
            {
                [UIView beginAnimations:nil context:NULL];
                [UIView setAnimationDuration:0.3];
                subviews.frame = CGRectMake(0, yValueForSubviews, 320, viewBelowWhichItStarts.frame.size.height);
                [UIView commitAnimations];
                yChek +=44;

            }
        }
    }
}

5. Finally call the method to create outer list view from viewDidLoad (or any where else, according to where you are using it). For demo purpose am calling it with a value 17 which indicates the no. of rows to be drawn in outer list.

[self setTableOuter:17];

N.B: In this demo to show the dynamic behaviour, the inner list is drawn with no of rows equal to the index no of the outer table. Also the colours or images used in this demo can be fully customised. To get the full code used for this demo video click on download.

mobilePics
You can download here and download the source code from GitHub for Swift.