jerry-sl
3/12/2015 - 7:23 AM

AGCommand.h

#import "NSObject+AGCommands.h"
#import <objc/runtime.h>

@implementation NSObject (AGCommands)

#pragma mark - Public API

- (BOOL)canPerformCommand:(AGCommand *)command {
    return [self respondsToSelector:command.action];
}

- (BOOL)shouldPerformCommand:(AGCommand *)command {
    return YES;
}

- (NSObject *)targetForCommand:(AGCommand *)command {
    if (!command) return nil;
    
    NSObject *implementor = self;
    while (implementor) {
        if ([implementor canPerformCommand:command] == YES) {
            return implementor;
        }
        implementor = [implementor nextCommandResponder];
    }
    return nil;
}

- (BOOL)sendCommand:(AGCommand *)command {
    if (!command) return NO;
    if (command.firingObject == nil) {
        command.firingObject = self;
    }
    
    NSObject *target = [self targetForCommand:command];
    while (target) {
        if ([target shouldPerformCommand:command]) {
            [self performCommand:command onResponder:target];
            return YES;
        }
        target = [target.nextCommandResponder targetForCommand:command];
    }
    
    return NO;
}

#pragma mark - Properties

static void *const kNextCommandResponderKey = (void *)&kNextCommandResponderKey;
- (NSObject *)nextCommandResponder {
    NSObject *responder = objc_getAssociatedObject(self, kNextCommandResponderKey);
    if (responder) return responder;
    if ([self respondsToSelector:@selector(nextResponder)]) {
        return [self performSelector:@selector(nextResponder)];
    }
    return nil;
}

- (void)setNextCommandResponder:(NSObject *)object {
    objc_setAssociatedObject(self, kNextCommandResponderKey, object, OBJC_ASSOCIATION_ASSIGN);
}

#pragma mark - Helpers

- (void)performCommand:(AGCommand *)command onResponder:(NSObject *)responder {
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[responder class] instanceMethodSignatureForSelector:command.action]];
    [invocation setSelector:command.action];
    [invocation setTarget:responder];
    [invocation setArgument:&command atIndex:2];
    [invocation invoke];
}

@end

@implementation UIViewController (AGCommands)
- (NSObject *)nextCommandResponder {
    NSObject *nextResponder = [super nextCommandResponder];
    if (nextResponder) return nextResponder;
    return self.navigationController;
}
@end
@interface NSObject (AGCommands)

@property (nonatomic, assign) NSObject *nextCommandResponder;

- (BOOL)canPerformCommand:(AGCommand *)command;
- (BOOL)shouldPerformCommand:(AGCommand *)command;
- (NSObject *)targetForCommand:(AGCommand *)command;
- (BOOL)sendCommand:(AGCommand *)command;

@end
#import "AGDataSource.h"

@interface AGDataSource()

@property (nonatomic, strong) NSMutableDictionary *collectionCellClassNibs;
@property (nonatomic, strong) NSCache *cachedValues;
@property (nonatomic, assign) CGFloat averageHeight;
@property (nonatomic, strong) NSMutableDictionary *tableCellClassNibs;

@end


static NSString *undefinedObjectClass = @"NSObject";
static NSInteger collectionViewHeaderTag = 100;
static NSInteger collectionViewFooterTag = 101;

@implementation AGDataSource

- (id)init {
    self = [super init];
    if (self) {
        [self setupGenericDataSource];
    }
    return self;
}

- (void)awakeFromNib {
    [super awakeFromNib];
    [self setupGenericDataSource];
}

- (void)dealloc {
    //Unsafe delegates and datasource should be nullified manually (non-weak)
    if (self.tableView.dataSource == self) {
        self.tableView.dataSource = nil;
    }
    if (self.tableView.delegate == self) {
        self.tableView.delegate = nil;
    }
    if (self.collectionView.delegate == self) {
        self.collectionView.delegate = nil;
    }
    if (self.collectionView.dataSource == self) {
        self.collectionView.dataSource = nil;
    }
}

