Created
November 20, 2015 06:33
-
-
Save zetasq/dc9a9029d206359a2e81 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment