Expand/collapse section in UITableView in iOS

14 Solutions Collect From Internet About “Expand/collapse section in UITableView in iOS”

I’m pretty sure you’ll have to just make your own custom header row and just put that as the first row of each section. Subclassing the UITableView or the headers that are on there now would probably be a huge pain and I’m not sure you can easily get actions out of them the way they work now. You could easily set up a cell to LOOK like a header, and setup the tableView:didSelectRowAtIndexPath to expand or collapse the section it is within manually.

If I were you I’d store an array of booleans corresponding the the “expended” value of each of your sections. You could then have the tableView:didSelectRowAtIndexPath on each of your custom header rows toggle this value and then reload that specific section.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == 0) {
        ///it's the first row of any section so it would be your custom section header

        ///put in your code to toggle your boolean value here
        mybooleans[indexPath.section] = !mybooleans[indexPath.section];

        ///reload this section
        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
    }
}

You’d then setup your number numberOfRowsInSection to check the mybooleans value and return either 1 if the section isn’t expanded, or 1+ the number of items in the section if it is expanded.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    if (mybooleans[section]) {
        ///we want the number of people plus the header cell
        return [self numberOfPeopleInGroup:section] + 1;
    } else {
        ///we just want the header cell
        return 1;
    }
}

You would also have to update your cellForRowAtIndexPath to return a custom header cell for the first row in any section.

Some sample code for animating an expand/collapse action using a table view section header is provided by Apple here: Table View Animations and Gestures

The key to this approach is to implement - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section and return a custom UIView which includes a button (typically the same size as the header view itself). By subclassing UIView and using that for the header view (as this sample does), you can easily store additional data such as the section number.

I got a nice solution inspired by Apple’s Table View Animations and Gestures. I deleted unnecessary parts from Apple’s sample and translated it into swift.

I know the answer is quite long, but all the code is necessary. Fortunately, you can just copy&past most of the code and just need to do a bit modification on the step 1 and 3

1.create SectionHeaderView.swift and SectionHeaderView.xib

import UIKit

protocol SectionHeaderViewDelegate {
    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int)
    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int)
}

class SectionHeaderView: UITableViewHeaderFooterView {

    var section: Int?
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var disclosureButton: UIButton!
    @IBAction func toggleOpen() {
        self.toggleOpenWithUserAction(true)
    }
    var delegate: SectionHeaderViewDelegate?

    func toggleOpenWithUserAction(userAction: Bool) {
        self.disclosureButton.selected = !self.disclosureButton.selected

        if userAction {
            if self.disclosureButton.selected {
                self.delegate?.sectionHeaderView(self, sectionClosed: self.section!)
            } else {
                self.delegate?.sectionHeaderView(self, sectionOpened: self.section!)
            }
        }
    }

    override func awakeFromNib() {
        var tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "toggleOpen")
        self.addGestureRecognizer(tapGesture)
        // change the button image here, you can also set image via IB.
        self.disclosureButton.setImage(UIImage(named: "arrow_up"), forState: UIControlState.Selected)
        self.disclosureButton.setImage(UIImage(named: "arrow_down"), forState: UIControlState.Normal)
    }

}

the SectionHeaderView.xib(the view with gray background) should look something like this in a tableview(you can customize it according to your needs, of course):
enter image description here

note:

a) the toggleOpen action should be linked to disclosureButton

b) the disclosureButton and toggleOpen action are not necessary. You can delete these 2 things if you don’t need the button.

2.create SectionInfo.swift

import UIKit

class SectionInfo: NSObject {
    var open: Bool = true
    var itemsInSection: NSMutableArray = []
    var sectionTitle: String?

    init(itemsInSection: NSMutableArray, sectionTitle: String) {
        self.itemsInSection = itemsInSection
        self.sectionTitle = sectionTitle
    }
}

3.in your tableview

import UIKit

class TableViewController: UITableViewController, SectionHeaderViewDelegate  {

    let SectionHeaderViewIdentifier = "SectionHeaderViewIdentifier"

