Expandable/Collapsible Table For iOS

While developing one of my iPhone apps, I came across a requirement in which nesting of table or you can say collapsible/expandable table was required. I thought there would be so many resources on the web that could help me to carry out the task, as it doesn’t sound that much difficult. But I was a bit surprised as I could not find anything else, apart from the apple sample code. So I decided to write a blog on this topic to help others as well.

Here are the screenshots that show the end product of this tutorial:

There are few things that I will like to tell you before we start developing the app.

  1. We have a plist file that will act as a data source.
  2. The approach I used to carry out the task is MVC (Modal View Controller) i.e. all the data related part is managed via modal classes.
  3. The basic idea behind the app is initially the table will be having empty sections. Rows will be inserted and deleted into/from these sections based on the user interaction.

So let’s start building the app

  1. Open XCode and choose the “Empty Application” template to start the project. Name it as you want. I named it “Custom Table”. I prefer it to be an “iPad” app. The reason for so is larger view area.
  2. Now we need to add a new file to the project i.e. subclass of the UITableViewController. You can do that from file menu or by pressing “Cmd+N” shortcut. I named it as “RootViewController” and made it subclass of UITableViewController.

  3. In “application:didFinishLaunchingWithOptions:” method add the following lines. They are assigning our rootviewcontroller’s instance as the rootViewController of the window instance.

    – (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

    {

        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

        // Override point for customization after application launch.

        RootViewController *viewController = [[RootViewController alloc] initWithNibName:@”RootViewController” bundle:nil];

        self.window.rootViewController = viewController;

    [obj-c] [/obj-c]

        self.window.backgroundColor = [UIColor whiteColor];

        [self.window makeKeyAndVisible];

        return YES;

    }

  4. Now you can run the app. There should not be any error. Although some warnings can be there. The simulator should be then showing an empty table.
  5. Now add a property list file to the project. Fill it with data as shown. The file contains various dictionaries where each item contains two attributes one is the name of the category and the other its sub categories.
  6. Add a new file (.h/.m pair) (NSObject Subclass) to the project. This will be our modal class. I named it “Category”. It includes one string object and one array object. So as to handle data in the plist file. A remote server can also act as data source. Each object of this class will correspond to the each dictionary of the plist file. Similarly add synthesis statements in implementation file.

    #import <Foundation/Foundation.h>

    @interface Category : NSObject

    @property (nonatomic, retain) NSString *name;

    @property (nonatomic, retain) NSArray *list;

    @end

    #import “Category.h”

    @implementation Category

    @synthesize name;

    @synthesize list;

    @end

  7. Now we need to create UIView subclass that will used as section view in our table..
  •  I put these attributes in it. 
    • sectionTitle”, a Label, as the label/title for the section. We will show category name as the title.
    • discButton”, a UIButton, in order  to show the current status of the section (Open /Close). This will be a carat image.
    • section”, a NSInteger denoting the section no. A number will be assigned to each section we’ll create.
    • delegate” that will hold the reference of the delegate so that it could inform it, about the user interaction when needed.
      @property (nonatomic, retain) UILabel *sectionTitle;

      @property (nonatomic, retain) UIButton *discButton;

      @property (nonatomic, assign) NSInteger section;

      @property (nonatomic, weak) id<SectionView> delegate;

  • The methods are:
    1. The “initWithFrame:WithTitle:Section:delegate:” method will initiate the section view with the required details passed to it as parameters.
    2. And the other two buttons to handle the user interaction.
  • The delegate methods:
    1. “sectionOpened:” method will tell the delegate to insert rows in the section
    2. “sectionClosed:” method  will tell the delegate to delete rows from the section
    3. The methods are declared as given below.
       – (id)initWithFrame:(CGRect)frame WithTitle: (NSString *) title Section:(NSInteger)sectionNumber delegate: (id <SectionView>) delegate;

      – (void) discButtonPressed : (id) sender;

      – (void) toggleButtonPressed : (BOOL) flag;

      // delegate methods

      – (void) sectionClosed : (NSInteger) section;

      – (void) sectionOpened : (NSInteger) section;

  • The init method in the respective implementation file.
      • first of all add gesture recogniser to the view
      • Then assigning the section number and delegate to the view.
      • Then preparing frame for label and adding the label to the view.
      • Similarly assembling the button.
      • And then applying gradient to the layer for better ui.

        – (id)initWithFrame:(CGRect)frame WithTitle: (NSString *) title Section:(NSInteger)sectionNumber delegate: (id <SectionView>) Delegate

        {

            self = [super initWithFrame:frame];

            if (self) {

                UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(discButtonPressed:)];

                [self addGestureRecognizer:tapGesture];

                self.userInteractionEnabled = YES;

                self.section = sectionNumber;

                self.delegate = Delegate;

                CGRect LabelFrame = self.bounds;

                LabelFrame.size.width -= 50;

                CGRectInset(LabelFrame, 0.0, 5.0);

                UILabel *label = [[UILabel alloc] initWithFrame:LabelFrame];

                label.text = title;

                label.font = [UIFont boldSystemFontOfSize:16.0];

                label.backgroundColor = [UIColor clearColor];

                label.textColor = [UIColor blackColor];

                label.textAlignment = UITextAlignmentLeft;

                [self addSubview:label];

                self.sectionTitle = label;

                CGRect buttonFrame = CGRectMake(LabelFrame.size.width, 0, 50, LabelFrame.size.height);

                UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];

                button.frame = buttonFrame;

                [button setImage:[UIImage imageNamed:@”carat.png”] forState:UIControlStateNormal];

                [button setImage:[UIImage imageNamed:@”carat-open.png”] forState:UIControlStateSelected];

                [button addTarget:self action:@selector(discButtonPressed:) forControlEvents:UIControlEventTouchUpInside];

                [self addSubview:button];

                self.discButton = button;

                static NSMutableArray *colors = nil;

                if (colors == nil) {

                    colors = [[NSMutableArray alloc] initWithCapacity:3];

                    UIColor *color = nil;

                    color = [UIColor colorWithRed:0.61 green:0.74 blue:0.78 alpha:1];

                    [colors addObject:(id)[color CGColor]];

                    color = [UIColor colorWithRed:0.50 green:0.54 blue:0.58 alpha:1];

                    [colors addObject:(id)[color CGColor]];

                    color = [UIColor colorWithRed:0.15 green:0.20 blue:0.23 alpha:1];

                    [colors addObject:(id)[color CGColor]];

                }

                [(CAGradientLayer *)self.layer setColors:colors];

                [(CAGradientLayer *)self.layer setLocations:[NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], [NSNumber numberWithFloat:0.48], [NSNumber numberWithFloat:1.0], nil]];

            }

            return self;

        }