- (void)setupGenericDataSource {
    _tableCellClassNibs = [NSMutableDictionary dictionary];
    _tableCellClassNibs[undefinedObjectClass] = [UITableViewCell class];
    _collectionCellClassNibs = [NSMutableDictionary dictionary];
    _collectionCellClassNibs[undefinedObjectClass] = [UICollectionViewCell class];
    _cachedValues = [[NSCache alloc] init];
}


- (void)setSectionObjects:(NSArray *)sectionObjects {
    if ([_sectionObjects isEqualToArray:sectionObjects]) return;
    _sectionObjects = sectionObjects;
}

- (void)setTableView:(UITableView *)tableView {
    _tableView = tableView;
    _tableView.delegate = self;
    _tableView.dataSource = self;
}

- (void)setCollectionView:(UICollectionView *)collectionView {
    _collectionView = collectionView;
    _collectionView.delegate = self;
    _collectionView.dataSource = self;
    
    //Register headers and footers
    [_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"HeaderView"];
    [_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"FooterView"];
    
    //Add previously registered classes
    for (NSString *key in self.collectionCellClassNibs) {
        id classNib = self.collectionCellClassNibs[key];
        if ([classNib isKindOfClass:[UINib class]]) {
            [_collectionView registerNib:classNib forCellWithReuseIdentifier:key];
        }
        else {
            [_collectionView registerClass:classNib forCellWithReuseIdentifier:key];
        }
    }
}

- (void)reloadWithData:(NSArray *)sectionObjects {
    self.sectionObjects = sectionObjects;
    [self reloadData];
}

- (void)updateWithData:(NSArray *)sectionObjects {
    [self updateWithData:sectionObjects animation:UITableViewRowAnimationAutomatic];
}

- (void)updateWithData:(NSArray *)sectionObjects animation:(UITableViewRowAnimation)animation {
    if (self.tableView.window) {
        
        //For different amount of sections just refresh
        if ([sectionObjects count] != [self.sectionObjects count]) {
            [self reloadWithData:sectionObjects];
            return;
        }

        NSArray *oldSections = self.sectionObjects;
        self.sectionObjects = sectionObjects;
        sectionObjects = self.sectionObjects;
    
        [self.tableView beginUpdates];
        
        for (NSInteger s = 0; s < [sectionObjects count]; s++) {
            NSMutableArray *removeIndexes = [NSMutableArray array];
            NSMutableArray *insertIndexes = [NSMutableArray array];

            NSArray *section = sectionObjects[s];
            NSArray *oldSection = oldSections[s];
            NSMutableArray *modifiedSection = [oldSection mutableCopy];
            
            //Removed non existing objects
            for (NSInteger r = 0; r < [oldSection count]; r++) {
                if (![section containsObject:oldSection[r]]) {
                    [removeIndexes addObject:[NSIndexPath indexPathForRow:r inSection:s]];
                    [modifiedSection removeObject:oldSection[r]];
                }
            }
            [self.tableView deleteRowsAtIndexPaths:removeIndexes withRowAnimation:animation];

            //Add new objects
            for (NSInteger r = 0; r < [section count]; r++) {
                if (![modifiedSection containsObject:section[r]]) {
                    [insertIndexes addObject:[NSIndexPath indexPathForRow:r inSection:s]];
                    [modifiedSection insertObject:section[r] atIndex:r];
                }
            }
            [self.tableView insertRowsAtIndexPaths:insertIndexes withRowAnimation:animation];

            //Move objects
            for (NSInteger r = 0; r < [modifiedSection count]; r++) {
                id obj = modifiedSection[r];
                if (![obj isEqual:section[r]]) {
                    NSInteger newPos = [section indexOfObject:obj];
                    if (newPos != NSNotFound) {
                        [self.tableView moveRowAtIndexPath:[NSIndexPath indexPathForRow:r inSection:s] toIndexPath:[NSIndexPath indexPathForRow:newPos inSection:s]];
                    }
                }
            }
        }        
        [self.tableView endUpdates];
    }
    else {
        [self reloadWithData:sectionObjects];
    }
}