    var sectionInfoArray: NSMutableArray = []

    override func viewDidLoad() {
        super.viewDidLoad()

        let sectionHeaderNib: UINib = UINib(nibName: "SectionHeaderView", bundle: nil)
        self.tableView.registerNib(sectionHeaderNib, forHeaderFooterViewReuseIdentifier: SectionHeaderViewIdentifier)

        // you can change section height based on your needs
        self.tableView.sectionHeaderHeight = 30

        // You should set up your SectionInfo here
        var firstSection: SectionInfo = SectionInfo(itemsInSection: ["1"], sectionTitle: "firstSection")
        var secondSection: SectionInfo = SectionInfo(itemsInSection: ["2"], sectionTitle: "secondSection"))
        sectionInfoArray.addObjectsFromArray([firstSection, secondSection])
    }

    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return sectionInfoArray.count
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if self.sectionInfoArray.count > 0 {
            var sectionInfo: SectionInfo = sectionInfoArray[section] as! SectionInfo
            if sectionInfo.open {
                return sectionInfo.open ? sectionInfo.itemsInSection.count : 0
            }
        }
        return 0
    }

    override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let sectionHeaderView: SectionHeaderView! = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier(SectionHeaderViewIdentifier) as! SectionHeaderView
        var sectionInfo: SectionInfo = sectionInfoArray[section] as! SectionInfo

        sectionHeaderView.titleLabel.text = sectionInfo.sectionTitle
        sectionHeaderView.section = section
        sectionHeaderView.delegate = self
        let backGroundView = UIView()
        // you can customize the background color of the header here
        backGroundView.backgroundColor = UIColor(red:0.89, green:0.89, blue:0.89, alpha:1)
        sectionHeaderView.backgroundView = backGroundView
        return sectionHeaderView
    }

    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int) {
        var sectionInfo: SectionInfo = sectionInfoArray[sectionOpened] as! SectionInfo
        var countOfRowsToInsert = sectionInfo.itemsInSection.count
        sectionInfo.open = true

        var indexPathToInsert: NSMutableArray = NSMutableArray()
        for i in 0..<countOfRowsToInsert {
            indexPathToInsert.addObject(NSIndexPath(forRow: i, inSection: sectionOpened))
        }
        self.tableView.insertRowsAtIndexPaths(indexPathToInsert as [AnyObject], withRowAnimation: .Top)
    }

    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int) {
        var sectionInfo: SectionInfo = sectionInfoArray[sectionClosed] as! SectionInfo
        var countOfRowsToDelete = sectionInfo.itemsInSection.count
        sectionInfo.open = false
        if countOfRowsToDelete > 0 {
            var indexPathToDelete: NSMutableArray = NSMutableArray()
            for i in 0..<countOfRowsToDelete {
                indexPathToDelete.addObject(NSIndexPath(forRow: i, inSection: sectionClosed))
            }
            self.tableView.deleteRowsAtIndexPaths(indexPathToDelete as [AnyObject], withRowAnimation: .Top)
        }
    }
}

I have a better solution that you should add a UIButton into section header and set this button’s size equal to section size, but make it hidden by clear background color, after that you are easily to check which section is clicked to expand or collapse

I ended up just creating a headerView that contained a button ( i saw Son Nguyen’s solution above after the fact, but heres my code.. it looks like a lot but it’s pretty simple):

declare a couple bools for you sections

bool customerIsCollapsed = NO;
bool siteIsCollapsed = NO;

…code

