Has anyone seen an implementation of an \"accordion\" (maybe called \"animated outline\") view for the iPhone? I found an example project for Cocoa, but before trying a port
Here's the CollapsingTableViewDelegate
class that I'm currently working with to do this. This only works with static table content.
You supply the CollapsingTableCellDelegate
implementation to this class, which must know how to compute the collapsed and expanded sizes of each row, and how to create a UIView
for each row. The view stays the same whether collapsed or expanded, so that the top sliver of each row's view serves as that row's clickable header.
You then make this class the datasource and delegate for your UITableView
.
Header file CollapsingTableViewDelegate.h
:
#import <UIKit/UIKit.h>
@protocol CollapsingTableCellDelegate<NSObject>
@required
- (CGFloat)collapsingCellHeightForRow:(int)row expanded:(BOOL)expanded;
- (UIView *)collapsingCellViewForRow:(int)row;
@optional
- (BOOL)collapsingCellAllowCollapse:(int)row;
@end
struct cell;
@interface CollapsingTableViewDelegate : NSObject <UITableViewDelegate, UITableViewDataSource> {
id<CollapsingTableCellDelegate> cellDelegate;
int numCells;
int currentSelection;
struct cell *cells;
}
@property (nonatomic, retain, readonly) id<CollapsingTableCellDelegate> cellDelegate;
@property (nonatomic, assign, readonly) int numCells;
@property (nonatomic, assign) int currentSelection;
@property (nonatomic, assign, readonly) struct cell *cells;
- (CollapsingTableViewDelegate *)initWithCellDelegate:(id<CollapsingTableCellDelegate>)delegate numCells:(int)numCells;
- (void)tableView:(UITableView *)tableView touchRow:(int)newSelection;
@end
and source file CollapsingTableViewDelegate.m
:
#import "CollapsingTableViewDelegate.h"
@implementation CollapsingTableViewDelegate
struct cell {
u_char expanded;
u_char collapsable;
};
@synthesize cellDelegate;
@synthesize currentSelection;
@synthesize cells;
@synthesize numCells;
#pragma mark -
#pragma mark Setup and Teardown
- (CollapsingTableViewDelegate *)initWithCellDelegate:(id<CollapsingTableCellDelegate>)delegate numCells:(int)num {
if ([super init] == nil)
return nil;
if ((cells = calloc(num, sizeof(*cells))) == NULL) {
[self autorelease];
return nil;
}
cellDelegate = [delegate retain];
numCells = num;
for (int row = 0; row < self.numCells; row++) {
struct cell *const cell = &self.cells[row];
cell->collapsable = ![self.cellDelegate respondsToSelector:@selector(collapsingCellAllowCollapse:)]
|| [self.cellDelegate collapsingCellAllowCollapse:row];
cell->expanded = !cell->collapsable;
}
currentSelection = -1;
return self;
}
- (void)dealloc {
[cellDelegate release];
free(cells);
[super dealloc];
}
- (void)tableView:(UITableView *)tableView reloadRow:(int)row fade:(BOOL)fade {
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:row inSection:0]]
withRowAnimation:fade ? UITableViewRowAnimationFade : UITableViewRowAnimationNone];
}
- (void)tableView:(UITableView *)tableView touchRow:(int)newSelection {
// Sanity check
if (newSelection < -1 || newSelection >= self.numCells) {
NSLog(@"CollapsingTableViewDelegate: invalid row %d not in the range [-1..%d)", newSelection, self.numCells);
return;
}
// Gather info
int oldSelection = self.currentSelection;
BOOL sameCellSelected = newSelection == oldSelection;
struct cell *const oldCell = oldSelection != -1 ? &self.cells[oldSelection] : NULL;
struct cell *const newCell = newSelection != -1 ? &self.cells[newSelection] : NULL;
// Mark old cell as collapsed and new cell as expanded
if (newCell != NULL)
newCell->expanded = TRUE;
if (oldCell != NULL)
oldCell->expanded = FALSE;
self.currentSelection = sameCellSelected ? -1 : newSelection;
// Update table view
if (oldSelection >= newSelection) {
if (oldSelection != -1)
[self tableView:tableView reloadRow:oldSelection fade:sameCellSelected];
if (newSelection != -1 && !sameCellSelected)
[self tableView:tableView reloadRow:newSelection fade:TRUE];
} else {
if (newSelection != -1 && !sameCellSelected)
[self tableView:tableView reloadRow:newSelection fade:TRUE];
if (oldSelection != -1)
[self tableView:tableView reloadRow:oldSelection fade:sameCellSelected];
}
// If expanding a cell, scroll it into view
if (newSelection != -1 && !sameCellSelected) {
[tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:newSelection inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:TRUE];
}
}
#pragma mark -
#pragma mark Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.numCells;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
int row = [indexPath row];
struct cell *const cell = &self.cells[row];
return [self.cellDelegate collapsingCellHeightForRow:row expanded:cell->expanded];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
int row = [indexPath row];
UIView *cellView = [self.cellDelegate collapsingCellViewForRow:row];
[cellView removeFromSuperview];
UITableViewCell *tvcell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil] autorelease];
[tvcell.contentView addSubview:cellView];
tvcell.clipsToBounds = TRUE;
tvcell.selectionStyle = UITableViewCellSelectionStyleNone;
return tvcell;
}
#pragma mark -
#pragma mark Table view delegate
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
int row = [indexPath row];
struct cell *const cell = &self.cells[row];
return cell->collapsable ? indexPath : nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)newSelection {
[tableView deselectRowAtIndexPath:newSelection animated:TRUE];
[self tableView:tableView touchRow:[newSelection row]];
}
@end
Not perfection, but seems to basically work for me.
I would just use a UITableView, make each cell's height depend on whether or not it's "open" and then go from there. It's easy to resize the rows and you could just make the total height for the combined cells be the available height in the UITableView so that it looks like an accordion more than just a table.
This is a quick hack that should work, but in your UITableViewController subclass's .h file:
bool sectionopen[4]; ///or some other way of storing the sections expanded/closed state
And in the .m file put something like:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 4;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (sectionopen[indexPath.row]) {
return 240;///it's open
} else {
return 45;///it's closed
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *mycell = [[[UITableViewCell alloc] init] autorelease];
mycell.textLabel.text= @"Section Name";
return mycell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
///turn them all off
sectionopen[0]=NO;
sectionopen[1]=NO;
sectionopen[2]=NO;
sectionopen[3]=NO;
///open this one
sectionopen[indexPath.row]=YES;
///animate the opening and expand the row
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
This will basically take 4 rows and turn them into collapsable sections where selecting one row will expand it to 240 pixels and collapse all other rows to 40. You can change all of those numbers and figure out the sections and do whatever else you'd like with it.
I've tried this out and it works. You can then complete it by adding the other content to your cell creation code to add whatever you'd like to a section (including possibly a scrolling UITextView if you'd like).
Every solution that I found was using UITableView, which didn't work for me, because I didn't display tabular data. This is why I created AccordionView control. Usage is pretty straightforward:
AccordionView *accordion = [[AccordionView alloc] initWithFrame:CGRectMake(0, 0, 320, 420)];
[self addSubview:accordion];
// Only height is taken into account, so other parameters are just dummy
UIButton *header1 = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 0, 30)];
[header1.titleLabel setText:@"First row"];
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 200)];
// ... add subviews to view1
[accordion addHeader:header1 withView:view1];
// ... add more panels
[accordion setSelectedIndex:0];
In real life it looks like this:
Black bars are headers and you can customize them all you want (I'm using Three20).
I just stumbled across this and found mjdth's solution very straight forward and helpful. However, you might want to use
[self.tableView reloadRowsAtIndexPaths: paths withRowAnimation:UITableViewRowAnimationBottom];
instead of the propoesed reloadSections
method as the reload rows gives you a much smoother transition.
I found this example here if anyone is interested.
http://www.cocoanetics.com/2011/03/expandingcollapsing-tableview-sections/
I found this code: A simple implementation of accordion..
https://github.com/kuon/ios-example-accordion
I hope this will help some one..