- (void)reloadData {
    self.averageHeight = (self.tableView.rowHeight > 0? self.tableView.rowHeight : 44);
    [self.cachedValues removeAllObjects];
    [self.tableView reloadData];
    [self.collectionView reloadData];
}

- (NSArray *)objectsAtSection:(NSInteger)section {
    if (section >= 0 && [self.sectionObjects count] > section) {
        return self.sectionObjects[section];
    }
    return nil;
}

- (id)objectAtIndexPath:(NSIndexPath *)indexPath {
    NSArray *objects = [self objectsAtSection:indexPath.section];
    if (indexPath.row >= 0 && [objects count] > indexPath.row) {
        return objects[indexPath.row];
    }
    return nil;
}

- (NSIndexPath *)indexPathOfObject:(id)object {
    if (!object) return nil;
    for (NSInteger section = 0; section < [self.sectionObjects count]; section++) {
        NSArray *objects = [self objectsAtSection:section];
        NSInteger row = [objects indexOfObject:object];
        if (row != NSNotFound) {
            return [NSIndexPath indexPathForRow:row inSection:section];
        }
    }
    return nil;
}

- (void)registerTableCellClass:(Class)cellClass forObjectClass:(Class)objectClass {
    NSString *key = NSStringFromClass(objectClass)? : undefinedObjectClass;
    self.tableCellClassNibs[key] = cellClass;
    
    NSString *cachedValueKey = [NSString stringWithFormat:@"height-table-cell-%@", NSStringFromClass(objectClass)];
    [self.cachedValues removeObjectForKey:cachedValueKey];
}

- (void)registerTableCellNib:(UINib *)cellNib forObjectClass:(Class)objectClass {
    NSString *key = NSStringFromClass(objectClass)? : undefinedObjectClass;
    self.tableCellClassNibs[key] = cellNib;
    
    NSString *cachedValueKey = [NSString stringWithFormat:@"height-table-cell-%@", NSStringFromClass(objectClass)];
    [self.cachedValues removeObjectForKey:cachedValueKey];
}

- (void)registerCollectionCellClass:(Class)cellClass forObjectClass:(Class)objectClass {
    NSString *key = NSStringFromClass(objectClass)? : undefinedObjectClass;
    self.collectionCellClassNibs[key] = cellClass;
    [self.collectionView registerClass:cellClass forCellWithReuseIdentifier:key];
    
    NSString *cachedValueKey = [NSString stringWithFormat:@"size-grid-cell-%@", NSStringFromClass(objectClass)];
    [self.cachedValues removeObjectForKey:cachedValueKey];
}

- (void)registerCollectionCellNib:(UINib *)cellNib forObjectClass:(Class)objectClass {
    NSString *key = NSStringFromClass(objectClass)? : undefinedObjectClass;
    self.collectionCellClassNibs[key] = cellNib;
    [self.collectionView registerNib:cellNib forCellWithReuseIdentifier:key];
    
    NSString *cachedValueKey = [NSString stringWithFormat:@"size-grid-cell-%@", NSStringFromClass(objectClass)];
    [self.cachedValues removeObjectForKey:cachedValueKey];
}

- (id)registeredTableCellNibOrClassForObject:(id)object {
    Class cls = [object class];
    while (cls) {
        id registeredNibOrClass = self.tableCellClassNibs[NSStringFromClass(cls)];
        if (registeredNibOrClass) return registeredNibOrClass;
        cls = [cls superclass];
    }
    return self.tableCellClassNibs[undefinedObjectClass]? : [UITableViewCell class];
}

#pragma mark - AGDataSourceProtocol proxies

