#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