//
// ZSDoubleCascadeTableView.m
//
// Created by ZhuShengqi on 25/9/15.
// Copyright © 2015 9tong. All rights reserved.
//
#import "ZSDoubleCascadeTableView.h"
@interface ZSDoubleCascadeTableView () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) NSMutableArray<NSMutableArray<NSNumber *> *> *expansionStates; // use BOOL to record if a certain parent cell is expanded at given section
@property (nonatomic, strong) NSMutableArray<NSMutableArray<NSNumber *> *> *numberOfChildCells; // use NSInteger to record number of child cells each parent cell hold at given section
@end
@implementation ZSDoubleCascadeTableView
{
struct {
unsigned int shouldSelectParentCell: 1;
unsigned int shouldExpandParentCell: 1;
unsigned int shouldSelectChildCell: 1;
unsigned int willExpandParentCell: 1;
unsigned int willShrinkParentCell: 1;
unsigned int didSelectParentCell: 1;
unsigned int didSelectChildCell: 1;
unsigned int didDeselectParentCell: 1;
unsigned int didDeselectChildCell: 1;
unsigned int scrollViewWillBeginDragging: 1;
} _zsDelegateFlags;
struct {
unsigned int numberOfSections: 1;
unsigned int sectionIndexTitles: 1;
unsigned int titleForHeader: 1;
unsigned int heightForHeader: 1;
unsigned int heightForFooter: 1;
unsigned int heightForParentCell: 1;
unsigned int heightForChildCell: 1;
} _zsDatasourceFlags;
}
- (nonnull instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
if (self = [super initWithFrame:frame style:style]) {
self.delegate = self;
self.dataSource = self;
}
return self;
}
- (NSMutableArray<NSMutableArray<NSNumber *> *> *)expansionStates {
if (!_expansionStates) {
NSInteger numberOfSections = _zsDatasourceFlags.numberOfSections ? [self.zsDatasource numberOfSectionsInTableView:self] : 1;
_expansionStates = [NSMutableArray arrayWithCapacity:numberOfSections];
for (int i = 0; i < numberOfSections; i++) {
NSInteger numberOfParentCells = [self.zsDatasource tableView:self numberOfParentCellsAtSection:i];
NSMutableArray *sectionExpansionState = [NSMutableArray arrayWithCapacity:numberOfParentCells];
for (int j = 0; j < numberOfParentCells; j++) {
[sectionExpansionState addObject:@NO];
}
[_expansionStates addObject:sectionExpansionState];
}
}
return _expansionStates;
}
- (NSMutableArray<NSMutableArray<NSNumber *> *> *)numberOfChildCells {
if (!_numberOfChildCells) {
NSInteger numberOfSections = _zsDatasourceFlags.numberOfSections ? [self.zsDatasource numberOfSectionsInTableView:self] : 1;
_numberOfChildCells = [NSMutableArray arrayWithCapacity:numberOfSections];
for (int i = 0; i < numberOfSections; i++) {
NSInteger numberOfParentCells = [self.zsDatasource tableView:self numberOfParentCellsAtSection:i];
NSMutableArray *childCellsNumberArray = [NSMutableArray arrayWithCapacity:numberOfParentCells];
for (int j = 0; j < numberOfParentCells; j++) {
[childCellsNumberArray addObject:@([self.zsDatasource tableView:self numberOfChildCellsAtParentIndex:j atSection:i])];
}
[_numberOfChildCells addObject:childCellsNumberArray];
}
}
return _numberOfChildCells;
}
- (void)setZsDelegate:(id<ZSDoubleCascadeTableViewDelegate>)zsDelegate {
_zsDelegate = zsDelegate;
_zsDelegateFlags.shouldSelectParentCell = [zsDelegate respondsToSelector:@selector(tableView:shouldSelectParentCellAtParentIndex:atSection:)];
_zsDelegateFlags.shouldExpandParentCell = [zsDelegate respondsToSelector:@selector(tableView:shouldExpandParentCellAtParentIndex:atSection:)];
_zsDelegateFlags.shouldSelectChildCell = [zsDelegate respondsToSelector:@selector(tableView:shouldSelectChildCellAtChildIndex:atParentIndex:atSection:)];
_zsDelegateFlags.willExpandParentCell = [zsDelegate respondsToSelector:@selector(tableView:willExpandParentCell:atParentIndex:atSection:)];
_zsDelegateFlags.willShrinkParentCell = [zsDelegate respondsToSelector:@selector(tableView:willShrinkParentCell:atParentIndex:atSection:)];
_zsDelegateFlags.didSelectParentCell = [zsDelegate respondsToSelector:@selector(tableView:didSelectParentCellAtParentIndex:atSection:)];
_zsDelegateFlags.didSelectChildCell = [zsDelegate respondsToSelector:@selector(tableView:didSelectChildCellAtChildIndex:atParentIndex:atSection:)];
_zsDelegateFlags.didDeselectParentCell = [zsDelegate respondsToSelector:@selector(tableView:didDeselectParentCellAtParentIndex:atSection:)];
_zsDelegateFlags.didDeselectChildCell = [zsDelegate respondsToSelector:@selector(tableView:didDeselectChildCellAtChildIndex:atParentIndex:atSection:)];
_zsDelegateFlags.scrollViewWillBeginDragging = [zsDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)];
}
- (void)setZsDatasource:(id<ZSDoubleCascadeTableViewDatasource>)zsDatasource {
_zsDatasource = zsDatasource;
NSAssert([zsDatasource respondsToSelector:@selector(tableView:numberOfParentCellsAtSection:)], @"zsDatasource doesn't implement selector: tableView:numberOfParentCellsAtSection:");
NSAssert([zsDatasource respondsToSelector:@selector(tableView:numberOfChildCellsAtParentIndex:atSection:)], @"zsDatasource doesn't implement selector: tableView:numberOfChildCellsAtParentIndex:atSection:");
NSAssert([zsDatasource respondsToSelector:@selector(tableView:parentCellAtParentIndex:atSection:isExpanded:)], @"zsDatasource doesn't implement selector: tableView:parentCellAtParentIndex:atSection:isExpanded:");
NSAssert([zsDatasource respondsToSelector:@selector(tableView:childCellAtChildIndex:atParentIndex:atSection:)], @"zsDatasource doesn't implement selector: tableView:childCellAtChildIndex:atParentIndex:atSection:");
_zsDatasourceFlags.numberOfSections = [zsDatasource respondsToSelector:@selector(numberOfSectionsInTableView:)];
_zsDatasourceFlags.sectionIndexTitles = [zsDatasource respondsToSelector:@selector(sectionIndexTitlesForTableView:)];
_zsDatasourceFlags.titleForHeader = [zsDatasource respondsToSelector:@selector(tableView:titleForHeaderInSection:)];
_zsDatasourceFlags.heightForHeader = [zsDatasource respondsToSelector:@selector(tableView:heightForHeaderInSection:)];
_zsDatasourceFlags.heightForFooter = [zsDatasource respondsToSelector:@selector(tableView:heightForFooterInSection:)];
_zsDatasourceFlags.heightForParentCell = [zsDatasource respondsToSelector:@selector(tableView:heightForParentCellAtParentIndex:atSection:)];
_zsDatasourceFlags.heightForChildCell = [zsDatasource respondsToSelector:@selector(tableView:heightForChildCellAtChildIndex:atParentIndex:atSection:)];
}
- (void)reloadWithPreviousCascadeState {
[self reloadData];
}
- (void)reloadData {
[self reloadWithCompactState];
}
- (void)reloadWithCompactState {
NSInteger numberOfSections = _zsDatasourceFlags.numberOfSections ? [self.zsDatasource numberOfSectionsInTableView:self] : 1;
_expansionStates = [NSMutableArray arrayWithCapacity:numberOfSections];
for (int i = 0; i < numberOfSections; i++) {
NSInteger numberOfParentCells = [self.zsDatasource tableView:self numberOfParentCellsAtSection:i];
NSMutableArray *sectionExpansionState = [NSMutableArray arrayWithCapacity:numberOfParentCells];
for (int j = 0; j < numberOfParentCells; j++) {
[sectionExpansionState addObject:@NO];
}
[_expansionStates addObject:sectionExpansionState];
}
_numberOfChildCells = [NSMutableArray arrayWithCapacity:numberOfSections];
for (int i = 0; i < numberOfSections; i++) {
NSInteger numberOfParentCells = [self.zsDatasource tableView:self numberOfParentCellsAtSection:i];
NSMutableArray *childCellsNumberArray = [NSMutableArray arrayWithCapacity:numberOfParentCells];
for (int j = 0; j < numberOfParentCells; j++) {
[childCellsNumberArray addObject:@([self.zsDatasource tableView:self numberOfChildCellsAtParentIndex:j atSection:i])];
}
[_numberOfChildCells addObject:childCellsNumberArray];
}
[super reloadData];
}
- (void)selectParentCellAtParentIndex:(NSInteger)parentIndex atSection:(NSInteger)section animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition {
if (self.numberOfChildCells[section][parentIndex].integerValue > 0) {
return;
}
NSInteger row = -1;
for (int i = 0; i < parentIndex; i++) {
row += 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
}
row += 1;
[self selectRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section] animated:animated scrollPosition:scrollPosition];
}
- (void)deselectParentCellAtParentIndex:(NSInteger)parentIndex atSection:(NSInteger)section animated:(BOOL)animated {
NSInteger row = -1;
for (int i = 0; i < parentIndex; i++) {
row += 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
}
row += 1;
[self deselectRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section] animated:animated];
}
- (void)selectChildCellAtChildIndex:(NSInteger)childIndex atParentIndex:(NSInteger)parentIndex atSection:(NSInteger)section animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition {
NSInteger row = -1;
for (int i = 0; i < parentIndex; i++) {
row += 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
}
row += 1 + (childIndex + 1);
[self selectRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section] animated:animated scrollPosition:scrollPosition];
}
- (void)deselectChildCellAtChildIndex:(NSInteger)childIndex atParentIndex:(NSInteger)parentIndex atSection:(NSInteger)section animated:(BOOL)animated {
NSInteger row = -1;
for (int i = 0; i < parentIndex; i++) {
row += 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
}
row += 1 + (childIndex + 1);
[self deselectRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section] animated:animated];
}
#pragma mark - UIScrollView Delegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if (_zsDelegateFlags.scrollViewWillBeginDragging) {
[self.zsDelegate scrollViewWillBeginDragging:self];
}
}
#pragma mark - UITableView Delegate
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
NSInteger section = indexPath.section;
NSInteger startIndex = -1;
for (int i = 0; i < self.expansionStates[section].count; i++) {
NSInteger newStartIndex = startIndex + 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
if (newStartIndex >= row) {
if (row == startIndex + 1) { // this means we need a parent cell
if (self.numberOfChildCells[section][i].integerValue) { // this parent cell can be expanded
if (!(self.expansionStates[section][i].boolValue)) { // not yet expanded
if (_zsDelegateFlags.shouldExpandParentCell) { // ask delegate for permission
if ([self.zsDelegate tableView:self shouldExpandParentCellAtParentIndex:i atSection:section]) {
return indexPath;
} else {
return nil;
}
} else {
return indexPath;
}
} else { // can shrink because it's already expanded
return indexPath;
}
} else {
if (_zsDelegateFlags.shouldSelectParentCell) { // check if the parent cell can be selected directly
if ([self.zsDelegate tableView:self shouldSelectParentCellAtParentIndex:i atSection:section]) {
return indexPath;
} else {
return nil;
}
} else {
return nil;
}
return nil;
}
} else { // this means we need a child cell
if (_zsDelegateFlags.shouldSelectChildCell) {
if ([self.zsDelegate tableView:self shouldSelectChildCellAtChildIndex:row - startIndex - 2 atParentIndex:i atSection:section]) {
return indexPath;
} else {
return nil;
}
} else {
return indexPath;
}
}
} else {
startIndex = newStartIndex;
}
}
NSAssert(false, @"Wrong IndexPath:row:%ld section:%ld for ZSDoubleCascadeTableView, caller: willSelectRowAtIndexPath", (long)indexPath.row, (long)indexPath.section);
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
NSInteger section = indexPath.section;
NSInteger startIndex = -1;
for (int i = 0; i < self.expansionStates[section].count; i++) {
NSInteger newStartIndex = startIndex + 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
if (newStartIndex >= row) {
if (row == startIndex + 1) { // this means we need a parent cell
if (self.numberOfChildCells[section][i].integerValue > 0) { // check if the parent cell can be expanded
if (self.expansionStates[section][i].boolValue) { // already expanded, now shrink
if (_zsDelegateFlags.willShrinkParentCell) {
[self.zsDelegate tableView:self willShrinkParentCell:[self cellForRowAtIndexPath:indexPath] atParentIndex:i atSection:section];
}
[self deselectRowAtIndexPath:indexPath animated:NO]; // prevent seperator line dissappearance
self.expansionStates[section][i] = @NO;
NSInteger numberOfRowsToDelete = self.numberOfChildCells[section][i].integerValue;
NSMutableArray<NSIndexPath *> *rowsToDelete = [NSMutableArray arrayWithCapacity:numberOfRowsToDelete];
for (int j = 0; j < numberOfRowsToDelete; j++) {
[rowsToDelete addObject:[NSIndexPath indexPathForRow:row + j + 1 inSection:section]];
}
[self deleteRowsAtIndexPaths:rowsToDelete withRowAnimation:UITableViewRowAnimationFade];
return;
} else { // already shrinked, now expand
if (_zsDelegateFlags.willExpandParentCell) {
[self.zsDelegate tableView:self willExpandParentCell:[self cellForRowAtIndexPath:indexPath] atParentIndex:i atSection:section];
}
[self deselectRowAtIndexPath:indexPath animated:NO]; // prevent seperator line dissappearance
self.expansionStates[section][i] = @YES;
NSInteger numberOfRowsToInsert = self.numberOfChildCells[section][i].integerValue;
NSMutableArray<NSIndexPath *> *rowsToInsert = [NSMutableArray arrayWithCapacity:numberOfRowsToInsert];
for (int j = 0; j < numberOfRowsToInsert; j++) {
[rowsToInsert addObject:[NSIndexPath indexPathForRow:row + j + 1 inSection:section]];
}
[self insertRowsAtIndexPaths:rowsToInsert withRowAnimation:UITableViewRowAnimationFade];
return;
}
} else {
if (_zsDelegateFlags.didSelectParentCell) {
[self.zsDelegate tableView:self didSelectParentCellAtParentIndex:i atSection:section];
}
return;
}
} else { // this means we need a child cell
if (_zsDelegateFlags.didSelectChildCell) {
[self.zsDelegate tableView:self didSelectChildCellAtChildIndex:row - startIndex - 2 atParentIndex:i atSection:section];
}
return;
}
} else {
startIndex = newStartIndex;
}
}
NSAssert(false, @"Wrong IndexPath:row: %ld section: %ld for ZSDoubleCascadeTableView caller: didSelectRowAtIndexPath", (long)indexPath.row, (long)indexPath.section);
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
NSInteger section = indexPath.section;
NSInteger startIndex = -1;
for (int i = 0; i < self.expansionStates[section].count; i++) {
NSInteger newStartIndex = startIndex + 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
if (newStartIndex >= row) {
if (row == startIndex + 1) { // this means we need a parent cell
if (_zsDelegateFlags.didDeselectParentCell) {
[self.zsDelegate tableView:self didDeselectParentCellAtParentIndex:i atSection:section];
}
return;
} else { // this means we need a child cell
if (_zsDelegateFlags.didDeselectChildCell) {
[self.zsDelegate tableView:self didDeselectChildCellAtChildIndex:row - startIndex - 2 atParentIndex:i atSection:section];
}
return;
}
} else {
startIndex = newStartIndex;
}
}
NSAssert(false, @"Wrong IndexPath:row: %ld section: %ld for ZSDoubleCascadeTableView caller: didDeselectRowAtIndexPath", (long)indexPath.row, (long)indexPath.section);
}
#pragma mark - UITableView Datasource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if (_zsDatasourceFlags.numberOfSections) {
return [self.zsDatasource numberOfSectionsInTableView:self];
} else {
return 1;
}
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (_zsDatasourceFlags.titleForHeader) {
return [self.zsDatasource tableView:self titleForHeaderInSection:section];
} else {
return nil;
}
}
- (NSArray<NSString *> * _Nullable)sectionIndexTitlesForTableView:(UITableView * _Nonnull)tableView {
if (_zsDatasourceFlags.sectionIndexTitles) {
return [self.zsDatasource sectionIndexTitlesForTableView:self];
} else {
return nil;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (_zsDatasourceFlags.heightForHeader) {
return [self.zsDatasource tableView:self heightForHeaderInSection:section];
} else {
return 0;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
if (_zsDatasourceFlags.heightForFooter) {
return [self.zsDatasource tableView:self heightForFooterInSection:section];
} else {
return 0;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger numberOfRows = self.expansionStates[section].count;
for (int i = 0; i < self.expansionStates[section].count; i++) {
if (self.expansionStates[section][i].boolValue) {
numberOfRows += self.numberOfChildCells[section][i].integerValue;
}
}
return numberOfRows;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (!(_zsDatasourceFlags.heightForChildCell) && !(_zsDatasourceFlags.heightForParentCell)) {
return 44;
}
NSInteger row = indexPath.row;
NSInteger section = indexPath.section;
NSInteger startIndex = -1;
for (int i = 0; i < self.expansionStates[section].count; i++) {
NSInteger newStartIndex = startIndex + 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
if (newStartIndex >= row) {
if (row == startIndex + 1) { // this means we need a parent cell
if (_zsDatasourceFlags.heightForParentCell) {
return [self.zsDatasource tableView:self heightForParentCellAtParentIndex:i atSection:section];
} else {
return 44;
}
} else { // this means we need a child cell
if (_zsDatasourceFlags.heightForChildCell) {
return [self.zsDatasource tableView:self heightForChildCellAtChildIndex:row - startIndex - 2 atParentIndex:i atSection:section];
} else {
return 44;
}
}
} else {
startIndex = newStartIndex;
}
}
NSAssert(false, @"Wrong IndexPath:row: %ld section: %ld for ZSDoubleCascadeTableView caller: heightForRowAtIndexPath", (long)indexPath.row, (long)indexPath.section);
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
NSInteger section = indexPath.section;
NSInteger startIndex = -1;
for (int i = 0; i < self.expansionStates[section].count; i++) {
NSInteger newStartIndex = startIndex + 1 + self.expansionStates[section][i].boolValue * self.numberOfChildCells[section][i].integerValue;
if (newStartIndex >= row) {
if (row == startIndex + 1) { // this means we need a parent cell
return [self.zsDatasource tableView:self parentCellAtParentIndex:i atSection:section isExpanded:self.expansionStates[section][i].boolValue];
} else { // this means we need a child cell
return [self.zsDatasource tableView:self childCellAtChildIndex:row - startIndex - 2 atParentIndex:i atSection:section];
}
} else {
startIndex = newStartIndex;
}
}
NSAssert(false, @"Wrong IndexPath:row: %ld section: %ld for ZSDoubleCascadeTableView caller: cellForRowAtIndexPath", (long)indexPath.row, (long)indexPath.section);
return nil;
}
@end