From 636f6b5a3de48aeb6cfea9ce0612372c2ff3c79e Mon Sep 17 00:00:00 2001 From: Maxime Ollivier Date: Wed, 9 Oct 2024 11:38:29 -0700 Subject: [PATCH] create global IGListAdapterDelegateAnnouncer Summary: In a few cases, we need to listen to all `IGListAdapter` events. We bend over backwards to detect adapters and swap their delegate with a proxy objects. Lets make things simpler and build it right into `IGListKit` by allowing global announcers, starting with `IGListAdapterDelegate`, but we could expand to the others if we like this. Generally, we want to avoid anything global, but given the complexity of the alternative, this feels like the better tradeoff. Differential Revision: D64042609 fbshipit-source-id: 0ca6bada27e640fee5a231148427be41994e4d43 --- IGListKit.xcodeproj/project.pbxproj | 6 + Source/IGListKit/IGListAdapter.m | 2 + .../IGListAdapterDelegateAnnouncer.h | 28 +++ .../IGListAdapterDelegateAnnouncer.m | 49 ++++++ .../IGListAdapterDelegateAnnouncerInternal.h | 19 +++ .../Internal/IGListAdapterInternal.h | 3 + .../IGListKit/Internal/IGListDisplayHandler.h | 1 + .../IGListKit/Internal/IGListDisplayHandler.m | 5 +- Tests/IGListAdapterDelegateAnnouncerTests.m | 161 ++++++++++++++++++ .../IGListAdapterDelegateAnnouncer.m | 1 + .../IGListAdapterDelegateAnnouncerInternal.h | 1 + .../include/IGListAdapterDelegateAnnouncer.h | 1 + 12 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 Source/IGListKit/IGListAdapterDelegateAnnouncer.h create mode 100644 Source/IGListKit/IGListAdapterDelegateAnnouncer.m create mode 100644 Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h create mode 100644 Tests/IGListAdapterDelegateAnnouncerTests.m create mode 120000 spm/Sources/IGListKit/IGListAdapterDelegateAnnouncer.m create mode 120000 spm/Sources/IGListKit/IGListAdapterDelegateAnnouncerInternal.h create mode 120000 spm/Sources/IGListKit/include/IGListAdapterDelegateAnnouncer.h diff --git a/IGListKit.xcodeproj/project.pbxproj b/IGListKit.xcodeproj/project.pbxproj index 8758799f7..cd49879a3 100644 --- a/IGListKit.xcodeproj/project.pbxproj +++ b/IGListKit.xcodeproj/project.pbxproj @@ -87,6 +87,8 @@ 576029E72C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029DA2C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h */; }; 576029E82C61B91D006E50E2 /* IGListUpdateCoalescer.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029DB2C61B91D006E50E2 /* IGListUpdateCoalescer.m */; }; 576029E92C61B91D006E50E2 /* IGListUpdateCoalescer.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029DB2C61B91D006E50E2 /* IGListUpdateCoalescer.m */; }; + 5766613E2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */; }; + 5766613F2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */; }; 57B22E6C2502AAB20055DC2F /* IGListTransitionData.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E662502AAB10055DC2F /* IGListTransitionData.m */; }; 57B22E6F2502AAB20055DC2F /* IGListTransitionData.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E692502AAB10055DC2F /* IGListTransitionData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 57B22E7F2502AAC40055DC2F /* IGListBatchUpdateTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E712502AAC20055DC2F /* IGListBatchUpdateTransaction.m */; }; @@ -662,6 +664,7 @@ 576029D92C61B91D006E50E2 /* IGListViewVisibilityTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListViewVisibilityTracker.m; sourceTree = ""; }; 576029DA2C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListViewVisibilityTrackerInternal.h; sourceTree = ""; }; 576029DB2C61B91D006E50E2 /* IGListUpdateCoalescer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListUpdateCoalescer.m; sourceTree = ""; }; + 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterDelegateAnnouncerTests.m; sourceTree = ""; }; 57B22E662502AAB10055DC2F /* IGListTransitionData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTransitionData.m; sourceTree = ""; }; 57B22E692502AAB10055DC2F /* IGListTransitionData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTransitionData.h; sourceTree = ""; }; 57B22E712502AAC20055DC2F /* IGListBatchUpdateTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListBatchUpdateTransaction.m; sourceTree = ""; }; @@ -1294,6 +1297,7 @@ children = ( 294369AF1DB1B7AE0025F6E7 /* Assets */, 88144EE21D870EDC007C7F66 /* IGListAdapterE2ETests.m */, + 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */, 29C4748A1DDF45E700AE68CE /* IGListAdapterProxyTests.m */, 8240C7F11DC284C300B3AAE7 /* IGListAdapterStoryboardTests.m */, 88144EE31D870EDC007C7F66 /* IGListAdapterTests.m */, @@ -2131,6 +2135,7 @@ 29C4748F1DDF460500AE68CE /* IGListDiffResultTests.m in Sources */, F1ED68B729E9B3B9003744F8 /* IGListTransactionTests.m in Sources */, F1ED68BE29E9B41A003744F8 /* IGListContentInsetTests.m in Sources */, + 5766613F2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */, 885FE2421DC51B86009CE2B4 /* IGTestSingleStoryboardItemDataSource.m in Sources */, 885FE2301DC51B76009CE2B4 /* IGListDiffTests.m in Sources */, 885FE22E1DC51B76009CE2B4 /* IGListBatchUpdateDataTests.m in Sources */, @@ -2253,6 +2258,7 @@ F1855A4C29BC565600558D18 /* IGListDiffDescriptionStringTests.m in Sources */, 821BC4D31DB981AB00172ED0 /* IGTestSingleStoryboardItemDataSource.m in Sources */, 298DDA3D1E3B170400F76F50 /* IGLayoutTestSection.m in Sources */, + 5766613E2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */, 298DDA091E3AE31D00F76F50 /* IGTestDiffingSectionController.m in Sources */, 88144F151D870EDC007C7F66 /* IGListTestSection.m in Sources */, 82914C5B1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m in Sources */, diff --git a/Source/IGListKit/IGListAdapter.m b/Source/IGListKit/IGListAdapter.m index 42d146582..edbfc84ce 100644 --- a/Source/IGListKit/IGListAdapter.m +++ b/Source/IGListKit/IGListAdapter.m @@ -14,6 +14,7 @@ #endif #import "IGListAdapterUpdater.h" +#import "IGListAdapterDelegateAnnouncer.h" #import "IGListArrayUtilsInternal.h" #import "IGListDebugger.h" #import "IGListDefaultExperiments.h" @@ -55,6 +56,7 @@ - (instancetype)initWithUpdater:(id )updater NSMapTable *table = [[NSMapTable alloc] initWithKeyPointerFunctions:keyFunctions valuePointerFunctions:valueFunctions capacity:0]; _sectionMap = [[IGListSectionMap alloc] initWithMapTable:table]; + _globalDelegateAnnouncer = [IGListAdapterDelegateAnnouncer sharedInstance]; _displayHandler = [IGListDisplayHandler new]; _workingRangeHandler = [[IGListWorkingRangeHandler alloc] initWithWorkingRangeSize:workingRangeSize]; _updateListeners = [NSHashTable weakObjectsHashTable]; diff --git a/Source/IGListKit/IGListAdapterDelegateAnnouncer.h b/Source/IGListKit/IGListAdapterDelegateAnnouncer.h new file mode 100644 index 000000000..d0ab30b1c --- /dev/null +++ b/Source/IGListKit/IGListAdapterDelegateAnnouncer.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "IGListAdapterDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListAdapterDelegateAnnouncer : NSObject + +/// Default announcer for all `IGListAdapter` ++ (instancetype)sharedInstance; + +/// Add a delegate that will receive callbacks for all `IGListAdapter`. +/// This is a weak reference, so you don't need to remove it on dealloc. +- (void)addListener:(id)listener; + +/// Remove delegate +- (void)removeListener:(id)listener; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/IGListAdapterDelegateAnnouncer.m b/Source/IGListKit/IGListAdapterDelegateAnnouncer.m new file mode 100644 index 000000000..32ce56f0b --- /dev/null +++ b/Source/IGListKit/IGListAdapterDelegateAnnouncer.m @@ -0,0 +1,49 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "IGListAdapterDelegateAnnouncerInternal.h" + +@implementation IGListAdapterDelegateAnnouncer { + NSHashTable> *_delegates; +} + ++ (instancetype)sharedInstance { + static IGListAdapterDelegateAnnouncer *shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + shared = [self new]; + }); + return shared; +} + +- (void)addListener:(id)listener { + if (!_delegates) { + _delegates = [NSHashTable weakObjectsHashTable]; + } + + [_delegates addObject:listener]; +} + +- (void)removeListener:(id)listener { + [_delegates removeObject:listener]; +} + +- (void)announceCellDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index { + for (id delegate in [_delegates allObjects]) { + [delegate listAdapter:listAdapter willDisplayObject:object atIndex:index]; + } +} + +- (void)announceCellEndDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index { + for (id delegate in [_delegates allObjects]) { + [delegate listAdapter:listAdapter didEndDisplayingObject:object atIndex:index]; + } +} + +@end diff --git a/Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h b/Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h new file mode 100644 index 000000000..9ad989034 --- /dev/null +++ b/Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapterDelegateAnnouncer.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListAdapterDelegateAnnouncer () + +- (void)announceCellDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index; +- (void)announceCellEndDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/Internal/IGListAdapterInternal.h b/Source/IGListKit/Internal/IGListAdapterInternal.h index 5d22cfc25..1c2843c72 100644 --- a/Source/IGListKit/Internal/IGListAdapterInternal.h +++ b/Source/IGListKit/Internal/IGListAdapterInternal.h @@ -47,6 +47,9 @@ IGListBatchContext @property (nonatomic, strong, nullable) IGListAdapterProxy *delegateProxy; +// Set as a property for unit testing +@property (nonatomic, strong, nullable) IGListAdapterDelegateAnnouncer *globalDelegateAnnouncer; + @property (nonatomic, strong, nullable) UIView *emptyBackgroundView; // We need to special case interactive section moves that are moved to the last position diff --git a/Source/IGListKit/Internal/IGListDisplayHandler.h b/Source/IGListKit/Internal/IGListDisplayHandler.h index eeae152da..8452ef358 100644 --- a/Source/IGListKit/Internal/IGListDisplayHandler.h +++ b/Source/IGListKit/Internal/IGListDisplayHandler.h @@ -15,6 +15,7 @@ @class IGListAdapter; @class IGListSectionController; +@class IGListAdapterDelegateAnnouncer; diff --git a/Source/IGListKit/Internal/IGListDisplayHandler.m b/Source/IGListKit/Internal/IGListDisplayHandler.m index fe23b4d32..aa470dc96 100644 --- a/Source/IGListKit/Internal/IGListDisplayHandler.m +++ b/Source/IGListKit/Internal/IGListDisplayHandler.m @@ -12,7 +12,8 @@ #else #import #endif -#import "IGListAdapter.h" +#import "IGListAdapterInternal.h" +#import "IGListAdapterDelegateAnnouncerInternal.h" #import "IGListDisplayDelegate.h" #import "IGListSectionController.h" #import "IGListSectionControllerInternal.h" @@ -55,6 +56,7 @@ - (void)_willDisplayReusableView:(UICollectionReusableView *)view if ([visibleListSections countForObject:sectionController] == 0) { [sectionController willDisplaySectionControllerWithListAdapter:listAdapter]; [listAdapter.delegate listAdapter:listAdapter willDisplayObject:object atIndex:indexPath.section]; + [listAdapter.globalDelegateAnnouncer announceCellDisplayWithAdapter:listAdapter object:object index:indexPath.section]; } [visibleListSections addObject:sectionController]; } @@ -80,6 +82,7 @@ - (void)_didEndDisplayingReusableView:(UICollectionReusableView *)view if ([visibleSections countForObject:sectionController] == 0) { [sectionController didEndDisplayingSectionControllerWithListAdapter:listAdapter]; [listAdapter.delegate listAdapter:listAdapter didEndDisplayingObject:object atIndex:section]; + [listAdapter.globalDelegateAnnouncer announceCellEndDisplayWithAdapter:listAdapter object:object index:indexPath.section]; } } diff --git a/Tests/IGListAdapterDelegateAnnouncerTests.m b/Tests/IGListAdapterDelegateAnnouncerTests.m new file mode 100644 index 000000000..64eaa48a9 --- /dev/null +++ b/Tests/IGListAdapterDelegateAnnouncerTests.m @@ -0,0 +1,161 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +#import + +#import "IGListAdapterInternal.h" +#import "IGListAdapterUpdater.h" +#import "IGListTestHelpers.h" +#import "IGTestDelegateDataSource.h" +#import "IGTestObject.h" +#import "IGListAdapterDelegateAnnouncer.h" + +@interface IGListAdapterDelegateAnnouncerTests : XCTestCase + +// These objects are created for you in -setUp +@property (nonatomic, strong) UIWindow *window; +@property (nonatomic, strong) UIViewController *viewController; +@property (nonatomic, strong) IGListAdapterDelegateAnnouncer *announcer; + +@property (nonatomic, strong) UICollectionView *collectionView1; +@property (nonatomic, strong) UICollectionView *collectionView2; +@property (nonatomic, strong) id dataSource1; +@property (nonatomic, strong) id dataSource2; +@property (nonatomic, strong) IGListAdapter *adapter1; +@property (nonatomic, strong) IGListAdapter *adapter2; + +@end + +@implementation IGListAdapterDelegateAnnouncerTests + +- (void)setUp { + [super setUp]; + + self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + self.viewController = [UIViewController new]; + self.announcer = [IGListAdapterDelegateAnnouncer new]; + + self.collectionView1 = [[UICollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:[UICollectionViewFlowLayout new]]; + [self.window addSubview:self.collectionView1]; + + self.collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:[UICollectionViewFlowLayout new]]; + [self.window addSubview:self.collectionView2]; + + self.dataSource1 = [IGTestDelegateDataSource new]; + self.dataSource2 = [IGTestDelegateDataSource new]; + + self.adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:self.viewController]; + self.adapter1.globalDelegateAnnouncer = self.announcer; + + self.adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:self.viewController]; + self.adapter2.globalDelegateAnnouncer = self.announcer; +} + +- (void)setupAdapter1WithObjects:(NSArray *)objects { + self.dataSource1.objects = objects; + self.adapter1.collectionView = self.collectionView1; + self.adapter1.dataSource = self.dataSource1; + [self.collectionView1 layoutIfNeeded]; +} + +- (void)setupAdapter2WithObjects:(NSArray *)objects { + self.dataSource2.objects = objects; + self.adapter2.collectionView = self.collectionView2; + self.adapter2.dataSource = self.dataSource2; + [self.collectionView2 layoutIfNeeded]; +} + +#pragma mark - Single adapter, multiple listeners + +- (void)test_whenShowingOneItem_withTwoListeners_withOneAdapter_thatBothListenersReceivesWillDisplay{ + [self setupAdapter1WithObjects:@[]]; + + IGTestObject *const object = genTestObject(@1, @1); + self.dataSource1.objects = @[ + object + ]; + + id mockDisplayHandler1 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; + [self.announcer addListener:mockDisplayHandler1]; + [[mockDisplayHandler1 expect] listAdapter:self.adapter1 willDisplayObject:object atIndex:0]; + + id mockDisplayHandler2 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; + [self.announcer addListener:mockDisplayHandler2]; + [[mockDisplayHandler2 expect] listAdapter:self.adapter1 willDisplayObject:object atIndex:0]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter1 performUpdatesAnimated:NO completion:^(BOOL finished2) { + [mockDisplayHandler1 verify]; + [mockDisplayHandler2 verify]; + XCTAssertTrue(finished2); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenRemovignOneItem_withTwoListeners_withOneAdapter_thatBothListenersReceivesEndDisplay { + IGTestObject *const object = genTestObject(@1, @1); + [self setupAdapter1WithObjects:@[object]]; + + self.dataSource1.objects = @[]; + + id mockDisplayHandler1 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; + [self.announcer addListener:mockDisplayHandler1]; + [[mockDisplayHandler1 expect] listAdapter:self.adapter1 didEndDisplayingObject:object atIndex:0]; + + id mockDisplayHandler2 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; + [self.announcer addListener:mockDisplayHandler2]; + [[mockDisplayHandler2 expect] listAdapter:self.adapter1 didEndDisplayingObject:object atIndex:0]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter1 performUpdatesAnimated:NO completion:^(BOOL finished2) { + [mockDisplayHandler1 verify]; + [mockDisplayHandler2 verify]; + XCTAssertTrue(finished2); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +#pragma mark - Two adapters, single listener + +- (void)test_whenShowingTwoItems_withOneListeners_withTwoAdapters_thatBothItemsSendWillDisplay { + [self setupAdapter1WithObjects:@[]]; + [self setupAdapter2WithObjects:@[]]; + + IGTestObject *const object1 = genTestObject(@1, @1); + self.dataSource1.objects = @[ + object1 + ]; + + IGTestObject *const object2 = genTestObject(@1, @1); + self.dataSource2.objects = @[ + object2 + ]; + + id mockDisplayHandler = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; + [self.announcer addListener:mockDisplayHandler]; + [[mockDisplayHandler expect] listAdapter:self.adapter1 willDisplayObject:object1 atIndex:0]; + [[mockDisplayHandler expect] listAdapter:self.adapter2 willDisplayObject:object2 atIndex:0]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter1 performUpdatesAnimated:NO completion:^(BOOL finished1) { + [self.adapter2 performUpdatesAnimated:NO completion:^(BOOL finished2) { + [mockDisplayHandler verify]; + XCTAssertTrue(finished1); + XCTAssertTrue(finished2); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +@end diff --git a/spm/Sources/IGListKit/IGListAdapterDelegateAnnouncer.m b/spm/Sources/IGListKit/IGListAdapterDelegateAnnouncer.m new file mode 120000 index 000000000..67497fd23 --- /dev/null +++ b/spm/Sources/IGListKit/IGListAdapterDelegateAnnouncer.m @@ -0,0 +1 @@ +../../../Source/IGListKit/IGListAdapterDelegateAnnouncer.m \ No newline at end of file diff --git a/spm/Sources/IGListKit/IGListAdapterDelegateAnnouncerInternal.h b/spm/Sources/IGListKit/IGListAdapterDelegateAnnouncerInternal.h new file mode 120000 index 000000000..ffd76f27e --- /dev/null +++ b/spm/Sources/IGListKit/IGListAdapterDelegateAnnouncerInternal.h @@ -0,0 +1 @@ +../../../Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h \ No newline at end of file diff --git a/spm/Sources/IGListKit/include/IGListAdapterDelegateAnnouncer.h b/spm/Sources/IGListKit/include/IGListAdapterDelegateAnnouncer.h new file mode 120000 index 000000000..c59a56bcc --- /dev/null +++ b/spm/Sources/IGListKit/include/IGListAdapterDelegateAnnouncer.h @@ -0,0 +1 @@ +../../../../Source/IGListKit/IGListAdapterDelegateAnnouncer.h \ No newline at end of file