now in your tableview delegate methods…

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _tblSearchResults.frame.size.width, 35)];

    UILabel *lblSection = [UILabel new];
    [lblSection setFrame:CGRectMake(0, 0, 300, 30)];
    [lblSection setFont:[UIFont fontWithName:@"Helvetica-Bold" size:17]];
    [lblSection setBackgroundColor:[UIColor clearColor]];
    lblSection.alpha = 0.5;
    if(section == 0)
    {
        if(!customerIsCollapsed)
            [lblSection setText:@"Customers    --touch to show--"];
        else
            [lblSection setText:@"Customers    --touch to hide--"];
    }
    else
    {
        if(!siteIsCollapsed)
            [lblSection setText:@"Sites    --touch to show--"];
        else
            [lblSection setText:@"Sites    --touch to hide--"];    }

    UIButton *btnCollapse = [UIButton buttonWithType:UIButtonTypeCustom];
    [btnCollapse setFrame:CGRectMake(0, 0, _tblSearchResults.frame.size.width, 35)];
    [btnCollapse setBackgroundColor:[UIColor clearColor]];
    [btnCollapse addTarget:self action:@selector(touchedSection:) forControlEvents:UIControlEventTouchUpInside];
    btnCollapse.tag = section;


    [headerView addSubview:lblSection];
    [headerView addSubview:btnCollapse];

    return headerView;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    if(section == 0)
    {
        if(customerIsCollapsed)
            return 0;
        else
            return _customerArray.count;
    }
    else if (section == 1)
    {
        if(siteIsCollapsed)
            return 0;
        else
        return _siteArray.count;

    }
    return 0;
}

and finally the function that gets called when you touch one of the section header buttons:

- (IBAction)touchedSection:(id)sender
{
    UIButton *btnSection = (UIButton *)sender;

    if(btnSection.tag == 0)
    {
        NSLog(@"Touched Customers header");
        if(!customerIsCollapsed)
            customerIsCollapsed = YES;
        else
            customerIsCollapsed = NO;

    }
    else if(btnSection.tag == 1)
    {
        NSLog(@"Touched Site header");
        if(!siteIsCollapsed)
            siteIsCollapsed = YES;
        else
            siteIsCollapsed = NO;

    }
    [_tblSearchResults reloadData];
}

This is the best way i found to create expandable table view cells

.h file

  NSMutableIndexSet *expandedSections;

.m file

if (!expandedSections)
    {
        expandedSections = [[NSMutableIndexSet alloc] init];
    }
   UITableView *masterTable = [[UITableView alloc] initWithFrame:CGRectMake(0,100,1024,648) style:UITableViewStyleGrouped];
    masterTable.delegate = self;
    masterTable.dataSource = self;
    [self.view addSubview:masterTable];

Table view delegate methods

- (BOOL)tableView:(UITableView *)tableView canCollapseSection:(NSInteger)section
{
    // if (section>0) return YES;

    return YES;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 4;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if ([self tableView:tableView canCollapseSection:section])
    {
        if ([expandedSections containsIndex:section])
        {
            return 5; // return rows when expanded
        }

        return 1; // only top row showing
    }

    // Return the number of rows in the section.
    return 1;
}

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

    // Configure the cell...

    if ([self tableView:tableView canCollapseSection:indexPath.section])
    {
        if (!indexPath.row)
        {
            // first row
            cell.textLabel.text = @"Expandable"; // only top row showing

            if ([expandedSections containsIndex:indexPath.section])
            {

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableContract"]];
                cell.accessoryView = imView;
            }
            else
            {

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableExpand"]];
                cell.accessoryView = imView;
            }
        }
        else
        {
            // all other rows
            if (indexPath.section == 0) {
                cell.textLabel.text = @"section one";
            }else if (indexPath.section == 1) {
                cell.textLabel.text = @"section 2";
            }else if (indexPath.section == 2) {
                cell.textLabel.text = @"3";
            }else {
                cell.textLabel.text = @"some other sections";
            }

            cell.accessoryView = nil;
            cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        }
    }
    else
    {
        cell.accessoryView = nil;
        cell.textLabel.text = @"Normal Cell";

    }

    return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([self tableView:tableView canCollapseSection:indexPath.section])
    {
        if (!indexPath.row)
        {
            // only first row toggles exapand/collapse
            [tableView deselectRowAtIndexPath:indexPath animated:YES];

            NSInteger section = indexPath.section;
            BOOL currentlyExpanded = [expandedSections containsIndex:section];
            NSInteger rows;


            NSMutableArray *tmpArray = [NSMutableArray array];

            if (currentlyExpanded)
            {
                rows = [self tableView:tableView numberOfRowsInSection:section];
                [expandedSections removeIndex:section];

            }
            else
            {
                [expandedSections addIndex:section];
                rows = [self tableView:tableView numberOfRowsInSection:section];
            }


            for (int i=1; i<rows; i++)
            {
                NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i 
                                                               inSection:section];
                [tmpArray addObject:tmpIndexPath];
            }

            UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

            if (currentlyExpanded)
            {
                [tableView deleteRowsAtIndexPaths:tmpArray 
                                 withRowAnimation:UITableViewRowAnimationTop];

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableExpand"]];
                cell.accessoryView = imView;
            }
            else
            {
                [tableView insertRowsAtIndexPaths:tmpArray 
                                 withRowAnimation:UITableViewRowAnimationTop];

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableContract"]];
                cell.accessoryView = imView;
            }
        }
    }

    NSLog(@"section :%d,row:%d",indexPath.section,indexPath.row);

}

