| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| #import "WXRecyclerUpdateController.h" |
| #import "WXCellComponent.h" |
| #import "WXAssert.h" |
| #import "WXLog.h" |
| #import "WXDiffUtil.h" |
| #import "NSArray+Weex.h" |
| |
| @interface WXRecyclerDiffResult : NSObject |
| |
| @property (nonatomic, strong, readonly) NSIndexSet *insertSections; |
| @property (nonatomic, strong, readonly) NSIndexSet *deleteSections; |
| @property (nonatomic, strong, readonly) NSIndexSet *reloadSections; |
| |
| @property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *deleteIndexPaths; |
| @property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *insertIndexPaths; |
| @property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *reloadIndexPaths; |
| |
| - (BOOL)hasChanges; |
| |
| @end |
| |
| @implementation WXRecyclerDiffResult |
| |
| - (instancetype)initWithInsertSections:(NSIndexSet *)insertSections |
| deleteSections:(NSIndexSet *)deletesSections |
| reloadSections:(NSIndexSet *)reloadSections |
| insertIndexPaths:(NSMutableSet<NSIndexPath *> *)insertIndexPaths |
| deleteIndexPaths:(NSMutableSet<NSIndexPath *> *)deleteIndexPaths |
| reloadIndexPaths:(NSMutableSet<NSIndexPath *> *)reloadIndexPaths |
| { |
| if (self = [super init]) { |
| _insertSections = [insertSections copy]; |
| _deleteSections = [deletesSections copy]; |
| _reloadSections = [reloadSections copy]; |
| _insertIndexPaths = [insertIndexPaths copy]; |
| _deleteIndexPaths = [deleteIndexPaths copy]; |
| _reloadIndexPaths = [reloadIndexPaths copy]; |
| } |
| |
| return self; |
| } |
| |
| - (BOOL)hasChanges |
| { |
| return _insertSections.count > 0 || _deleteSections.count > 0 || _reloadSections.count > 0 || _insertIndexPaths.count > 0 || _deleteIndexPaths.count > 0 || _reloadIndexPaths.count > 0; |
| } |
| |
| - (NSString *)description |
| { |
| return [NSString stringWithFormat:@"<%@: %p; insert sections: %@; delete sections: %@; reload sections: %@; insert index paths: %@; delete index paths: %@; reload index paths: %@", NSStringFromClass([self class]), self,_insertSections, _deleteSections, _reloadSections, _insertIndexPaths, _deleteIndexPaths, _reloadIndexPaths]; |
| } |
| |
| @end |
| |
| @interface WXRecyclerUpdateController () |
| |
| @property (nonatomic, copy) NSArray<WXSectionDataController *> *theNewData; |
| @property (nonatomic, copy) NSArray<WXSectionDataController *> *theOldData; |
| @property (nonatomic, weak) UICollectionView *collectionView; |
| @property (nonatomic, strong) NSMutableSet<NSIndexPath *> *reloadIndexPaths; |
| @property (nonatomic, assign) BOOL isUpdating; |
| |
| @end |
| |
| @implementation WXRecyclerUpdateController |
| |
| - (void)performUpdatesWithNewData:(NSArray<WXSectionDataController *> *)newData oldData:(NSArray<WXSectionDataController *> *)oldData view:(UICollectionView *)collectionView |
| { |
| if (!collectionView) { |
| return; |
| } |
| |
| self.theNewData = newData; |
| self.theOldData = oldData; |
| self.collectionView = collectionView; |
| |
| [self checkUpdates]; |
| } |
| |
| - (void)reloadItemsAtIndexPath:(NSIndexPath *)indexPath |
| { |
| if (!indexPath) { |
| return; |
| } |
| |
| if (!_reloadIndexPaths) { |
| _reloadIndexPaths = [NSMutableSet set]; |
| } |
| |
| [_reloadIndexPaths addObject:indexPath]; |
| |
| [self checkUpdates]; |
| } |
| |
| - (void)checkUpdates |
| { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| if (self.isUpdating) { |
| return ; |
| } |
| |
| [self performBatchUpdates]; |
| }); |
| } |
| |
| - (void)performBatchUpdates |
| { |
| WXAssertMainThread(); |
| WXAssert(!self.isUpdating, @"Can not perform updates while an updating is being performed"); |
| |
| UICollectionView *collectionView = self.collectionView; |
| if (!collectionView) { |
| return; |
| } |
| |
| NSArray<WXSectionDataController *> *newData = [self.theNewData copy]; |
| NSArray<WXSectionDataController *> *oldData = [self.theOldData copy]; |
| |
| [self cleanup]; |
| |
| WXRecyclerDiffResult *diffResult = [self diffWithNewData:newData oldData:oldData]; |
| if (![diffResult hasChanges] && self.reloadIndexPaths.count == 0) { |
| return; |
| } |
| |
| void (^updates)(void) = [^{ |
| [self.delegate updateController:self willPerformUpdateWithNewData:newData]; |
| [UIView setAnimationsEnabled:NO]; |
| WXLogDebug(@"UICollectionView update:%@", diffResult); |
| if(!diffResult.hasChanges) { return ; } |
| [self applyUpdate:diffResult toCollectionView:self.collectionView]; |
| } copy]; |
| |
| void (^completion)(BOOL) = [^(BOOL finished) { |
| [UIView setAnimationsEnabled:YES]; |
| self.isUpdating = NO; |
| [self.delegate updateController:self didPerformUpdateWithFinished:finished]; |
| [self.reloadIndexPaths removeAllObjects]; |
| [self checkUpdates]; |
| } copy]; |
| |
| self.isUpdating = YES; |
| |
| if (!self.delegate || !collectionView.dataSource) { |
| return; |
| } |
| WXLogDebug(@"Diff result:%@", diffResult); |
| [collectionView performBatchUpdates:updates completion:completion]; |
| } |
| |
| - (void)cleanup |
| { |
| self.theNewData = nil; |
| self.theOldData = nil; |
| } |
| |
| - (WXRecyclerDiffResult *)diffWithNewData:(NSArray<WXSectionDataController *> *)newData |
| oldData:(NSArray<WXSectionDataController *> *)oldData |
| { |
| NSMutableIndexSet *reloadSections = [NSMutableIndexSet indexSet]; |
| NSMutableSet<NSIndexPath *> *reloadIndexPaths = [NSMutableSet set]; |
| NSMutableSet<NSIndexPath *> *deleteIndexPaths = [NSMutableSet set]; |
| NSMutableSet<NSIndexPath *> *insertIndexPaths = [NSMutableSet set]; |
| |
| WXDiffResult *sectionDiffResult = [WXDiffUtil diffWithMinimumDistance:newData oldArray:oldData]; |
| |
| WXLogDebug(@"section diff result:%@", sectionDiffResult); |
| |
| [sectionDiffResult.inserts enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { |
| WXSectionDataController *newSection = [newData wx_safeObjectAtIndex:idx]; |
| [newSection.cellComponents enumerateObjectsUsingBlock:^(WXCellComponent * _Nonnull obj, NSUInteger idx2, BOOL * _Nonnull stop) { |
| if (obj.isLayoutComplete) { |
| NSIndexPath *insertIndexPath = [NSIndexPath indexPathForItem:idx2 inSection:idx]; |
| [insertIndexPaths addObject:insertIndexPath]; |
| } |
| }]; |
| WXAssert(newSection, @"No section found in new index:%ld"); |
| }]; |
| |
| for (WXDiffUpdateIndex *sectionUpdate in sectionDiffResult.updates) { |
| WXSectionDataController *oldSection = [oldData wx_safeObjectAtIndex:sectionUpdate.oldIndex]; |
| WXSectionDataController *newSection = [newData wx_safeObjectAtIndex:sectionUpdate.newIndex]; |
| WXAssert(newSection && oldSection, @"No section found in old index:%ld, new index:%ld", sectionUpdate.oldIndex, sectionUpdate.newIndex); |
| |
| WXDiffResult *itemDiffResult = [WXDiffUtil diffWithMinimumDistance:newSection.cellComponents oldArray:oldSection.cellComponents]; |
| if (![itemDiffResult hasChanges]) { |
| // header or footer need to be updated |
| [reloadSections addIndex:sectionUpdate.oldIndex]; |
| } else { |
| for (WXDiffUpdateIndex *update in itemDiffResult.updates) { |
| NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForItem:update.oldIndex inSection:sectionUpdate.oldIndex]; |
| [reloadIndexPaths addObject:reloadIndexPath]; |
| } |
| |
| [itemDiffResult.inserts enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { |
| WXCellComponent *cell = [newSection.cellComponents wx_safeObjectAtIndex:idx]; |
| if (cell.isLayoutComplete) { |
| NSIndexPath *insertIndexPath = [NSIndexPath indexPathForItem:idx inSection:sectionUpdate.oldIndex]; |
| [insertIndexPaths addObject:insertIndexPath]; |
| } |
| }]; |
| |
| [itemDiffResult.deletes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { |
| NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForItem:idx inSection:sectionUpdate.oldIndex]; |
| [deleteIndexPaths addObject:deleteIndexPath]; |
| }]; |
| } |
| |
| } |
| |
| WXRecyclerDiffResult *result = [[WXRecyclerDiffResult alloc] initWithInsertSections:sectionDiffResult.inserts |
| deleteSections:sectionDiffResult.deletes |
| reloadSections:reloadSections |
| insertIndexPaths:insertIndexPaths |
| deleteIndexPaths:deleteIndexPaths |
| reloadIndexPaths:reloadIndexPaths]; |
| |
| return result; |
| } |
| |
| - (void)applyUpdate:(WXRecyclerDiffResult *)diffResult toCollectionView:(UICollectionView *)collectionView |
| { |
| if (!collectionView) { |
| return; |
| } |
| |
| // reload index paths should not inculde delete index paths, otherwise it will cause crash: |
| // Assertion failure in |
| // -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:] |
| NSMutableSet *reloadIndexPaths = self.reloadIndexPaths ? [[diffResult.reloadIndexPaths setByAddingObjectsFromSet:self.reloadIndexPaths] mutableCopy]: [diffResult.reloadIndexPaths mutableCopy]; |
| [reloadIndexPaths minusSet:diffResult.deleteIndexPaths]; |
| |
| [collectionView deleteItemsAtIndexPaths:[diffResult.deleteIndexPaths allObjects]]; |
| [collectionView insertItemsAtIndexPaths:[diffResult.insertIndexPaths allObjects]]; |
| [collectionView reloadItemsAtIndexPaths:[reloadIndexPaths allObjects]]; |
| |
| [collectionView deleteSections:diffResult.deleteSections]; |
| [collectionView insertSections:diffResult.insertSections]; |
| [collectionView reloadSections:diffResult.reloadSections]; |
| } |
| |
| @end |