zetasq
11/20/2015 - 6:33 AM

ZSDoubleCascadeTableView.m

//
//  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