To implement the collapsible table section in iOS, the magic is how to control the number of rows for each section, or we can manage the height of rows for each section.

Also, we need to customize the section header so that we can listen to the tap event from the header area (whether it’s a button or the whole header).

How to deal with the header? It’s very simple, we extend the UITableViewCell class and make a custom header cell like so:

import UIKit

class CollapsibleTableViewHeader: UITableViewCell {

    @IBOutlet var titleLabel: UILabel!
    @IBOutlet var toggleButton: UIButton!

}

then use the viewForHeaderInSection to hook up the header cell:

override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  let header = tableView.dequeueReusableCellWithIdentifier("header") as! CollapsibleTableViewHeader

  header.titleLabel.text = sections[section].name
  header.toggleButton.tag = section
  header.toggleButton.addTarget(self, action: #selector(CollapsibleTableViewController.toggleCollapse), forControlEvents: .TouchUpInside)

  header.toggleButton.rotate(sections[section].collapsed! ? 0.0 : CGFloat(M_PI_2))

  return header.contentView
}

remember we have to return the contentView because this function expects a UIView to be returned.

Now let’s deal with the collapsible part, here is the toggle function that toggle the collapsible prop of each section:

func toggleCollapse(sender: UIButton) {
  let section = sender.tag
  let collapsed = sections[section].collapsed

  // Toggle collapse
  sections[section].collapsed = !collapsed

  // Reload section
  tableView.reloadSections(NSIndexSet(index: section), withRowAnimation: .Automatic)
}

depends on how you manage the section data, in this case, I have the section data something like this:

struct Section {
  var name: String!
  var items: [String]!
  var collapsed: Bool!

  init(name: String, items: [String]) {
    self.name = name
    self.items = items
    self.collapsed = false
  }
}

var sections = [Section]()

sections = [
  Section(name: "Mac", items: ["MacBook", "MacBook Air", "MacBook Pro", "iMac", "Mac Pro", "Mac mini", "Accessories", "OS X El Capitan"]),
  Section(name: "iPad", items: ["iPad Pro", "iPad Air 2", "iPad mini 4", "Accessories"]),
  Section(name: "iPhone", items: ["iPhone 6s", "iPhone 6", "iPhone SE", "Accessories"])
]

at last, what we need to do is based on the collapsible prop of each section, control the number of rows of that section:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return (sections[section].collapsed!) ? 0 : sections[section].items.count
}

I have a fully working demo on my Github: https://github.com/jeantimex/ios-swift-collapsible-table-section

demo

If you want to implement the collapsible sections in a grouped-style table, I have another demo with source code here: https://github.com/jeantimex/ios-swift-collapsible-table-section-in-grouped-section

Hope that helps.

So, based on the ‘button in header’ solution, here is a clean and minimalist implementation:

  • you keep track of collapsed (or expanded) sections in a property
  • you tag the button with the section index
  • you set a selected state on that button to change the arrow direction (like △ and ▽)

