Drop-Down List in UITableView in iOS

Here is an MVC based solution.

Create a Model class ClsMenuGroup for your Sections

class ClsMenuGroup: NSObject {

    // We can also add Menu group's name and other details here.
    var isSelected:Bool = false
    var arrMenus:[ClsMenu]!
}

Create a Model class ClsMenu for your Rows

class ClsMenu: NSObject {

    var strMenuTitle:String!
    var strImageNameSuffix:String!
    var objSelector:Selector!   // This is the selector method which will be called when this menu is selected.
    var isSelected:Bool = false

    init(pstrTitle:String, pstrImageName:String, pactionMehod:Selector) {

        strMenuTitle = pstrTitle
        strImageNameSuffix = pstrImageName
        objSelector = pactionMehod
    }
}

Create groups array in your ViewController

 class YourViewController: UIViewController, UITableViewDelegate {

    @IBOutlet var tblMenu: UITableView!
    var objTableDataSource:HDTableDataSource!
    var arrMenuGroups:[AnyObject]!

    // MARK: - View Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        if arrMenuGroups == nil {
            arrMenuGroups = Array()
        }

        let objMenuGroup = ClsMenuGroup()
        objMenuGroup.arrMenus = Array()

        var objMenu = ClsMenu(pstrTitle: "Manu1", pstrImageName: "Manu1.png", pactionMehod: "menuAction1")
        objMenuGroup.arrMenus.append(objMenu)

        objMenu = ClsMenu(pstrTitle: "Menu2", pstrImageName: "Menu2.png", pactionMehod: "menuAction2")
        objMenuGroup.arrMenus.append(objMenu)

        arrMenuGroups.append(objMenuGroup)
        configureTable()
    }


    func configureTable(){

        objTableDataSource = HDTableDataSource(items: nil, cellIdentifier: "SideMenuCell", configureCellBlock: { (cell, item, indexPath) -> Void in

            let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
            let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]
            let objCell:YourCell = cell as! YourCell

            objCell.configureCell(objTmpMenu)  // This method sets the IBOutlets of cell in YourCell.m file.
        })

        objTableDataSource.sectionItemBlock = {(objSection:AnyObject!) -> [AnyObject]! in

            let objMenuGroup = objSection as! ClsMenuGroup
            return (objMenuGroup.isSelected == true) ? objMenuGroup.arrMenus : 0
        }

        objTableDataSource.arrSections = self.arrMenuGroups
        tblMenu.dataSource = objTableDataSource
        tblMenu.reloadData()
    }

    // MARK: - Tableview Delegate

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

        let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
        let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]

        if objTmpMenu.objSelector != nil && self.respondsToSelector(objTmpMenu.objSelector) == true {
            self.performSelector(objTmpMenu.objSelector)  // Call the method for the selected menu.
        }

        tableView.reloadData()
    }

    func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

        let arrViews:[AnyObject] = NSBundle.mainBundle().loadNibNamed("YourCustomSectionView", owner: self, options: nil)
        let objHeaderView = arrViews[0] as! UIView
        objHeaderView.sectionToggleBlock = {(objSection:AnyObject!) -> Void in

            let objMenuGroup = objSection as! ClsMenuGroup
            objMenuGroup.isSelected = !objMenuGroup.isSelected
            tableView.reloadData()
        }
        return objHeaderView
    }

    // MARK: - Menu methods

    func menuAction1(){

    }

    func menuAction2(){

    }
}

I have used HDTableDataSource in place of Tableview's data source methods. You may find example of HDTableDataSource from Github.

Advantages of above code is

  1. You can anytime change the order of any menu or section or interchange menu and section, without changing other functions.
  2. You will not need to add long code of else if ladder in your tableview's delegate methods
  3. You can specify icon, title or other attribute for your menu item separately like adding badge count, changing selected menu's color etc.
  4. You may also use multiple cells or sections by applying minor changes to existing code

The easier and most natural way to implement this if via table view cells. No expanding cell views, no section headers, plain and simply cells (we're in a table view after all).

The design is as following:

  • using a MVVM approach, create a CollapsableViewModel class that holds the information needed to configure the cell: label, image
  • besides the above one, there are two extra fields: children, which is an array of CollapsableViewModel objects, and isCollapsed, which holds the state of the drop down
  • the view controller holds a reference to the hierarchy of CollapsableViewModel, as well as a flat list containing the view models that will be rendered on the screen (the displayedRows property)
  • whenever a cell is tapped, check if it has children, and add or remove rows in both displayedRows and in the table view, via the insertRowsAtIndexPaths() and deleteRowsAtIndexPaths() functions.

The Swift code is as following (note that the code makes use only of the label property of the view model, to keep it clean):

import UIKit

class CollapsableViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsableViewModel]
    var isCollapsed: Bool
    
    init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed
    }
}