Note:Make sure to add the “QuartzCore” framework and the following method in order to get the gradient part working.

+ (Class)layerClass {

    return [CAGradientLayer class];

}

9.  The toggle method is first of all toggling the button’s state. The value of the flag passed to it will help to determine whether we need to inform the delegate or not. And in former case, informing the delegate whether to open the section or to close it, which is decided upon the button state (selected or not) That’s it from section view.

– (void) toggleButtonPressed : (BOOL) flag

{

    self.discButton.selected = !self.discButton.selected;

    if(flag)

    {

        if (self.discButton.selected)

        {

            if ([self.delegate respondsToSelector:@selector(sectionOpened:)])

            {

                [self.delegate sectionOpened:self.section];

            }

        } else

        {

            if ([self.delegate respondsToSelector:@selector(sectionClosed:)])

            {

                [self.delegate sectionClosed:self.section];

            }

        }

    }

}

10. Now we will create our section view controller called “SectionInfo”. That will basically configure our section view and carry out the other required functionality.

The class interface includes these variables:

    1. A BOOL property “open” that will used to track if the section is opened or not.
    2. An instance of the model class.
    3. An instance of the section view
    4. One array to hold the no of rows in terms of their heights.

      @property (assign) BOOL open;

      @property (strong) Category *category;

      @property (strong) SectionView *sectionView;

      @property (nonatomic,strong,readonly) NSMutableArray *rowHeights;

       And various insertion and deletion function upon the array.

– (NSUInteger)countOfRowHeights;