Here is the code:

@interface MyTableViewController ()
@property (nonatomic, strong) NSMutableIndexSet *collapsedSections;
@end

...

@implementation MyTableViewController

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (!self)
        return;
    self.collapsedSections = [NSMutableIndexSet indexSet];
    return self;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // if section is collapsed
    if ([self.collapsedSections containsIndex:section])
        return 0;

    // if section is expanded
#warning incomplete implementation
    return [super tableView:tableView numberOfRowsInSection:section];
}

- (IBAction)toggleSectionHeader:(UIView *)sender
{
    UITableView *tableView = self.tableView;
    NSInteger section = sender.tag;

    MyTableViewHeaderFooterView *headerView = (MyTableViewHeaderFooterView *)[self tableView:tableView viewForHeaderInSection:section];

    if ([self.collapsedSections containsIndex:section])
    {
        // section is collapsed
        headerView.button.selected = YES;
        [self.collapsedSections removeIndex:section];
    }
    else
    {
        // section is expanded
        headerView.button.selected = NO;
        [self.collapsedSections addIndex:section];
    }

    [tableView beginUpdates];
    [tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView endUpdates];
}

@end

I found another relatively simple way to solve that problem. By using this method we will not required to alter our cell which is almost always related to data array index, potentially causing mess in our view controller.

First, we add this following properties to our controller class:

@property (strong, nonatomic) NSMutableArray* collapsedSections;
@property (strong, nonatomic) NSMutableArray* sectionViews;

collapsedSections will save collapsed section numbers.
sectionViews will store our custom section view.

Synthesize it:

@synthesize collapsedSections;
@synthesize sectionViews;

Initialize it:

- (void) viewDidLoad
{
    [super viewDidLoad];

    self.collapsedSections = [NSMutableArray array];
    self.sectionViews      = [NSMutableArray array];
}

After that, we must connect our UITableView so it can be accessed from within our view controller class:

@property (strong, nonatomic) IBOutlet UITableView *tblMain;

Connect it from XIB to view controller using ctrl + drag like usually.

Then we create view as custom section header for our table view by implementing this UITableView delegate:

- (UIView*) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    // Create View
    CGRect frame = CGRectZero;

    frame.origin = CGPointZero;

    frame.size.height = 30.f;
    frame.size.width  = tableView.bounds.size.width;

    UIView* view = [[UIView alloc] initWithFrame:frame];

    [view setBackgroundColor:[UIColor blueColor]];

    // Add label for title
    NSArray* titles = @[@"Title 1", @"Title 2", @"Title 3"];

    NSString* selectedTitle = [titles objectAtIndex:section];

    CGRect labelFrame = frame;

    labelFrame.size.height = 30.f;
    labelFrame.size.width -= 20.f;
    labelFrame.origin.x += 10.f;

    UILabel* titleLabel = [[UILabel alloc] initWithFrame:labelFrame];

    [titleLabel setText:selectedTitle];
    [titleLabel setTextColor:[UIColor whiteColor]];

    [view addSubview:titleLabel];

    // Add touch gesture
    [self attachTapGestureToView:view];

    // Save created view to our class property array
    [self saveSectionView:view inSection:section];

    return view;
}

Next, we implement method to save our previously created custom section header in class property:

- (void) saveSectionView:(UIView*) view inSection:(NSInteger) section
{
    NSInteger sectionCount = [self numberOfSectionsInTableView:[self tblMain]];

    if(section < sectionCount)
    {
        if([[self sectionViews] indexOfObject:view] == NSNotFound)
        {
            [[self sectionViews] addObject:view];
        }
    }
}

Add UIGestureRecognizerDelegate to our view controller .h file:

@interface MyViewController : UIViewController<UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate>

Then we create method attachTapGestureToView:

- (void) attachTapGestureToView:(UIView*) view
{
    UITapGestureRecognizer* tapAction = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];

    [tapAction setDelegate:self];

    [view addGestureRecognizer:tapAction];
}

