iOS Swift UITableView Collapse/Expand Sections example

iOS Swift UITableView Collapse/Expand Sections example

Here are 4 easy steps to do it

  1. Keep track of the open section
  2. 
    private var openSection: Int = 0
    
    
  3. Create custom section view using method viewForHeaderInSection so that we can attach a UITapGestureRecognizer
  4. 
        func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            
            //my custom view for the section display
            let sectionView = UIView()
            sectionView.backgroundColor = UIColor(red:0.87, green:0.87, blue:0.87, alpha:1.0)
            
            //set the tag so that we can find the section no later
            sectionView.tag = section
            
            //add a tap gesture so we know when user taps on the section
            let tap = UITapGestureRecognizer(target: self, action:#selector(self.sectionTap(_:)))
            sectionView.addGestureRecognizer(tap)
            
            //add label so that we can display the section text
            let label = UILabel()
            label.translatesAutoresizingMaskIntoConstraints = false
            sectionView.addSubview(label)
            
            //just auto layout stuff
            let views = ["sectionView": view!, "label" : label]
            var allConstraints: [NSLayoutConstraint] = []
            allConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[label(50)]|", options: [], metrics: nil, views: views as [String : Any])
            allConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]|", options: [], metrics: nil, views: views as [String : Any])
            NSLayoutConstraint.activate(allConstraints)
            
            //just some easy indication of expanded and collapsed section using + and -,
            //you can use images icons for better creativity
            if section < companies.count  {
                var prefix = "-"
                if section == openSection {
                    prefix = "+"
                }
                
                //section title
                let sectionName = "\(prefix) \(companies[section].name): Count is \(companies[section].employees.count)"
                label.text = sectionName
                return sectionView
            }
            
            return nil
        }
    
    
  5. Set row count only for section that you want expanded else send ZERO
  6. 
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            
            //hide rows if not the open section
            if section == openSection {
                return companies[section].employees.count
            }
            else {
                return 0
            }
        }
    
    
  7. Then on tap check if the new section is different, if so then expand that one and collapse the other one
  8. 
        @objc func sectionTap(_ sender: UITapGestureRecognizer) {
            
            let newSection = sender.view!.tag
            
            //only do anything if new section is different
            if(newSection != openSection) {
                
                var sectionArray: [Int] = [openSection]
                sectionArray.append(newSection)
                openSection = newSection
                
                //list of original open section and the new section that we need to expand
                let indices: IndexSet = IndexSet(sectionArray)
                //refresh those sections
                myTableView.reloadSections(indices, with: .automatic)
                
            }
        }
    
    

Complete Source for UIViewController to Collapse and Expand Sections


import UIKit

//for parsing json data
struct Company: Codable {
    var name: String
    var employees: [Employee]
}

