// The MIT License (MIT) // // Copyright (c) 2015-2016 forkingdog ( https://github.com/forkingdog ) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import "UITableView+FDIndexPathHeightCache.h" #import typedef NSMutableArray *> FDIndexPathHeightsBySection; @interface FDIndexPathHeightCache () @property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForPortrait; @property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForLandscape; @end @implementation FDIndexPathHeightCache - (instancetype)init { self = [super init]; if (self) { _heightsBySectionForPortrait = [NSMutableArray array]; _heightsBySectionForLandscape = [NSMutableArray array]; } return self; } - (FDIndexPathHeightsBySection *)heightsBySectionForCurrentOrientation { return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ? self.heightsBySectionForPortrait: self.heightsBySectionForLandscape; } - (void)enumerateAllOrientationsUsingBlock:(void (^)(FDIndexPathHeightsBySection *heightsBySection))block { block(self.heightsBySectionForPortrait); block(self.heightsBySectionForLandscape); } - (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath { [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; NSNumber *number = self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row]; return ![number isEqualToNumber:@-1]; } - (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath { self.automaticallyInvalidateEnabled = YES; [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row] = @(height); } - (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath { [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; NSNumber *number = self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row]; #if CGFLOAT_IS_DOUBLE return number.doubleValue; #else return number.floatValue; #endif } - (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath { [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { heightsBySection[indexPath.section][indexPath.row] = @-1; }]; } - (void)invalidateAllHeightCache { [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection removeAllObjects]; }]; } - (void)buildCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths { // Build every section array or row array which is smaller than given index path. [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [self buildSectionsIfNeeded:indexPath.section]; [self buildRowsIfNeeded:indexPath.row inExistSection:indexPath.section]; }]; } - (void)buildSectionsIfNeeded:(NSInteger)targetSection { [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { for (NSInteger section = 0; section <= targetSection; ++section) { if (section >= heightsBySection.count) { heightsBySection[section] = [NSMutableArray array]; } } }]; } - (void)buildRowsIfNeeded:(NSInteger)targetRow inExistSection:(NSInteger)section { [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { NSMutableArray *heightsByRow = heightsBySection[section]; for (NSInteger row = 0; row <= targetRow; ++row) { if (row >= heightsByRow.count) { heightsByRow[row] = @-1; } } }]; } @end @implementation UITableView (FDIndexPathHeightCache) - (FDIndexPathHeightCache *)fd_indexPathHeightCache { FDIndexPathHeightCache *cache = objc_getAssociatedObject(self, _cmd); if (!cache) { cache = [FDIndexPathHeightCache new]; objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return cache; } @end // We just forward primary call, in crash report, top most method in stack maybe FD's, // but it's really not our bug, you should check whether your table view's data source and // displaying cells are not matched when reloading. static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void (^callout)(void)) { callout(); } #define FDPrimaryCall(...) do {__FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(^{__VA_ARGS__});} while(0) @implementation UITableView (FDIndexPathHeightCacheInvalidation) - (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache { FDPrimaryCall([self fd_reloadData];); } + (void)load { // All methods that trigger height cache's invalidation SEL selectors[] = { @selector(reloadData), @selector(insertSections:withRowAnimation:), @selector(deleteSections:withRowAnimation:), @selector(reloadSections:withRowAnimation:), @selector(moveSection:toSection:), @selector(insertRowsAtIndexPaths:withRowAnimation:), @selector(deleteRowsAtIndexPaths:withRowAnimation:), @selector(reloadRowsAtIndexPaths:withRowAnimation:), @selector(moveRowAtIndexPath:toIndexPath:) }; for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) { SEL originalSelector = selectors[index]; SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]); Method originalMethod = class_getInstanceMethod(self, originalSelector); Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); } } - (void)fd_reloadData { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection removeAllObjects]; }]; } FDPrimaryCall([self fd_reloadData];); } - (void)fd_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { [self.fd_indexPathHeightCache buildSectionsIfNeeded:section]; [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection insertObject:[NSMutableArray array] atIndex:section]; }]; }]; } FDPrimaryCall([self fd_insertSections:sections withRowAnimation:animation];); } - (void)fd_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { [self.fd_indexPathHeightCache buildSectionsIfNeeded:section]; [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection removeObjectAtIndex:section]; }]; }]; } FDPrimaryCall([self fd_deleteSections:sections withRowAnimation:animation];); } - (void)fd_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [sections enumerateIndexesUsingBlock: ^(NSUInteger section, BOOL *stop) { [self.fd_indexPathHeightCache buildSectionsIfNeeded:section]; [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection[section] removeAllObjects]; }]; }]; } FDPrimaryCall([self fd_reloadSections:sections withRowAnimation:animation];); } - (void)fd_moveSection:(NSInteger)section toSection:(NSInteger)newSection { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [self.fd_indexPathHeightCache buildSectionsIfNeeded:section]; [self.fd_indexPathHeightCache buildSectionsIfNeeded:newSection]; [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection exchangeObjectAtIndex:section withObjectAtIndex:newSection]; }]; } FDPrimaryCall([self fd_moveSection:section toSection:newSection];); } - (void)fd_insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [self.fd_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection[indexPath.section] insertObject:@-1 atIndex:indexPath.row]; }]; }]; } FDPrimaryCall([self fd_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation];); } - (void)fd_deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [self.fd_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; NSMutableDictionary *mutableIndexSetsToRemove = [NSMutableDictionary dictionary]; [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { NSMutableIndexSet *mutableIndexSet = mutableIndexSetsToRemove[@(indexPath.section)]; if (!mutableIndexSet) { mutableIndexSet = [NSMutableIndexSet indexSet]; mutableIndexSetsToRemove[@(indexPath.section)] = mutableIndexSet; } [mutableIndexSet addIndex:indexPath.row]; }]; [mutableIndexSetsToRemove enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSIndexSet *indexSet, BOOL *stop) { [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection[key.integerValue] removeObjectsAtIndexes:indexSet]; }]; }]; } FDPrimaryCall([self fd_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];); } - (void)fd_reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [self.fd_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { heightsBySection[indexPath.section][indexPath.row] = @-1; }]; }]; } FDPrimaryCall([self fd_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];); } - (void)fd_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) { [self.fd_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:@[sourceIndexPath, destinationIndexPath]]; [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { NSMutableArray *sourceRows = heightsBySection[sourceIndexPath.section]; NSMutableArray *destinationRows = heightsBySection[destinationIndexPath.section]; NSNumber *sourceValue = sourceRows[sourceIndexPath.row]; NSNumber *destinationValue = destinationRows[destinationIndexPath.row]; sourceRows[sourceIndexPath.row] = destinationValue; destinationRows[destinationIndexPath.row] = sourceValue; }]; } FDPrimaryCall([self fd_moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];); } @end