Above method will add tap gesture recognizer to all of section view we created before. Next we should implement onTap: selector

- (void) onTap:(UITapGestureRecognizer*) gestureRecognizer
{
    // Take view who attach current recognizer
    UIView* sectionView = [gestureRecognizer view]; 

    // [self sectionViews] is Array containing our custom section views
    NSInteger section = [self sectionNumberOfView:sectionView];

    // [self tblMain] is our connected IBOutlet table view
    NSInteger sectionCount = [self numberOfSectionsInTableView:[self tblMain]];

    // If section more than section count minus one set at last
    section = section > (sectionCount - 1) ? 2 : section;

    [self toggleCollapseSection:section];
}

Above method will invoked when user tap any of our table view section. This method search correct section number based on our sectionViews array we created before.

Also, we implement method to get wihch section of header view belongs to.

- (NSInteger) sectionNumberOfView:(UIView*) view
{
    UILabel* label = [[view subviews] objectAtIndex:0];

    NSInteger sectionNum = 0;

    for(UIView* sectionView in [self sectionViews])
    {
        UILabel* sectionLabel = [[sectionView subviews] objectAtIndex:0];

        //NSLog(@"Section: %d -> %@ vs %@", sectionNum, [label text], [sectionLabel text]);

        if([[label text] isEqualToString:[sectionLabel text]])
        {
            return sectionNum;
        }

        sectionNum++;
    }

    return NSNotFound;
}

Next, we must implement method toggleCollapseSection:

- (void) toggleCollapseSection:(NSInteger) section
{
    if([self isCollapsedSection:section])
    {
        [self removeCollapsedSection:section];
    }
    else
    {
        [self addCollapsedSection:section];
    }

    [[self tblMain] reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationFade];
}

This method will insert/remove section number to our collapsedSections array we created before. When a section number inserted to that array, it means that the section should be collapsed and expanded if otherwise.

Next we implement removeCollapsedSection:, addCollapsedSection:section and isCollapsedSection:section

- (BOOL)isCollapsedSection:(NSInteger) section
{
    for(NSNumber* existing in [self collapsedSections])
    {
        NSInteger current = [existing integerValue];

        if(current == section)
        {
            return YES;
        }
    }

    return NO;
}

- (void)removeCollapsedSection:(NSInteger) section
{
    [[self collapsedSections] removeObjectIdenticalTo:[NSNumber numberWithInteger:section]];
}

- (void)addCollapsedSection:(NSInteger) section
{
    [[self collapsedSections] addObject:[NSNumber numberWithInteger:section]];
}

This three method is just helpers to make us easier in accessing collapsedSections array.

Finally, implement this table view delegate so our custom section views looks nice.

- (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    return 30.f; // Same as each custom section view height
}

Hope it helps.

I’ve used a NSDictionary as datasource, this looks like a lot of code, but it’s really simple and works very well!
how looks here

I created a enum for the sections

typedef NS_ENUM(NSUInteger, TableViewSection) {

    TableViewSection0 = 0,
    TableViewSection1,
    TableViewSection2,
    TableViewSectionCount
};

sections property:

@property (nonatomic, strong) NSMutableDictionary * sectionsDisctionary;

A method returning my sections:

-(NSArray <NSNumber *> * )sections{

    return @[@(TableViewSection0), @(TableViewSection1), @(TableViewSection2)];
}

And then setup my data soruce:

-(void)loadAndSetupData{

    self.sectionsDisctionary = [NSMutableDictionary dictionary];

    NSArray * sections = [self sections];

    for (NSNumber * section in sections) {

    NSArray * sectionObjects = [self objectsForSection:section.integerValue];

    [self.sectionsDisctionary setObject:[NSMutableDictionary dictionaryWithDictionary:@{@"visible" : @YES, @"objects" : sectionObjects}] forKey:section];
    }
}

