Make nested accordion menu with tableView (expandible cells) in swift

1.9k views Asked by At

I have an example of accordion menu like below, and I want o make a nested menu, (e.g. the menu within menu) which is mostly related to expandable tableViews, but I need expandable within expandable tableView or any other solution here is the code from internet that does the single step accordion which I need the nested one: PS: my porject is a bit heavy so I don't want to add other libraries, maybe just a class, thank you very much in advance

//  Created by ingdanni on 05/11/15.
//  Copyright (c) 2015 ManaguaIO. All rights reserved.
//

import UIKit

struct Section {
    let title: String
    let rows: [String]
    var selected: Bool
}

class ViewController: UIViewController {

    let CellIdentifier = "Cell"
    var sections = [Section]()

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup Sections
        sections.append(Section(title: "A", rows: ["1","2","3","4"], selected: false))
        sections.append(Section(title: "B", rows: ["5","6","7","8"], selected: false))
        sections.append(Section(title: "C", rows: ["9","10"], selected: false))

        // Set cell reuse identifier
        self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier)

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

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


    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{

        if sections[section].selected
        {
            return sections[section].rows.count
        }
        return 0
    }

    func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return ""
    }

    func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {

        return 50
    }

    func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 1
    }

    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        if sections[indexPath.section].selected {
            return 50
        }
        return 2
    }

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

        let headerView = UIView(frame: CGRectMake(0, 0, tableView.frame.size.width, 40))

        headerView.backgroundColor = UIColor.lightGrayColor()
        headerView.tag = section

        let headerString = UILabel(frame: CGRect(x: 10, y: 10, width: tableView.frame.size.width-10, height: 30)) as UILabel

        headerString.text = sections[section].title
        headerView.addSubview(headerString)

        let headerTapped = UITapGestureRecognizer(target: self, action:"sectionHeaderTapped:")
        headerView.addGestureRecognizer(headerTapped)

        return headerView
    }

    func sectionHeaderTapped(recognizer: UITapGestureRecognizer) {

        let indexPath = NSIndexPath(forRow: 0, inSection:(recognizer.view?.tag as Int!)!)

        if indexPath.row == 0 {

            sections[indexPath.section].selected = !sections[indexPath.section].selected

            //reload specific section animated
            let range = NSMakeRange(indexPath.section, 1)

            let sectionToReload = NSIndexSet(indexesInRange: range)

            self.tableView.reloadSections(sectionToReload, withRowAnimation:UITableViewRowAnimation.Fade)
        }

    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{

        let cell = self.tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath)
        cell.textLabel?.text = sections[indexPath.section].rows[indexPath.row]
        return cell
    }


}

like this

and schematically like this

1

There are 1 answers

0
flunk On

Actually after struggling a lot on this on how to make nested accordion I found my answer but then decide not to use it for dynamic data from server and JSON web service (e.g when you have nested tree menu which is has dynamic structure from the server) thus I have used each category in one page and define three level and use segues, but If you need to make such menu I found the trick in making an NSObject as for defining properties of each cell here it is:

import UIKit

class AMPGenericObject: NSObject {

    var name:String?
    var parentName:String?
    var canBeExpanded = false // Bool to determine whether the cell can be expanded
    var isExpanded = false    // Bool to determine whether the cell is expanded
    var level:Int?            // Indendation level of tabelview
    var type:Int?
    var children:[AMPGenericObject] = []

    enum ObjectType:Int{
        case OBJECT_TYPE_REGION = 0
        case OBJECT_TYPE_LOCATION
        case OBJECT_TYPE_USERS
    }

}

and here is the viewController:

import UIKit