- (UITableViewCell *)dataSource:(AGDataSource *)dataSource createTableCellWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath {
    NSString *cellIdentifier = NSStringFromClass([object class]) ?: @"Cell";
    if (self.useIndexAsReuseIdentifier) {
        cellIdentifier = [cellIdentifier stringByAppendingFormat:@"-%ld-%ld", (long)indexPath.section, (long)indexPath.row];
    }
    
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        id classNib = [self registeredTableCellNibOrClassForObject:object];
        if ([classNib isKindOfClass:[UINib class]]) {
            NSArray *nibContents = [classNib instantiateWithOwner:self options:nil];
            if (([nibContents count]) && ([nibContents[0] isKindOfClass:[UITableViewCell class]])) {
                cell = nibContents[0];
                [cell setValue:cellIdentifier forKey:@"reuseIdentifier"];
            }
        }
        else {
            cell = [[classNib alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
        }
    }
    return cell;
}

- (UICollectionViewCell *)dataSource:(AGDataSource *)dataSource createCollectionCellWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath {
    NSString *cellIdentifier = NSStringFromClass([object class]);
    if (self.collectionCellClassNibs[cellIdentifier] == nil) {
        cellIdentifier = undefinedObjectClass;
    }
    UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
    return cell;
}

- (void)dataSource:(AGDataSource *)dataSource configureCell:(id)cell withObject:(id)object forIndexPath:(NSIndexPath *)indexPath {
    if ([cell conformsToProtocol:@protocol(AGDataSource)]) {
        [cell setObject:object];
    }
    else if ([cell respondsToSelector:@selector(textLabel)]){
        [[cell textLabel] setText:[object description]];
    }
}

- (void)dataSource:(AGDataSource *)dataSource didSelectCellWithOject:(id)object atIndexPath:(NSIndexPath *)indexPath {
    AGCommand *command = nil;
    if (self.tableView) {
        [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
        UITableViewCell<AGCellProtocol> *cell = (UITableViewCell<AGCellProtocol> *)[self.tableView cellForRowAtIndexPath:indexPath];
        if ([cell respondsToSelector:@selector(didSelectCommand)]) {
            command = [cell didSelectCommand];
        }
    }
    else {
        [self.collectionView deselectItemAtIndexPath:indexPath animated:YES];
        UICollectionViewCell<AGCellProtocol> *cell = (UICollectionViewCell<AGCellProtocol> *)[self.collectionView cellForItemAtIndexPath:indexPath];
        if ([cell respondsToSelector:@selector(didSelectCommand)]) {
            command = [cell didSelectCommand];
        }
    }
    
    if (!command) {
        command = [AGCommand factoryCommandWithObject:object];
    }
    [self.tableView sendCommand:command];
    [self.collectionView sendCommand:command];
}

- (UIView *)dataSource:(AGDataSource *)dataSource viewForHeaderInSection:(NSInteger)section {
    return nil;
}

- (UIView *)dataSource:(AGDataSource *)dataSource viewForFooterInSection:(NSInteger)section {
    return nil;
}

- (CGFloat)dataSource:(AGDataSource *)dataSource tableCellHeightWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath {
    
    //Fetch from cache first
    NSString *cachedValueKey = [NSString stringWithFormat:@"height-table-cell-%@", NSStringFromClass([object class])];
    if ([self.cachedValues objectForKey:cachedValueKey]) {
        return [[self.cachedValues objectForKey:cachedValueKey] floatValue];
    }
    
    //Fetch from cell size
    UITableViewCell *cell = nil;
    if ([self.delegate respondsToSelector:@selector(dataSource:createTableCellWithObject:forIndexPath:)]) {
        cell = [self.delegate dataSource:self createTableCellWithObject:object forIndexPath:indexPath];
    }
    else {
        cell = [self dataSource:self createTableCellWithObject:object forIndexPath:indexPath];
    }
    CGFloat height = cell.frame.size.height;
    [self.cachedValues setObject:@(height) forKey:cachedValueKey];
    return height;
}

- (CGSize)dataSource:(AGDataSource *)dataSource collectionCellSizeWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath {
    
    //Fetch from cache first
    NSString *cachedValueKey = [NSString stringWithFormat:@"size-grid-cell-%@", NSStringFromClass([object class])];
    if ([self.cachedValues objectForKey:cachedValueKey]) {
        return [[self.cachedValues objectForKey:cachedValueKey] CGSizeValue];
    }
    
    //Fetch from cell size
    UICollectionViewCell *cell = nil;
    
    NSString *objectClass = NSStringFromClass([object class]);
    id classNib = self.collectionCellClassNibs[objectClass]? : self.collectionCellClassNibs[undefinedObjectClass];
    if ([classNib isKindOfClass:[UINib class]]) {
        NSArray *nibContents = [classNib instantiateWithOwner:self options:nil];
        if (([nibContents count]) && ([nibContents[0] isKindOfClass:[UICollectionViewCell class]])) {
            cell = nibContents[0];
        }
    }
    else {
        cell = [[classNib alloc] init];
    }
    [self.cachedValues setObject:[NSValue valueWithCGSize:cell.frame.size] forKey:cachedValueKey];
    return cell.frame.size;
}

#pragma mark - UITableViewDataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [self.sectionObjects count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [[self objectsAtSection:section] count];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    id object = [self objectAtIndexPath:indexPath];
    
    //Create cell
    UITableViewCell *cell = nil;
    if ([self.delegate respondsToSelector:@selector(dataSource:createTableCellWithObject:forIndexPath:)]) {
        cell = [self.delegate dataSource:self createTableCellWithObject:object forIndexPath:indexPath];
    }
    else {
        cell = [self dataSource:self createTableCellWithObject:object forIndexPath:indexPath];
    }
    
    //Configure cell
    if ([self.delegate respondsToSelector:@selector(dataSource:configureCell:withObject:forIndexPath:)]) {
        [self.delegate dataSource:self configureCell:cell withObject:object forIndexPath:indexPath];
    }
    else {
        [self dataSource:self configureCell:cell withObject:object forIndexPath:indexPath];
    }
    return cell;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    id object = [self objectAtIndexPath:indexPath];
    if ([self.delegate respondsToSelector:@selector(dataSource:didSelectCellWithOject:atIndexPath:)]) {
        [self.delegate dataSource:self didSelectCellWithOject:object atIndexPath:indexPath];
    }
    else {
        [self dataSource:self didSelectCellWithOject:object atIndexPath:indexPath];
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat height = 0;
    id object = [self objectAtIndexPath:indexPath];
    if ([self.delegate respondsToSelector:@selector(dataSource:tableCellHeightWithObject:forIndexPath:)]) {
        height = [self.delegate dataSource:self tableCellHeightWithObject:object forIndexPath:indexPath];
    }
    else {
        height = [self dataSource:self tableCellHeightWithObject:object forIndexPath:indexPath];
    }
    self.averageHeight += (height - self.averageHeight) * .3;
    return height;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    id object = [self objectAtIndexPath:indexPath];
    NSString *cachedValueKey = [NSString stringWithFormat:@"height-table-cell-%@", NSStringFromClass([object class])];
    if ([self.cachedValues objectForKey:cachedValueKey]) {
        return [[self.cachedValues objectForKey:cachedValueKey] floatValue];
    }
    return [self tableView:tableView heightForRowAtIndexPath:indexPath];
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    if ([self.delegate respondsToSelector:@selector(dataSource:viewForHeaderInSection:)]) {
        return [self.delegate dataSource:self viewForHeaderInSection:section];
    }
    else {
        return [self dataSource:self viewForHeaderInSection:section];
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    UIView *header = [self tableView:tableView viewForHeaderInSection:section];
    if (header) return header.frame.size.height;
    return UITableViewAutomaticDimension;
}

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
    if ([self.delegate respondsToSelector:@selector(dataSource:viewForFooterInSection:)]) {
        return [self.delegate dataSource:self viewForFooterInSection:section];
    }
    else {
        return [self dataSource:self viewForFooterInSection:section];
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
    UIView *footer = [self tableView:tableView viewForFooterInSection:section];
    if (footer) return footer.frame.size.height;
    return UITableViewAutomaticDimension;
}


#pragma mark - UICollectionViewDataSource

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return [self numberOfSectionsInTableView:nil];
}

// Populating subview items
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return [self tableView:nil numberOfRowsInSection:section];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    id object = [self objectAtIndexPath:indexPath];
    
    //Create cell
    UICollectionViewCell *cell = nil;
    if ([self.delegate respondsToSelector:@selector(dataSource:createCollectionCellWithObject:forIndexPath:)]) {
        cell = [self.delegate dataSource:self createCollectionCellWithObject:object forIndexPath:indexPath];
    }
    else {
        cell = [self dataSource:self createCollectionCellWithObject:object forIndexPath:indexPath];
    }
    
    //Configure cell
    if ([self.delegate respondsToSelector:@selector(dataSource:configureCell:withObject:forIndexPath:)]) {
        [self.delegate dataSource:self configureCell:cell withObject:object forIndexPath:indexPath];
    }
    else {
        [self dataSource:self configureCell:cell withObject:object forIndexPath:indexPath];
    }
    
    return cell;
}

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
        UIView *view = nil;
        if ([self.delegate respondsToSelector:@selector(dataSource:viewForHeaderInSection:)]) {
            view = [self.delegate dataSource:self viewForHeaderInSection:indexPath.section];
        }
        else {
            view = [self dataSource:self viewForHeaderInSection:indexPath.section];
        }
        if (!view) return nil;
        
        UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"HeaderView" forIndexPath:indexPath];
        
        [[headerView viewWithTag:collectionViewHeaderTag] removeFromSuperview];
        view.frame = headerView.bounds;
        view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [view setTag:collectionViewHeaderTag];
        [headerView addSubview:view];
        
        return headerView;
    }
    if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
        UIView *view = nil;
        if ([self.delegate respondsToSelector:@selector(dataSource:viewForFooterInSection:)]) {
            view = [self.delegate dataSource:self viewForFooterInSection:indexPath.section];
        }
        else {
            view = [self dataSource:self viewForFooterInSection:indexPath.section];
        }
        if (!view) return nil;
        
        UICollectionReusableView *footerCellView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"FooterView" forIndexPath:indexPath];
        
        [[footerCellView viewWithTag:collectionViewFooterTag] removeFromSuperview];
        view.frame = footerCellView.bounds;
        view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [view setTag:collectionViewFooterTag];
        [footerCellView addSubview:view];
        
        return footerCellView;
    }
    return nil;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
    UIView *view = nil;
    if ([self.delegate respondsToSelector:@selector(dataSource:viewForHeaderInSection:)]) {
        view = [self.delegate dataSource:self viewForHeaderInSection:section];
    }
    else {
        view = [self dataSource:self viewForHeaderInSection:section];
    }
    if (view) {
        return view.frame.size;
    }
    return CGSizeZero;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
    UIView *view = nil;
    if ([self.delegate respondsToSelector:@selector(dataSource:viewForFooterInSection:)]) {
        view = [self.delegate dataSource:self viewForFooterInSection:section];
    }
    else {
        view = [self dataSource:self viewForFooterInSection:section];
    }
    if (view) {
        return view.frame.size;
    }
    return CGSizeZero;
}