-(NSArray *)objectsForSection:(NSInteger)section{

    NSArray * objects;

    switch (section) {

        case TableViewSection0:

            objects = @[] // objects for section 0;
            break;

        case TableViewSection1:

            objects = @[] // objects for section 1;
            break;

        case TableViewSection2:

            objects = @[] // objects for section 2;
            break;

        default:
            break;
    }

    return objects;
}

The next methods, will help you to know when a section is opened, and how to respond to tableview datasource:

Respond the section to datasource:

/**
 *  Asks the delegate for a view object to display in the header of the specified section of the table view.
 *
 *  @param tableView The table-view object asking for the view object.
 *  @param section   An index number identifying a section of tableView .
 *
 *  @return A view object to be displayed in the header of section .
 */
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{

    NSString * headerName = [self titleForSection:section];

    YourCustomSectionHeaderClass * header = (YourCustomSectionHeaderClass *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:YourCustomSectionHeaderClassIdentifier];

    [header setTag:section];
    [header addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]];
    header.title = headerName;
    header.collapsed = [self sectionIsOpened:section];


    return header;
}

/**
 * Asks the data source to return the number of sections in the table view
 *
 * @param An object representing the table view requesting this information.
 * @return The number of sections in tableView.
 */
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    // Return the number of sections.

    return self.sectionsDisctionary.count;
}

/**
 * Tells the data source to return the number of rows in a given section of a table view
 *
 * @param tableView: The table-view object requesting this information.
 * @param section: An index number identifying a section in tableView.
 * @return The number of rows in section.
 */
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{

    BOOL sectionOpened = [self sectionIsOpened:section];
    return sectionOpened ? [[self objectsForSection:section] count] : 0;
}

Tools:

/**
 Return the section at the given index

 @param index the index

 @return The section in the given index
 */
-(NSMutableDictionary *)sectionAtIndex:(NSInteger)index{

    NSString * asectionKey = [self.sectionsDisctionary.allKeys objectAtIndex:index];

    return [self.sectionsDisctionary objectForKey:asectionKey];
}

/**
 Check if a section is currently opened

 @param section the section to check

 @return YES if is opened
 */
-(BOOL)sectionIsOpened:(NSInteger)section{

    NSDictionary * asection = [self sectionAtIndex:section];
    BOOL sectionOpened = [[asection objectForKey:@"visible"] boolValue];

    return sectionOpened;
}


/**
 Handle the section tap

 @param tap the UITapGestureRecognizer
 */
- (void)handleTapGesture:(UITapGestureRecognizer*)tap{

    NSInteger index = tap.view.tag;

    [self toggleSection:index];
}

Toggle section visibility

/**
 Switch the state of the section at the given section number

 @param section the section number
 */
-(void)toggleSection:(NSInteger)section{

    if (index >= 0){

        NSMutableDictionary * asection = [self sectionAtIndex:section];

        [asection setObject:@(![self sectionIsOpened:section]) forKey:@"visible"];

        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationFade];
    }
}
// -------------------------------------------------------------------------------
//  tableView:viewForHeaderInSection:
// -------------------------------------------------------------------------------
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    UIView *mView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 20, 20)];
    [mView setBackgroundColor:[UIColor greenColor]];

    UIImageView *logoView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 5, 20, 20)];
    [logoView setImage:[UIImage imageNamed:@"carat.png"]];
    [mView addSubview:logoView];

    UIButton *bt = [UIButton buttonWithType:UIButtonTypeCustom];
    [bt setFrame:CGRectMake(0, 0, 150, 30)];
    [bt setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [bt setTag:section];
    [bt.titleLabel setFont:[UIFont systemFontOfSize:20]];
    [bt.titleLabel setTextAlignment:NSTextAlignmentCenter];
    [bt.titleLabel setTextColor:[UIColor blackColor]];
    [bt setTitle: @"More Info" forState: UIControlStateNormal];
    [bt addTarget:self action:@selector(addCell:) forControlEvents:UIControlEventTouchUpInside];
    [mView addSubview:bt];
    return mView;

}