– (id)objectInRowHeightsAtIndex:(NSUInteger)idx;

– (void)insertObject:(id)anObject inRowHeightsAtIndex:(NSUInteger)idx;

– (void)removeObjectFromRowHeightsAtIndex:(NSUInteger)idx;

– (void)replaceObjectInRowHeightsAtIndex:(NSUInteger)idx withObject:(id)anObject;

– (void)insertRowHeights:(NSArray *)rowHeightArray atIndexes:(NSIndexSet *)indexes;

– (void)removeRowHeightsAtIndexes:(NSIndexSet *)indexes;

– (void)replaceRowHeightsAtIndexes:(NSIndexSet *)indexes withRowHeights:(NSArray *)rowHeightArray;

 These functions are defined like this. There are so simple and I don’t think that I need to explain them.

– init {

self = [super init];

if (self) {

rowHeights = [[NSMutableArray alloc] init];

}

return self;

}

– (NSUInteger)countOfRowHeights {

return [rowHeights count];

}

– (id)objectInRowHeightsAtIndex:(NSUInteger)idx {

return [rowHeights objectAtIndex:idx];

}

– (void)insertObject:(id)anObject inRowHeightsAtIndex:(NSUInteger)idx {

[rowHeights insertObject:anObject atIndex:idx];

}

– (void)insertRowHeights:(NSArray *)rowHeightArray atIndexes:(NSIndexSet *)indexes {

[rowHeights insertObjects:rowHeightArray atIndexes:indexes];

}

– (void)removeObjectFromRowHeightsAtIndex:(NSUInteger)idx {

[rowHeights removeObjectAtIndex:idx];

}

– (void)removeRowHeightsAtIndexes:(NSIndexSet *)indexes {

[rowHeights removeObjectsAtIndexes:indexes];

}

– (void)replaceObjectInRowHeightsAtIndex:(NSUInteger)idx withObject:(id)anObject {

[rowHeights replaceObjectAtIndex:idx withObject:anObject];

}

– (void)replaceRowHeightsAtIndexes:(NSIndexSet *)indexes withRowHeights:(NSArray *)rowHeightArray {

[rowHeights replaceObjectsAtIndexes:indexes withObjects:rowHeightArray];

}

11.  The “RootViewController” implementation file privately contains an array named “categoryList” that will contain the list of all the categories. To set the array we have the following function that will fetch the data from the plist file and populate our array. I called the function in the “viewDidLoad” method.

– (void) setCategoryArray

{

    NSURL *url = [[NSBundle mainBundle] URLForResource:@”CategoryList” withExtension:@”plist”];

    NSArray *mainArray = [[NSArray alloc] initWithContentsOfURL:url];

    NSMutableArray *categoryArray = [[NSMutableArray alloc] initWithCapacity:[mainArray count]];

    for (NSDictionary *dictionary in mainArray) {

        Category *category = [[Category alloc] init];

        category.name = [dictionary objectForKey:@”name”];

        category.list = [dictionary objectForKey:@”list”];

        [categoryArray addObject:category];

    }

    self.categoryList = categoryArray;

}

12.  It also has an array of “sectionInfo” class objects.  And one variable “openSectionIndex” to track of the currently opened section. It is currently initialized to NSNotFound that means by default no section is open.

13.  In viewWillAppear method we are populating our array of “sectionInfo”. The sectionInfoArray is only initialized if previously it doesn’t exist.

– (void)viewWillAppear:(BOOL)animated

{

    [super viewWillAppear:animated];

    if ((self.sectionInfoArray == nil)|| ([self.sectionInfoArray count] != [self numberOfSectionsInTableView:self.tableView])) {

        NSMutableArray *array = [[NSMutableArray alloc] init];

        for (Category *cat in self.categoryList) {

            SectionInfo *section = [[SectionInfo alloc] init];

            section.category = cat;

            section.open = NO;

            NSNumber *defaultHeight = [NSNumber numberWithInt:44];

            NSInteger count = [[section.category list] count];

            for (NSInteger i= 0; i<count; i++) {

                [section insertObject:defaultHeight inRowHeightsAtIndex:i];

            }

            [array addObject:section];

        }

        self.sectionInfoArray = array;

    }

}