#pragma mark - UICollectionViewDelegate

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    id object = [self objectAtIndexPath:indexPath];
    if ([self.delegate respondsToSelector:@selector(dataSource:didSelectCellWithOject:atIndexPath:)]) {
        [self.delegate dataSource:self didSelectCellWithOject:object atIndexPath:indexPath];
    }
    else {
        [self dataSource:self didSelectCellWithOject:object atIndexPath:indexPath];
    }
}

#pragma mark - UICollectionViewFlowLayoutDelegate

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    id object = [self objectAtIndexPath:indexPath];
    if ([self.delegate respondsToSelector:@selector(dataSource:collectionCellSizeWithObject:forIndexPath:)]) {
        return [self.delegate dataSource:self collectionCellSizeWithObject:object forIndexPath:indexPath];
    }
    else {
        return [self dataSource:self collectionCellSizeWithObject:object forIndexPath:indexPath];
    }
}

@end


//Custom forwarding to allow extension of the delegate methods (scroll view specially)

#import <objc/runtime.h>

@implementation AGDataSource(ForwardInvocation)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (BOOL)respondsToSelector:(SEL)aSelector {
    BOOL ret = [super respondsToSelector:aSelector];
    if (ret || self.delegate == self) {
        return ret;
    }
    return [self methodCanBeForwardedToCustomDelegate:aSelector];
}
#pragma clang diagnostic pop