class AMPTableViewController: UITableViewController {


var dataArray:[AMPGenericObject] = []
var indendationLevel:Int   = 0
var indendationWidth:CGFloat = 20.0

override func viewDidLoad() {
    super.viewDidLoad()

    for var i=0; i<=10; i++ {

    }

    for i in 0..<10 {

        let prod = AMPGenericObject()
        prod.name = "Region \(i)"
        prod.parentName = ""
        prod.isExpanded = false
        prod.level = 0;
        prod.type  = 0;
        // Randomly assign canBeExpanded status
        let rem = i % 2
        if(rem == 0)
        {
            prod.canBeExpanded  = true;
        }
        else
        {
            prod.canBeExpanded = false;
        }
        dataArray.append(prod)

    }
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - Table view data source

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows
    return dataArray.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
    let obj = dataArray[indexPath.row]
    // All optionals are ensured to have values, so we can safely unwrap
    cell.textLabel!.text = obj.name!;
    cell.detailTextLabel!.text = obj.parentName!;
    cell.indentationLevel = obj.level!;
    cell.indentationWidth = indendationWidth;
    // Configure the cell...
    // Show disclosure only if the cell can expand
    if(obj.canBeExpanded)
    {
        cell.accessoryView = self.viewForDisclosureForState(obj.isExpanded)
    }
    else
    {
        //cell.accessoryType = UITableViewCellAccessoryNone;
        cell.accessoryView = nil;
    }

    return cell
}

func viewForDisclosureForState(isExpanded:Bool)->UIView{

    var imageName:String = ""
    if(isExpanded)
    {
        imageName = "ArrowD_blue";
    }
    else
    {
        imageName = "ArrowR_blue";
    }
    let view = UIView(frame: CGRectMake(0, 0, 40, 40))
    let imageView = UIImageView(image: UIImage(named: imageName))
    imageView.frame = CGRectMake(0, 6, 24, 24)
    view.addSubview(imageView)
    return view
}

func fetchChildrenforParent(parentProduct:AMPGenericObject){

    // If canBeExpanded then only we need to create child
    if(parentProduct.canBeExpanded)
    {
        // If Children are already added then no need to add again
        if(parentProduct.children.count > 0){
        return
        }
        // The children property of the parent will be filled with this objects
        // If the parent is of type region, then fetch the location.
        if (parentProduct.type == 0) {
            for i in 0..<10
            {
                let prod = AMPGenericObject()
                prod.name = "Location \(i)"
                prod.level  = parentProduct.level! + 1;
                prod.parentName = "Child \(i) of Level \(prod.level!)"
                // This is used for setting the indentation level so that it look like an accordion view
                prod.type = 1 //OBJECT_TYPE_LOCATION;
                prod.isExpanded = false;

                if(i % 2 == 0)
                {
                    prod.canBeExpanded = true
                }
                else
                {
                    prod.canBeExpanded = false
                }
                parentProduct.children.append(prod)
            }
        }
            // If tapping on Location, fetch the users
        else{

            for i in 0..<10
            {
                let prod = AMPGenericObject()
                prod.name = "User \(i)"
                prod.level  = parentProduct.level! + 1;
                prod.parentName = "Child \(i) of Level \(prod.level!)"
                // This is used for setting the indentation level so that it look like an accordion view
                prod.type = 1 //OBJECT_TYPE_LOCATION;
                prod.isExpanded = false;
                // Users need not expand
                prod.canBeExpanded = false
                parentProduct.children.append(prod)
            }
        }

    }
}

func collapseCellsFromIndexOf(prod:AMPGenericObject,indexPath:NSIndexPath,tableView:UITableView)->Void{

    // Find the number of childrens opened under the parent recursively as there can be expanded children also
    let collapseCol = self.numberOfCellsToBeCollapsed(prod)
    // Find the end index by adding the count to start index+1
    let end = indexPath.row + 1 + collapseCol
    // Find the range from the parent index and the length to be removed.
    let collapseRange =  Range(start: indexPath.row+1, end: end)
    // Remove all the objects in that range from the main array so that number of rows are maintained properly
    dataArray.removeRange(collapseRange)
    prod.isExpanded = false
    // Create index paths for the number of rows to be removed
    var indexPaths = [NSIndexPath]()
    for i in 0..<collapseRange.count {
        indexPaths.append(NSIndexPath.init(forRow: collapseRange.startIndex+i, inSection: 0))
    }
    // Animate and delete
    tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Left)

}

func expandCellsFromIndexOf(prod:AMPGenericObject,indexPath:NSIndexPath,tableView:UITableView)->Void{

    // Create dummy children
    self.fetchChildrenforParent(prod)


    // Expand only if children are available
    if(prod.children.count>0)
    {
        prod.isExpanded = true
        var i = 0;
        // Insert all the child to the main array just after the parent
        for prodData in prod.children {
            dataArray.insert(prodData, atIndex: indexPath.row+i+1)
            i++;
        }
        // Find the range for insertion
        let expandedRange = NSMakeRange(indexPath.row, i)

        var indexPaths = [NSIndexPath]()
        // Create index paths for the range
        for i in 0..<expandedRange.length {
            indexPaths.append(NSIndexPath.init(forRow: expandedRange.location+i+1, inSection: 0))
        }
        // Insert the rows
        tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Left)
    }
}

func numberOfCellsToBeCollapsed(prod:AMPGenericObject)->Int{

    var total = 0

    if(prod.isExpanded)
    {
        // Set the expanded status to no
        prod.isExpanded = false
        let child = prod.children
        total = child.count

        // traverse through all the children of the parent and get the count.
        for prodData in child{

            total += self.numberOfCellsToBeCollapsed(prodData)
        }
    }
    return total
}

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

    let prod = dataArray[indexPath.row]
    let selectedCell = tableView.cellForRowAtIndexPath(indexPath)

    if(prod.canBeExpanded)
    {
        if(prod.isExpanded){
        self.collapseCellsFromIndexOf(prod, indexPath: indexPath, tableView: tableView)
        selectedCell?.accessoryView = self.viewForDisclosureForState(false)
        }
        else{
            self.expandCellsFromIndexOf(prod, indexPath: indexPath, tableView: tableView)
            selectedCell?.accessoryView = self.viewForDisclosureForState(true)
        }
    }

}

}

I specially thanks the user anoopm and I lost the link to repository anyone has it is good to put it in here, thanks by the way for reading this