struct Employee: Codable {
    var name: String
    var salary: Double
}

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    private var myTableView: UITableView!
    private var companies: [Company]!
    
    //keeping track of the current open section
    private var openSection: Int = 0
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        self.navigationItem.title = "Main View"
        
        //get my sample data for loading the table view
        companies = getCompanyData()
        
        //setup my table view
        myTableView = UITableView()
        myTableView.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
        myTableView.dataSource = self
        myTableView.delegate = self
        myTableView.translatesAutoresizingMaskIntoConstraints = false
        myTableView.tableFooterView = UIView()
        self.view.addSubview(myTableView)
        
        //auto layout for the table view
        let views = ["view": view!, "tableView" : myTableView]
        var allConstraints: [NSLayoutConstraint] = []
        allConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[tableView]|", options: [], metrics: nil, views: views as [String : Any])
        allConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[tableView]|", options: [], metrics: nil, views: views as [String : Any])
        NSLayoutConstraint.activate(allConstraints)
        
        
    }
    
    //when user taps on the row
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Company Selected: \(companies[indexPath.section].name)")
        print("Employee Selected: \(companies[indexPath.section].employees[indexPath.row].name)")
    }
    
    //no of sections for the table view
    func numberOfSections(in tableView: UITableView) -> Int {
        return companies.count
    }
    
    //section heading, not used when you implement viewForHeaderInSection
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if section < companies.count  {
            let sectionName = "\(companies[section].name): Count is \(companies[section].employees.count)"
            return sectionName
        }
        
        return nil
    }
    
    //custom view for section header
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        
        //my custom view for the section display
        let sectionView = UIView()
        sectionView.backgroundColor = UIColor(red:0.87, green:0.87, blue:0.87, alpha:1.0)
        
        //set the tag so that we can find the section no later
        sectionView.tag = section
        
        //add a tap gesture so we know when user taps on the section
        let tap = UITapGestureRecognizer(target: self, action:#selector(self.sectionTap(_:)))
        sectionView.addGestureRecognizer(tap)
        
        //add label so that we can display the section text
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        sectionView.addSubview(label)
        
        //just auto layout stuff
        let views = ["sectionView": view!, "label" : label]
        var allConstraints: [NSLayoutConstraint] = []
        allConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[label(50)]|", options: [], metrics: nil, views: views as [String : Any])
        allConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]|", options: [], metrics: nil, views: views as [String : Any])
        NSLayoutConstraint.activate(allConstraints)
        
        //just some easy indication of expanded and collapsed section using + and -,
        //you can use images icons for better creativity
        if section < companies.count  {
            var prefix = "-"
            if section == openSection {
                prefix = "+"
            }
            
            //section title
            let sectionName = "\(prefix) \(companies[section].name): Count is \(companies[section].employees.count)"
            label.text = sectionName
            return sectionView
        }
        
        return nil
    }
    
    //when the user taps on the section
    @objc func sectionTap(_ sender: UITapGestureRecognizer) {
        
        let newSection = sender.view!.tag
        
        //only do anything if new section is different
        if(newSection != openSection) {
            
            var sectionArray: [Int] = [openSection]
            sectionArray.append(newSection)
            openSection = newSection
            
            //list of original open section and the new section that we need to expand
            let indices: IndexSet = IndexSet(sectionArray)
            //refresh those sections
            myTableView.reloadSections(indices, with: .automatic)
            
        }
    }
    
    //number of rows for that section
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        //hide rows if not the open section
        if section == openSection {
            return companies[section].employees.count
        }
        else {
            return 0
        }
    }
    
    //render the cell and display data
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) 
        
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        
        let employee = companies[indexPath.section].employees[indexPath.row]
        let salary = formatter.string(from: NSNumber(value: employee.salary))
        cell.textLabel!.text = "\(employee.name), salary is \(salary ?? "")"
        return cell
    }
    
    
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
    }
    
    //get JSON data into object for display
    func getCompanyData() -> [Company] {
        
        var companies: [Company] = [Company]()
        let myJsonData = """
            [{
                     "name": "ABC Company",
                     "employees": [{
                             "name": "Employee1",
                             "salary": 110000
                         },
                         {
                             "name": "Employee2",
                             "salary": 150000
                         }
                     ]
                 },
                    {
                     "name": "KLM Company",
                     "employees": [{
                             "name": "Employee3",
                             "salary": 170000
                         },
                         {
                             "name": "Employee4",
                             "salary": 190000
                         }
                     ]
                 },
                 {
                     "name": "XYZ Company",
                     "employees": [{
                             "name": "Employee5",
                             "salary": 220000.5
                         },
                         {
                             "name": "Employee6",
                             "salary": 215000.0
                         },
                         {
                             "name": "Employee7",
                             "salary": 2350000
                         }
                     ]
                 }
             ]
            """
        
        //Decode JSON Data String to Swift Objects
        if let jsonData = myJsonData.data(using: .utf8) {
            let decoder = JSONDecoder()
            do {
                companies = try decoder.decode([Company].self, from: jsonData)
            } catch {
                //do nothing
            }
            
            
        }
        
        return companies
        
    }
    
}


No comments:

Post a Comment

NO JUNK, Please try to keep this clean and related to the topic at hand.
Comments are for users to ask questions, collaborate or improve on existing.