14.  Now in the tableview delegate methods we are returning the no of sections as same as no of categories we have. Also, for no. of rows in each section we are calculating the no of rows in category. And returning the number on the basis of the whether the section is opened or not.

– (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

{

    return [self.categoryList count];

}

– (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

{

    SectionInfo *array = [self.sectionInfoArray objectAtIndex:section];

    NSInteger rows = [[array.category list] count];

    return (array.open) ? rows : 0;

}

15.  In table view data source methods, we are setting the title of the cell as name of the sub-category. Similarly returning the height of the rows in method. In “table:viewForHeaderInSection” method we are initializing the section views if they already don’t exist.

– (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];

    }

    Category *category = (Category *)[self.categoryList objectAtIndex:indexPath.section];

    cell.textLabel.text = [category.list objectAtIndex:indexPath.row];

    return cell;

}

-(CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {

    SectionInfo *array = [self.sectionInfoArray objectAtIndex:indexPath.section];

    return [[array objectInRowHeightsAtIndex:indexPath.row] floatValue];

}

– (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section

{

    SectionInfo *array  = [self.sectionInfoArray objectAtIndex:section];

    if (!array.sectionView)

    {

        NSString *title = array.category.name;

        array.sectionView = [[SectionView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, 45) WithTitle:title Section:section delegate:self];

    }

    return array.sectionView;

}

16.  And in the last we just need to define the section view delegate methods. The first one is “sectionOpened:” method. The method is called if the user tries to open a section that is already not opened. In this we first count the no of rows in section then creating the indexpath array for those rows.

17.  Similarly if previously some section is opened then we also need to close it as well. Thus creating an array of indexpath of the rows to be deleted ,also.

Note: Here we also called the “toggleButtonPressed” method with  false flag. That means we want the button to be updated without informing the delegate.

18.  Then after setting the animation and we update the table using the beginUpdates and endUpdates methods. Also setting the value of open to currently opened object.

– (void) sectionOpened : (NSInteger) section

{

    SectionInfo *array = [self.sectionInfoArray objectAtIndex:section];

    array.open = YES;

    NSInteger count = [array.category.list count];

    NSMutableArray *indexPathToInsert = [[NSMutableArray alloc] init];

    for (NSInteger i = 0; i<count;i++)

    {

        [indexPathToInsert addObject:[NSIndexPath indexPathForRow:i inSection:section]];

    }

    NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];

    NSInteger previousOpenIndex = self.openSectionIndex;

    if (previousOpenIndex != NSNotFound)

    {

        SectionInfo *sectionArray = [self.sectionInfoArray objectAtIndex:previousOpenIndex];

        sectionArray.open = NO;

        NSInteger counts = [sectionArray.category.list count];

        [sectionArray.sectionView toggleButtonPressed:FALSE];

        for (NSInteger i = 0; i<counts; i++)

        {

            [indexPathsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:previousOpenIndex]];

        }

    }

    UITableViewRowAnimation insertAnimation;

    UITableViewRowAnimation deleteAnimation;

    if (previousOpenIndex == NSNotFound || section < previousOpenIndex)

    {

        insertAnimation = UITableViewRowAnimationTop;

        deleteAnimation = UITableViewRowAnimationBottom;

    }

    else

    {

        insertAnimation = UITableViewRowAnimationBottom;

        deleteAnimation = UITableViewRowAnimationTop;

    }

    [self.tableView beginUpdates];

    [self.tableView insertRowsAtIndexPaths:indexPathToInsert withRowAnimation:insertAnimation];

    [self.tableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:deleteAnimation];

    [self.tableView endUpdates];

    self.openSectionIndex = section;

}

19.  As we carried out the deletions in above method, the same way we delete in the “sectionClosed:” method. The method is called if user tries to close the section that is already opened.