- (id)forwardingTargetForSelector:(SEL)aSelector {
    //Send to delegate first
    if ([self methodCanBeForwardedToCustomDelegate:aSelector]) {
        return self.delegate;
    }
    return self;
}

- (BOOL)methodCanBeForwardedToCustomDelegate:(SEL)aSelector {
//    BOOL methodInProtocol = protocol_getMethodDescription(@protocol(AGDataSourceProtocol), aSelector, NO, YES).name != NULL;
    return [self.delegate respondsToSelector:aSelector];
}

@end


#import <Foundation/Foundation.h>

@class AGDataSource;
@class AGCommand;

@protocol AGDataSourceProtocol <UITableViewDelegate, UICollectionViewDelegate>

@optional

//Generic methods
- (void)dataSource:(AGDataSource *)dataSource configureCell:(id)cell withObject:(id)object forIndexPath:(NSIndexPath *)indexPath;
- (void)dataSource:(AGDataSource *)dataSource didSelectCellWithOject:(id)object atIndexPath:(NSIndexPath *)indexPath;
- (UIView *)dataSource:(AGDataSource *)dataSource viewForHeaderInSection:(NSInteger)section;
- (UIView *)dataSource:(AGDataSource *)dataSource viewForFooterInSection:(NSInteger)section;

