Could somebody tell me the way to perform UITableView expandable/collapsible animations in sections of UITableView as below?

or

Could somebody tell me the way to perform UITableView expandable/collapsible animations in sections of UITableView as below?

or

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.
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 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
I have done the same thing using multiple sections .
class SCTierBenefitsViewController: UIViewController {
@IBOutlet private weak var tblTierBenefits: UITableView!
private var selectedIndexPath: IndexPath?
private var isSelected:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
tblTierBenefits.register(UINib(nibName:"TierBenefitsTableViewCell", bundle: nil), forCellReuseIdentifier:"TierBenefitsTableViewCell")
tblTierBenefits.register(UINib(nibName:"TierBenefitsDetailsCell", bundle: nil), forCellReuseIdentifier:"TierBenefitsDetailsCell")
tblTierBenefits.rowHeight = UITableViewAutomaticDimension;
tblTierBenefits.estimatedRowHeight = 44.0;
tblTierBenefits.tableFooterView = UIView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
extension SCTierBenefitsViewController : UITableViewDataSource{
func numberOfSections(in tableView: UITableView) -> Int {
return 7
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (isSelected && section == selectedIndexPath?.section) ? 2 : 1
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0.01
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let cell:TierBenefitsTableViewCell = tableView.dequeueReusableCell(withIdentifier: "TierBenefitsTableViewCell")! as! TierBenefitsTableViewCell
cell.selectionStyle = .none
cell.contentView.setNeedsLayout()
cell.contentView.layoutIfNeeded()
return cell
case 1:
let cell:TierBenefitsDetailsCell = tableView.dequeueReusableCell(withIdentifier: "TierBenefitsDetailsCell")! as! TierBenefitsDetailsCell
cell.selectionStyle = .none
return cell
default:
break
}
return UITableViewCell()
}
}
extension SCTierBenefitsViewController : UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
if let _selectedIndexPath = selectedIndexPath ,selectedIndexPath?.section == indexPath.section {
tblTierBenefits.beginUpdates()
expandCollapse(indexPath: _selectedIndexPath, isExpand: false)
selectedIndexPath = nil
}
else{
tblTierBenefits.beginUpdates()
if selectedIndexPath != nil {
tblTierBenefits.reloadSections([(selectedIndexPath?.section)!], with: .none)
}
expandCollapse(indexPath: indexPath, isExpand: true)
}
}
}
private func expandCollapse(indexPath: IndexPath?,isExpand: Bool){
isSelected = isExpand
selectedIndexPath = indexPath
tblTierBenefits.reloadSections([(indexPath?.section)!], with: .none)
tblTierBenefits.endUpdates()
}
}
I am adding this solution for completeness and showing how to work with section headers.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet var tableView: UITableView!
var headerButtons: [UIButton]!
var sections = [true, true, true]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
let section0Button = UIButton(type: .detailDisclosure)
section0Button.setTitle("Section 0", for: .normal)
section0Button.addTarget(self, action: #selector(section0Tapped), for: .touchUpInside)
let section1Button = UIButton(type: .detailDisclosure)
section1Button.setTitle("Section 1", for: .normal)
section1Button.addTarget(self, action: #selector(section1Tapped), for: .touchUpInside)
let section2Button = UIButton(type: .detailDisclosure)
section2Button.setTitle("Section 2", for: .normal)
section2Button.addTarget(self, action: #selector(section2Tapped), for: .touchUpInside)
headerButtons = [UIButton]()
headerButtons.append(section0Button)
headerButtons.append(section1Button)
headerButtons.append(section2Button)
}
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section] ? 3 : 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellReuseId = "cellReuseId"
let cell = UITableViewCell(style: .default, reuseIdentifier: cellReuseId)
cell.textLabel?.text = "\(indexPath.section): \(indexPath.row)"
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return headerButtons[section]
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44
}
@objc func section0Tapped() {
sections[0] = !sections[0]
tableView.reloadSections([0], with: .fade)
}
@objc func section1Tapped() {
sections[1] = !sections[1]
tableView.reloadSections([1], with: .fade)
}
@objc func section2Tapped() {
sections[2] = !sections[2]
tableView.reloadSections([2], with: .fade)
}
}
Link to gist: https://gist.github.com/pawelkijowskizimperium/fe1e8511a7932a0d40486a2669316d2c
in support to @jean.timex solution, use below code if you want to open one section at any time. create a variable like: var expandedSection = -1;
func toggleSection(_ header: CollapsibleTableViewHeader, section: Int) {
let collapsed = !sections[section].collapsed
// Toggle collapse
sections[section].collapsed = collapsed
header.setCollapsed(collapsed)
tableView.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)
if (expandedSection >= 0 && expandedSection != section){
sections[expandedSection].collapsed = true
tableView.reloadSections(NSIndexSet(index: expandedSection) as IndexSet, with: .automatic)
}
expandedSection = section;
}