From 5cf01cc0a7c41d370600df495aff91d1099fa0bc Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Fri, 20 Oct 2017 08:41:01 -0700 Subject: [PATCH] Add update finished announcer API Summary: Adding a new API to support adding 1:many listeners to observe when an `IGListAdapter` finishes performing an update event. It supports: - `performUpdates:` - `reloadData...` - Even handling update triggered from //inside// a section controller Differential Revision: D6108096 fbshipit-source-id: d0b43d83f1963fdbf6ef388685cbd8f6890177fa --- CHANGELOG.md | 2 + IGListKit.xcodeproj/project.pbxproj | 14 ++ Source/IGListAdapter.h | 5 + Source/IGListAdapter.m | 33 +++- Source/IGListAdapterUpdateListener.h | 55 ++++++ Source/IGListKit.h | 11 +- Source/Internal/IGListAdapterInternal.h | 1 - Tests/IGListAdapterE2ETests.m | 212 ++++++++++++++++++++++ Tests/Objects/IGListAdapterUpdateTester.h | 26 +++ Tests/Objects/IGListAdapterUpdateTester.m | 26 +++ 10 files changed, 376 insertions(+), 9 deletions(-) create mode 100644 Source/IGListAdapterUpdateListener.h create mode 100644 Tests/Objects/IGListAdapterUpdateTester.h create mode 100644 Tests/Objects/IGListAdapterUpdateTester.m diff --git a/CHANGELOG.md b/CHANGELOG.md index d8004864..2608dcb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag - Added `-[IGListSectionController didHighlightItemAtIndex:]` and `-[IGListSectionController didUnhighlightItemAtIndex:]` APIs to support `UICollectionView` cell highlighting. [Kevin Delannoy](https://github.com/delannoyk) [(#933)](https://github.com/Instagram/IGListKit/pull/933) +- Added a new listener API to be notified when `IGListAdapter` finishes updating. Add listeners via `-[IGListAdapter addUpdateListener:]` with objects conforming to the new `IGListAdapterUpdateListener` protocol. [Ryan Nystrom](https://github.com/rnystrom) [(tbd)](https://github.com/Instagram/IGListKit/pull/tbd) + ### Fixes - Weakly reference the `UICollectionView` in coalescence so that it can be released if the rest of system is destroyed. [Ryan Nystrom](https://github.com/rnystrom) [(#tbd)](https://github.com/Instagram/IGListKit/pull/tbd) diff --git a/IGListKit.xcodeproj/project.pbxproj b/IGListKit.xcodeproj/project.pbxproj index 414b469f..6a689fad 100644 --- a/IGListKit.xcodeproj/project.pbxproj +++ b/IGListKit.xcodeproj/project.pbxproj @@ -196,6 +196,10 @@ 294652BB1EA927750063BDD9 /* IGListSectionMap+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 290DF36E1E931457009FE456 /* IGListSectionMap+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; 294652BC1EA927750063BDD9 /* UICollectionView+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 290DF3561E930CE2009FE456 /* UICollectionView+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; 294AC6321DDE4C19002FCE5D /* IGListDiffResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */; }; + 294CDE5F1F98E3A7002CF6E4 /* IGListAdapterUpdateListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 294CDE5E1F98E3A6002CF6E4 /* IGListAdapterUpdateListener.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 294CDE601F995488002CF6E4 /* IGListAdapterUpdateListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 294CDE5E1F98E3A6002CF6E4 /* IGListAdapterUpdateListener.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 294CDE631F995DD7002CF6E4 /* IGListAdapterUpdateTester.h in Headers */ = {isa = PBXBuildFile; fileRef = 294CDE611F995DD7002CF6E4 /* IGListAdapterUpdateTester.h */; }; + 294CDE641F995DD7002CF6E4 /* IGListAdapterUpdateTester.m in Sources */ = {isa = PBXBuildFile; fileRef = 294CDE621F995DD7002CF6E4 /* IGListAdapterUpdateTester.m */; }; 296AC95C1EA518D3005137E2 /* IGListReloadIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 296AC95A1EA518D3005137E2 /* IGListReloadIndexPath.h */; settings = {ATTRIBUTES = (Private, ); }; }; 296AC95D1EA518D3005137E2 /* IGListReloadIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 296AC95A1EA518D3005137E2 /* IGListReloadIndexPath.h */; settings = {ATTRIBUTES = (Private, ); }; }; 296AC95F1EA518D3005137E2 /* IGListReloadIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 296AC95B1EA518D3005137E2 /* IGListReloadIndexPath.m */; }; @@ -460,6 +464,9 @@ 292807381E82CE240077A81C /* IGListBatchContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBatchContext.h; sourceTree = ""; }; 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IGTestNibCell.xib; sourceTree = ""; }; 294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDiffResultTests.m; sourceTree = ""; }; + 294CDE5E1F98E3A6002CF6E4 /* IGListAdapterUpdateListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdateListener.h; sourceTree = ""; }; + 294CDE611F995DD7002CF6E4 /* IGListAdapterUpdateTester.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdateTester.h; sourceTree = ""; }; + 294CDE621F995DD7002CF6E4 /* IGListAdapterUpdateTester.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterUpdateTester.m; sourceTree = ""; }; 296AC95A1EA518D3005137E2 /* IGListReloadIndexPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListReloadIndexPath.h; sourceTree = ""; }; 296AC95B1EA518D3005137E2 /* IGListReloadIndexPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListReloadIndexPath.m; sourceTree = ""; }; 297278BB1E6B58560099D8EA /* IGListBatchUpdates.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBatchUpdates.h; sourceTree = ""; }; @@ -632,6 +639,7 @@ 0B3B927F1E08D7F5008390ED /* Common */, 0B3B929A1E08D7F5008390ED /* IGListAdapter.h */, 0B3B929B1E08D7F5008390ED /* IGListAdapter.m */, + 294CDE5E1F98E3A6002CF6E4 /* IGListAdapterUpdateListener.h */, 0B3B929C1E08D7F5008390ED /* IGListAdapterDataSource.h */, 0B3B929D1E08D7F5008390ED /* IGListAdapterDelegate.h */, 0B3B929E1E08D7F5008390ED /* IGListAdapterUpdater.h */, @@ -851,6 +859,8 @@ 88144F061D870EDC007C7F66 /* IGTestSupplementarySource.m */, 2995409A1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.h */, 2995409B1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.m */, + 294CDE611F995DD7002CF6E4 /* IGListAdapterUpdateTester.h */, + 294CDE621F995DD7002CF6E4 /* IGListAdapterUpdateTester.m */, ); path = Objects; sourceTree = ""; @@ -925,6 +935,7 @@ 989317641E0ED45900DB93B3 /* IGListCompatibility.h in Headers */, 0B3B92FB1E08D7F5008390ED /* IGListAdapterDataSource.h in Headers */, 0B3B92E91E08D7F5008390ED /* IGListIndexSetResultInternal.h in Headers */, + 294CDE601F995488002CF6E4 /* IGListAdapterUpdateListener.h in Headers */, 0B3B930B1E08D7F5008390ED /* IGListDisplayDelegate.h in Headers */, 0B3B933F1E08D7F5008390ED /* IGListStackedSectionControllerInternal.h in Headers */, 0B3B93171E08D7F5008390ED /* IGListSectionController.h in Headers */, @@ -1002,6 +1013,7 @@ 0B3B93441E08D7F5008390ED /* UICollectionView+IGListBatchUpdateData.h in Headers */, 0B3B92DC1E08D7F5008390ED /* IGListMacros.h in Headers */, 0B3B92D61E08D7F5008390ED /* IGListIndexSetResult.h in Headers */, + 294CDE5F1F98E3A7002CF6E4 /* IGListAdapterUpdateListener.h in Headers */, 0B3B92C61E08D7F5008390ED /* IGListBatchUpdateData.h in Headers */, 290DF3741E931B57009FE456 /* IGListDebuggingUtilities.h in Headers */, 0B3B92EC1E08D7F5008390ED /* IGListMoveIndexPathInternal.h in Headers */, @@ -1041,6 +1053,7 @@ 0B3B92FC1E08D7F5008390ED /* IGListAdapterDelegate.h in Headers */, 0B3B92F61E08D7F5008390ED /* IGListAdapter.h in Headers */, 298DD9C21E3ACF4800F76F50 /* IGListBindable.h in Headers */, + 294CDE631F995DD7002CF6E4 /* IGListAdapterUpdateTester.h in Headers */, 292807391E82CE240077A81C /* IGListBatchContext.h in Headers */, 0B3B92E21E08D7F5008390ED /* IGListMoveIndexPath.h in Headers */, 297278C41E6B59D50099D8EA /* IGListBatchUpdateState.h in Headers */, @@ -1536,6 +1549,7 @@ 0B3B92D41E08D7F5008390ED /* IGListIndexPathResult.m in Sources */, 0B3B93361E08D7F5008390ED /* IGListDisplayHandler.m in Sources */, 0B3B92E01E08D7F5008390ED /* IGListMoveIndex.m in Sources */, + 294CDE641F995DD7002CF6E4 /* IGListAdapterUpdateTester.m in Sources */, 0B3B93461E08D7F5008390ED /* UICollectionView+IGListBatchUpdateData.m in Sources */, 290DF3551E930C89009FE456 /* IGListDebugger.m in Sources */, 0B3B92E41E08D7F5008390ED /* IGListMoveIndexPath.m in Sources */, diff --git a/Source/IGListAdapter.h b/Source/IGListAdapter.h index 17b5dd8b..73773508 100644 --- a/Source/IGListAdapter.h +++ b/Source/IGListAdapter.h @@ -12,6 +12,7 @@ #import #import #import +#import #import #import @@ -261,6 +262,10 @@ NS_SWIFT_NAME(ListAdapter) - (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; +- (void)addUpdateListener:(id)updateListener; + +- (void)removeUpdateListener:(id)updateListener; + /** :nodoc: */ diff --git a/Source/IGListAdapter.m b/Source/IGListAdapter.m index 85077f13..5ec62daf 100644 --- a/Source/IGListAdapter.m +++ b/Source/IGListAdapter.m @@ -20,6 +20,7 @@ NSMapTable *_viewSectionControllerMap; // An array of blocks to execute once batch updates are finished NSMutableArray *_queuedCompletionBlocks; + NSHashTable> *_updateListeners; } - (void)dealloc { @@ -50,6 +51,7 @@ _displayHandler = [IGListDisplayHandler new]; _workingRangeHandler = [[IGListWorkingRangeHandler alloc] initWithWorkingRangeSize:workingRangeSize]; + _updateListeners = [NSHashTable weakObjectsHashTable]; _viewSectionControllerMap = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality | NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; @@ -336,10 +338,10 @@ // release the previous items weakSelf.previousSectionMap = nil; + [weakSelf notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated]; if (completion) { completion(finished); } - [weakSelf exitBatchUpdates]; }]; } @@ -364,7 +366,12 @@ // purge all section controllers from the item map so that they are regenerated [weakSelf.sectionMap reset]; [weakSelf updateObjects:newItems dataSource:dataSource]; - } completion:completion]; + } completion:^(BOOL finished) { + [weakSelf notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO]; + if (completion) { + completion(finished); + } + }]; } - (void)reloadObjects:(NSArray *)objects { @@ -398,6 +405,26 @@ [self.updater reloadCollectionView:collectionView sections:sections]; } +- (void)addUpdateListener:(id)updateListener { + IGAssertMainThread(); + IGParameterAssert(updateListener != nil); + + [_updateListeners addObject:updateListener]; +} + +- (void)removeUpdateListener:(id)updateListener { + IGAssertMainThread(); + IGParameterAssert(updateListener != nil); + + [_updateListeners removeObject:updateListener]; +} + +- (void)notifyDidUpdate:(IGListAdapterUpdateType)update animated:(BOOL)animated { + for (id listener in _updateListeners) { + [listener listAdapter:self didFinishUpdate:update animated:animated]; + } +} + #pragma mark - List Items & Sections @@ -1001,10 +1028,10 @@ weakSelf.isInUpdateBlock = NO; } completion: ^(BOOL finished) { [weakSelf updateBackgroundViewShouldHide:![weakSelf itemCountIsZero]]; + [weakSelf notifyDidUpdate:IGListAdapterUpdateTypeItemUpdates animated:animated]; if (completion) { completion(finished); } - [weakSelf exitBatchUpdates]; }]; } diff --git a/Source/IGListAdapterUpdateListener.h b/Source/IGListAdapterUpdateListener.h new file mode 100644 index 00000000..ddfe9698 --- /dev/null +++ b/Source/IGListAdapterUpdateListener.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class IGListAdapter; + +NS_ASSUME_NONNULL_BEGIN + +/** + The type of update that was performed by an `IGListAdapter`. + */ +NS_SWIFT_NAME(ListAdapterUpdateType) +typedef NS_ENUM(NSInteger, IGListAdapterUpdateType) { + /** + `-[IGListAdapter performUpdatesAnimated:completion:]` was executed. + */ + IGListAdapterUpdateTypePerformUpdates, + /** + `-[IGListAdapter reloadDataWithCompletion:]` was executed. + */ + IGListAdapterUpdateTypeReloadData, + /** + `-[IGListCollectionContext performBatchAnimated:updates:completion:]` was executed by an `IGListSectionController`. + */ + IGListAdapterUpdateTypeItemUpdates, +}; + +NS_SWIFT_NAME(ListAdapterUpdateListener) +@protocol IGListAdapterUpdateListener + +/** + Notifies a listener that the listAdapter was updated. + + @param listAdapter The `IGListAdapter` that updated. + @param update The type of update executed. + @param animated A flag indicating if the update was animated. Always `NO` for `IGListAdapterUpdateTypeReloadData`. + + @note This event is sent before the completion block in `-[IGListAdapter performUpdatesAnimated:completion:]` and + `-[IGListAdapter reloadDataWithCompletion:]` is executed. This event is also delivered when an + `IGListSectionController` updates via `-[IGListCollectionContext performBatchAnimated:updates:completion:]`. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter + didFinishUpdate:(IGListAdapterUpdateType)update + animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit.h b/Source/IGListKit.h index d8665b06..7a3128e2 100644 --- a/Source/IGListKit.h +++ b/Source/IGListKit.h @@ -26,26 +26,27 @@ FOUNDATION_EXPORT const unsigned char IGListKitVersionString[]; #import #import #import +#import #import #import #import #import -#import -#import -#import #import +#import +#import +#import #import +#import #import #import #import -#import #import #import +#import #import #import #import #import -#import #import #endif diff --git a/Source/Internal/IGListAdapterInternal.h b/Source/Internal/IGListAdapterInternal.h index b5dd843a..ececa490 100644 --- a/Source/Internal/IGListAdapterInternal.h +++ b/Source/Internal/IGListAdapterInternal.h @@ -62,7 +62,6 @@ IGListBatchContext @property (nonatomic, strong) NSMutableSet *registeredSupplementaryViewIdentifiers; @property (nonatomic, strong) NSMutableSet *registeredSupplementaryViewNibNames; - - (void)mapView:(__kindof UIView *)view toSectionController:(IGListSectionController *)sectionController; - (nullable IGListSectionController *)sectionControllerForView:(__kindof UIView *)view; - (void)removeMapForView:(__kindof UIView *)view; diff --git a/Tests/IGListAdapterE2ETests.m b/Tests/IGListAdapterE2ETests.m index 00762f1c..1f488341 100644 --- a/Tests/IGListAdapterE2ETests.m +++ b/Tests/IGListAdapterE2ETests.m @@ -19,6 +19,7 @@ #import "IGTestDelegateDataSource.h" #import "IGTestObject.h" #import "IGListTestCase.h" +#import "IGListAdapterUpdateTester.h" @interface IGListAdapterE2ETests : IGListTestCase @end @@ -1601,4 +1602,215 @@ }]; } +- (void)test_whenAddingMultipleUpdateListeners_withPerformUpdatesAnimated_thatEventsReceived { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; + IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; + + [self.adapter addUpdateListener:listener1]; + [self.adapter addUpdateListener:listener2]; + + self.dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @1) + ]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { + XCTAssertEqual(listener1.hits, 1); + XCTAssertEqual(listener1.animated, YES); + XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); + XCTAssertEqual(listener2.hits, 1); + XCTAssertEqual(listener2.animated, YES); + XCTAssertEqual(listener2.type, IGListAdapterUpdateTypePerformUpdates); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenAddingMultipleUpdateListeners_withPerformUpdatesNotAnimated_thatEventsReceived { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; + IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; + + [self.adapter addUpdateListener:listener1]; + [self.adapter addUpdateListener:listener2]; + + self.dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @1) + ]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + XCTAssertEqual(listener1.hits, 1); + XCTAssertEqual(listener1.animated, NO); + XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); + XCTAssertEqual(listener2.hits, 1); + XCTAssertEqual(listener2.animated, NO); + XCTAssertEqual(listener2.type, IGListAdapterUpdateTypePerformUpdates); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenAddingMultipleUpdateListeners_withReloadData_thatEventsReceived { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; + IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; + + [self.adapter addUpdateListener:listener1]; + [self.adapter addUpdateListener:listener2]; + + self.dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @1) + ]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter reloadDataWithCompletion:^(BOOL finished) { + XCTAssertEqual(listener1.hits, 1); + XCTAssertEqual(listener1.animated, NO); + XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeReloadData); + XCTAssertEqual(listener2.hits, 1); + XCTAssertEqual(listener2.animated, NO); + XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeReloadData); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenAddingMultipleUpdateListeners_withItemUpdatesAnimated_thatEventsReceived { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; + IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; + + [self.adapter addUpdateListener:listener1]; + [self.adapter addUpdateListener:listener2]; + + self.dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @1) + ]; + + IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; + + XCTestExpectation *expectation = genExpectation; + [section.collectionContext performBatchAnimated:YES updates:^(id _Nonnull batchContext) { + [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; + } completion:^(BOOL finished) { + XCTAssertEqual(listener1.hits, 1); + XCTAssertEqual(listener1.animated, YES); + XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeItemUpdates); + XCTAssertEqual(listener2.hits, 1); + XCTAssertEqual(listener2.animated, YES); + XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeItemUpdates); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenAddingMultipleUpdateListeners_withItemUpdatesNotAnimated_thatEventsReceived { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; + IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; + + [self.adapter addUpdateListener:listener1]; + [self.adapter addUpdateListener:listener2]; + + self.dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @1) + ]; + + IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; + + XCTestExpectation *expectation = genExpectation; + [section.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { + [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; + } completion:^(BOOL finished) { + XCTAssertEqual(listener1.hits, 1); + XCTAssertEqual(listener1.animated, NO); + XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeItemUpdates); + XCTAssertEqual(listener2.hits, 1); + XCTAssertEqual(listener2.animated, NO); + XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeItemUpdates); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenAddingMultipleUpdateListeners_thenRemovingListener_thatRemainingReceives { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; + IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; + + [self.adapter addUpdateListener:listener1]; + [self.adapter addUpdateListener:listener2]; + [self.adapter removeUpdateListener:listener2]; + + self.dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @1) + ]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { + XCTAssertEqual(listener1.hits, 1); + XCTAssertEqual(listener1.animated, YES); + XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); + XCTAssertEqual(listener2.hits, 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenAddingUpdateListener_thenListenerReferenceHitsZero_thatListenerReleased { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + IGListAdapterUpdateTester *listener = [IGListAdapterUpdateTester new]; + __weak id weakListener = listener; + [self.adapter addUpdateListener:listener]; + listener = nil; + + self.dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @1) + ]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { + XCTAssertNil(weakListener); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + @end diff --git a/Tests/Objects/IGListAdapterUpdateTester.h b/Tests/Objects/IGListAdapterUpdateTester.h new file mode 100644 index 00000000..701bd16f --- /dev/null +++ b/Tests/Objects/IGListAdapterUpdateTester.h @@ -0,0 +1,26 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// GitHub: +// https://github.com/Instagram/IGListKit +// +// Documentation: +// https://instagram.github.io/IGListKit/ +// + +#import + +#import + +@interface IGListAdapterUpdateTester : NSObject + +@property (nonatomic, assign) NSInteger hits; +@property (nonatomic, assign) IGListAdapterUpdateType type; +@property (nonatomic, assign) BOOL animated; + +@end diff --git a/Tests/Objects/IGListAdapterUpdateTester.m b/Tests/Objects/IGListAdapterUpdateTester.m new file mode 100644 index 00000000..49c65583 --- /dev/null +++ b/Tests/Objects/IGListAdapterUpdateTester.m @@ -0,0 +1,26 @@ +// +// Copyright (c) 2016-present, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// GitHub: +// https://github.com/Instagram/IGListKit +// +// Documentation: +// https://instagram.github.io/IGListKit/ +// + +#import "IGListAdapterUpdateTester.h" + +@implementation IGListAdapterUpdateTester + +- (void)listAdapter:(IGListAdapter *)listAdapter didFinishUpdate:(IGListAdapterUpdateType)update animated:(BOOL)animated { + self.hits++; + self.type = update; + self.animated = animated; +} + +@end