Expand/collapse section in UITableView in iOS
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 and paste most of the code and just need to do a bit modification on 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):
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)
}
}
}
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.
You have to make your own custom header row and put that as the first row of each section. Subclassing the UITableView
or the headers that are already there will be a pain. Based on the way they work now, I am not sure you can easily get actions out of them. You could set up a cell to LOOK like a header, and setup the tableView:didSelectRowAtIndexPath
to manually expand or collapse the section it is in.
I'd store an array of booleans corresponding the the "expended" value of each of your sections. Then you could 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];
}
}
Then set numberOfRowsInSection
to check the mybooleans
value and return 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;
}
}
Also, you will need to update cellForRowAtIndexPath
to return a custom header cell for the first row in any section.