#pragma mark - Suppose you want to hide/show section 2... then
#pragma mark  add or remove the section on toggle the section header for more info

- (void)addCell:(UIButton *)bt{

    // If section of more information
    if(bt.tag == 2) {

        // Initially more info is close, if more info is open
        if(ifOpen) {
            DLog(@"close More info");

            // Set height of section
            heightOfSection = 0.0f;

            // Reset the parameter that more info is closed now
            ifOpen = NO;
        }else {
            // Set height of section
            heightOfSection = 45.0f;
            // Reset the parameter that more info is closed now
            DLog(@"open more info again");
            ifOpen = YES;
        }
        //[self.tableView reloadData];
        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:2] withRowAnimation:UITableViewRowAnimationFade];
    }

}// end addCell
#pragma mark -
#pragma mark  What will be the height of the section, Make it dynamic

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

    if (indexPath.section == 2) {
        return heightOfSection;
    }else {
        return 45.0f;
    }

// vKj

This action will happen in your didSelectRowAtIndexPath, when you will try to hide or show number of cell in a  section

first of all declare a global variable numberOfSectionInMoreInfo in .h file and in your viewDidLoad set suppose to numberOfSectionInMoreInfo = 4.

Now use following logic: 


 // More info link
        if(row == 3) {

            /*Logic: We are trying to hide/show the number of row into more information section */

            NSString *log= [NSString stringWithFormat:@"Number of section in more %i",numberOfSectionInMoreInfo];

            [objSpineCustomProtocol showAlertMessage:log];

            // Check if the number of rows are open or close in view
            if(numberOfSectionInMoreInfo > 4) {

                // close the more info toggle
                numberOfSectionInMoreInfo = 4;

            }else {

                // Open more info toggle
                numberOfSectionInMoreInfo = 9;

            }

            //reload this section
            [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];

//vKj

Some sample code for animating an expand/collapse action using a table view section header is provided by Apple here: Table View Animations and Gestures

The key to this approach is to implement – (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section and return a custom UIView which includes a button (typically the same size as the header view itself). By subclassing UIView and using that for the header view (as this sample does), you can easily store additional data such as the section number.

Expanding on this answer written in Objective C, I wrote the following for those writing in Swift

The idea is to use sections within the table and set the number of rows in the section to 1 (collapsed) and 3(expanded) when the first row in that section is tapped

The table decides how many rows to draw based on an array of Boolean values

You’ll need to create two rows in storyboard and give them the reuse identifiers ‘CollapsingRow’ and ‘GroupHeading’

import UIKit

class CollapsingTVC:UITableViewController{

    var sectionVisibilityArray:[Bool]!// Array index corresponds to section in table

    override func viewDidLoad(){
        super.viewDidLoad()
        sectionVisibilityArray = [false,false,false]
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }

    override func numberOfSections(in tableView: UITableView) -> Int{
        return sectionVisibilityArray.count
    }
    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat{
        return 0
    }

    // numberOfRowsInSection - Get count of entries
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        var rowsToShow:Int = 0
        if(sectionVisibilityArray[section]){
            rowsToShow = 3 // Or however many rows should be displayed in that section
        }else{
            rowsToShow = 1
        }
        return rowsToShow
    }// numberOfRowsInSection


    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){
        if(indexPath.row == 0){
            if(sectionVisibilityArray[indexPath.section]){
                sectionVisibilityArray[indexPath.section] = false
            }else{
                sectionVisibilityArray[indexPath.section] = true
            }
            self.tableView.reloadSections([indexPath.section], with: .automatic)
        }
    }

    // cellForRowAtIndexPath - Get table cell corresponding to this IndexPath
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        var cell:UITableViewCell

        if(indexPath.row == 0){
             cell = tableView.dequeueReusableCell(withIdentifier: "GroupHeading", for: indexPath as IndexPath)
        }else{
            cell = tableView.dequeueReusableCell(withIdentifier: "CollapsingRow", for: indexPath as IndexPath)
        }

        return cell

    }// cellForRowAtIndexPath

}