class CollapsableTableViewController: UITableViewController {
    let data = [
        CollapsableViewModel(label: "Account", image: nil, children: [
            CollapsableViewModel(label: "Profile"),
            CollapsableViewModel(label: "Activate account"),
            CollapsableViewModel(label: "Change password")]),
        CollapsableViewModel(label: "Group"),
        CollapsableViewModel(label: "Events", image: nil, children: [
            CollapsableViewModel(label: "Nearby"),
            CollapsableViewModel(label: "Global"),
            ]),
        CollapsableViewModel(label: "Deals"),
    ]
    
    var displayedRows: [CollapsableViewModel] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        displayedRows = data
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return displayedRows.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier") ?? UITableViewCell()
        let viewModel = displayedRows[indexPath.row]
        cell.textLabel!.text = viewModel.label
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
        let viewModel = displayedRows[indexPath.row]
        if viewModel.children.count > 0 {
            let range = indexPath.row+1...indexPath.row+viewModel.children.count
            let indexPaths = range.map { IndexPath(row: $0, section: indexPath.section) }
            tableView.beginUpdates()
            if viewModel.isCollapsed {
                displayedRows.insert(contentsOf: viewModel.children, at: indexPath.row + 1)
                tableView.insertRows(at: indexPaths, with: .automatic)
            } else {
                displayedRows.removeSubrange(range)
                tableView.deleteRows(at: indexPaths, with: .automatic)
            }
            tableView.endUpdates()
        }
        viewModel.isCollapsed = !viewModel.isCollapsed
    }
}

The Objective-C counterpart is easy to translate, I added the Swift version only as it's shorter and more readable.

With a couple of small changes, the code can be used to generate drop down lists of multiple levels.

Edit

People asked me about the separators, this can be achieved by adding a custom class CollapsibleTableViewCell which get's configured with a view model (finally, move the cell configuration logic from the controller to where it belongs - the cell). Credits for the separator logic only for some of the cells go to people answering this SO question.

Firstly, update the model, add a needsSeparator property that tells the table view cell to render or not the separator:

class CollapsableViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsableViewModel]
    var isCollapsed: Bool
    var needsSeparator: Bool = true
    
    init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed
        
        for child in self.children {
            child.needsSeparator = false
        }
        self.children.last?.needsSeparator = true
    }
}

Then, add the cell class:

class CollapsibleTableViewCell: UITableViewCell {
    let separator = UIView(frame: .zero)
    
    func configure(withViewModel viewModel: CollapsableViewModel) {
        self.textLabel?.text = viewModel.label
        if(viewModel.needsSeparator) {
            separator.backgroundColor = .gray
            contentView.addSubview(separator)
        } else {
            separator.removeFromSuperview()
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let separatorHeight = 1 / UIScreen.main.scale
        separator.frame = CGRect(x: separatorInset.left,
                                 y: contentView.bounds.height - separatorHeight,
                                 width: contentView.bounds.width-separatorInset.left-separatorInset.right,
                                 height: separatorHeight)
    }
}

cellForRowAtIndexPath would then need to be modified to return this kind of cells:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = (tableView.dequeueReusableCell(withIdentifier: "CollapsibleTableViewCell") as? CollapsibleTableViewCell) ?? CollapsibleTableViewCell(style: .default, reuseIdentifier: "CollapsibleTableViewCell")
        cell.configure(withViewModel: displayedRows[indexPath.row])
        return cell
    }

One last step, remove the default table view cell separators - either from xib or from code (tableView.separatorStyle = .none).


Usually I do it by setting the row height. For example, you have two menu items with drop-down lists:

  • Menu 1
    • Item 1.1
    • Item 1.2
    • Item 1.3
  • Menu 2
    • Item 2.1
    • Item 2.2

So you have to create a table view with 2 sections. The first section contains 4 rows (Menu 1 and it's items) and the seconds section contains 3 rows (Menu 2 and it's items).

You always set height only for first row in section. And if user clicks on the first row, you expand this section rows by settings the height and reload this section.


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'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.

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section is the better way to provide your "own custom header", as that's exactly what it's designed to do.

For more details, Refer this Answer or this PKCollapsingTableViewSections.

Also, You can get this type of tableviews using setIndentationLevel. Please refer this DemoCode for this example. I think this the best solution for Drop-Down tableviews.

If you want to make a simple header and cell drop down, then please refer STCollapseTableView.

Hope, this is what you're looking for. Any concern get back to me. :)