//Table related methods
- (UITableViewCell *)dataSource:(AGDataSource *)dataSource createTableCellWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)dataSource:(AGDataSource *)dataSource tableCellHeightWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath;

//Collection related methods
- (UICollectionViewCell *)dataSource:(AGDataSource *)dataSource createCollectionCellWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath;
- (CGSize)dataSource:(AGDataSource *)dataSource collectionCellSizeWithObject:(id)object forIndexPath:(NSIndexPath *)indexPath;

@end


@protocol AGCellProtocol <NSObject>
- (void)setObject:(id)object;
@optional
- (AGCommand *)didSelectCommand;
@end

#pragma mark - Generic datasource

@interface AGDataSource : NSObject <UITableViewDataSource, UITableViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, AGDataSourceProtocol>

//Generic properties
@property (nonatomic, strong) NSArray *sectionObjects;
@property (weak, nonatomic) IBOutlet id<AGDataSourceProtocol> delegate;
@property (nonatomic, assign) BOOL useIndexAsReuseIdentifier;

- (void)reloadWithData:(NSArray *)sectionObjects;
- (void)reloadData;
- (void)updateWithData:(NSArray *)sectionObjects;
- (void)updateWithData:(NSArray *)sectionObjects animation:(UITableViewRowAnimation)animation;
- (NSArray *)objectsAtSection:(NSInteger)section;
- (id)objectAtIndexPath:(NSIndexPath *)indexPath;
- (NSIndexPath *)indexPathOfObject:(id)object;

@end

#pragma mark - Table Specific

@interface AGDataSource ()

@property (weak, nonatomic) IBOutlet UITableView *tableView;