– (void) sectionClosed : (NSInteger) section{

    /*

     Create an array of the index paths of the rows in the section that was closed, then delete those rows from the table view.

     */

SectionInfo *sectionInfo = [self.sectionInfoArray objectAtIndex:section];

    sectionInfo.open = NO;

    NSInteger countOfRowsToDelete = [self.tableView numberOfRowsInSection:section];

    if (countOfRowsToDelete > 0) {

        NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];

        for (NSInteger i = 0; i < countOfRowsToDelete; i++) {

            [indexPathsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:section]];

        }

        [self.tableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:UITableViewRowAnimationTop];

    }

    self.openSectionIndex = NSNotFound;

}

And that’s it. Try building the project and run it.

Click here to download the source code

Happy coding.

References:

13 thoughts on “Expandable/Collapsible Table For iOS

    • Definitely Azeem. You only need to play with the delegate methods in SectionView Class:

      – (void) sectionClosed : (NSInteger) section;
      – (void) sectionOpened : (NSInteger) section;

      No need to change other functionality.
      Suppose, if you want to open some section say X, call the method of RootViewController. Like this

      RootViewController *viewController = [[RootViewController alloc] initWithNibName:@”RootViewController” bundle:nil];
      [viewController sectionOpened:X];

      Note: You just need to take care of the thing that the section you trying to open is already not opened. In that case you probably be looking for closing the section. Then you just need to call the other method. Like this

      [viewController sectionClosed:X];

      Thats so easy, buddy. Keep on experimenting. Good Luck.

  1. First, thanks for the tutorial
    Its very useful!

    I would like to leave the last selected cell highlighted, is that possible??
    How?
    Thanks a lot

    • Yes, it is possible. :)

      You just need to remove the following statement from the tableView:didSelectRowAtIndexPath: method in RootViewController (in the given code) or your own TableViewController :-

      [tableView deselectRowAtIndexPath:indexPath animated:YES];

      Explanation: The statement asks the system to simply “de-select” the row selected by user i..e indexPath.row. In case you want the row to stay selected. Simply dont ask the system. :)

  2. Help! I have followed your example almost exactly which is almost exactly like Apple’s documentation also. Everything works except for one thing. When I open another section the ‘previousOpenIndex’ section will not toggle closed and I am almost certain the ‘sectionArray’ is still open as well.
    The rows are deleted but the only way to toggle closed is to tap the section header again.
    I have used NSLog statements to verify the sections being opened as well as their identifiers. I am just stumped as I can not figure out what is wrong.
    Help me get past this one issue, please!

  3. Thanks for the Tutorial. It helped me a lot.

    Pls. can you help me for the following.

    1. Initially, I want to open first section.
    2. Want to add search functionality above the Table.

  4. It’s very cool. It give me a big help. So I use it in my project. But there is a bug and can’t solve it. The question is when I expandable one section which have more than one screen data, the table view can’t scroll to bottom and view the last three data. I want to attach my project but can’t. I need your help. This is my email address: primer2006@yahoo.com.cn . thanks very much.

  5. Hi, I had fixed the bug. I set the “autoresize subviews” property of the parent view of the tableview to true. but I didn’t set the subview’s autosizing.

  6. Thanks for the tutorial Harshit,

    A small Query….

    In this example, When we add a textfield to the cell and show it once cell expanded, fill in the details in textfield and collapse it…again do the same in another cell and scroll up and down….does the text entered in all the textfields holds or gets reset?

    Do we need to do anything to get the data hold?

  7. Thanks for the gr8 tutorial. But a help needed. I hav parsed the xml values and now i need to display it in expandablecollapsible view.. Is it possible? Kindly help me..

  8. I have to delete a particular cell from section when section is opened ,can u tell me if i have 10 cell in section one and section one is opened how can i delete any cell and maintain the opened state as well.

    • NSMutableArray *arr=[[NSMutableArray alloc]initWithArray:self.categoryList];
      [notesArrayImages removeObjectAtIndex:_indexPath.row];
      NSArray*newArrayImages=notesArrayImages;
      [arr removeObjectAtIndex:1];
      [arr insertObject:newArrayImages atIndex:1];
      self.categoryList=arr;
      [self.tableView beginUpdates];
      [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:_indexPath, nil] withRowAnimation:UITableViewRowAnimationRight];
      [self.tableView endUpdates];

Leave a Reply

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


2 + = eleven

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>