//Registers a specific table view cell class to use with the object class specified. Pass nil to set default
- (void)registerTableCellClass:(Class)cellClass forObjectClass:(Class)objectClass;

//Registers a specific table view cell nib to use with the object class specified. Pass nil to set default
- (void)registerTableCellNib:(UINib *)cellNib forObjectClass:(Class)objectClass;

//Returns the specific cell or nib registered for this object
- (id)registeredTableCellNibOrClassForObject:(id)object;

@end

#pragma mark - Collection Specific

@interface AGDataSource ()

@property (weak, nonatomic) IBOutlet UICollectionView *collectionView;

- (void)registerCollectionCellClass:(Class)cellClass forObjectClass:(Class)objectClass;
- (void)registerCollectionCellNib:(UINib *)cellNib forObjectClass:(Class)objectClass;

@end
#import "AGCommand.h"
#import <objc/runtime.h>

@implementation AGCommand

static NSMutableArray *commandClasses = nil;
static NSSortDescriptor *prioritySortDescriptor = nil;

+ (void)initialize {
//    commandClasses = [self discoverAllCommands];
    prioritySortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"priority" ascending:NO];
}

+ (instancetype)command {
    AGCommand *cmd = [[self alloc] init];    
    //Generic commands have less priority
    if ([cmd isMemberOfClass:[AGCommand class]]) {
        cmd.priority = AGCommandPriorityLow;
    }
    return cmd;
}

+ (instancetype)commandWithObject:(id)object {
    return nil;
}

+ (void)registerForFactory {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        commandClasses = [NSMutableArray array];
    });
    if (![commandClasses containsObject:self]) {
        [commandClasses addObject:self];
    }
}

+ (instancetype)factoryCommandWithObject:(id)object {
    NSMutableArray *commands = [NSMutableArray array];
    for (Class commandClass in commandClasses) {
        id command = [commandClass commandWithObject:object];
        if (command) {
            [commands addObject:command];
        }
    }
    return [[commands sortedArrayUsingDescriptors:@[prioritySortDescriptor]] firstObject];
}

- (SEL)action {
    return @selector(performGenericCommand:);
}

- (UIViewController *)firstViewController {
    UIViewController *firingVC = (UIViewController *)self.firingObject;
    while (firingVC && ![firingVC isKindOfClass:[UIViewController class]]) {
        firingVC = (UIViewController *)[firingVC nextCommandResponder];
    }
    return firingVC;
}

+ (NSArray *)discoverAllCommands {
    int numClasses = objc_getClassList(NULL, 0);
    __unsafe_unretained Class *classes = (Class *)malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);
    
    NSMutableArray *result = [NSMutableArray array];
    for (NSInteger i = 0; i < numClasses; i++)
    {
        Class superClass = classes[i];
        do
        {
            superClass = class_getSuperclass(superClass);
        } while(superClass && superClass != self);
        
        if (superClass == nil)
        {
            continue;
        }
        
        [result addObject:classes[i]];
    }
    
    free(classes);
    
    return result;
}

@end
typedef enum {
    AGCommandPriorityLow = -10,
    AGCommandPriorityDefault = 0,
    AGCommandPriorityHigh = 10
} AGCommandPriority;

@interface AGCommand : NSObject

@property(nonatomic, weak) NSObject *firingObject;
@property(nonatomic, readonly) SEL action;

//Set the object with user info for the command
@property(nonatomic, strong) id object;

//If multiple commands available for an object, fire the one with higher priority. Defaults to AGCommandPriorityDefault
@property(nonatomic, assign) AGCommandPriority priority;

//Factory method that will look on all possible commands and create one that accepts the input object
+ (void)registerForFactory;
+ (instancetype)factoryCommandWithObject:(id)object;

//Create a specific command with different parameters
+ (instancetype)command;
+ (instancetype)commandWithObject:(id)object;

- (UIViewController *)firstViewController;

@end