-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
4bad7d5
commit 636f6b5
Showing
12 changed files
with
276 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <Foundation/Foundation.h> | ||
|
||
#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<IGListAdapterDelegate>)listener; | ||
|
||
/// Remove delegate | ||
- (void)removeListener:(id<IGListAdapterDelegate>)listener; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <Foundation/Foundation.h> | ||
|
||
#import "IGListAdapterDelegateAnnouncerInternal.h" | ||
|
||
@implementation IGListAdapterDelegateAnnouncer { | ||
NSHashTable<id<IGListAdapterDelegate>> *_delegates; | ||
} | ||
|
||
+ (instancetype)sharedInstance { | ||
static IGListAdapterDelegateAnnouncer *shared = nil; | ||
static dispatch_once_t onceToken; | ||
dispatch_once(&onceToken, ^{ | ||
shared = [self new]; | ||
}); | ||
return shared; | ||
} | ||
|
||
- (void)addListener:(id<IGListAdapterDelegate>)listener { | ||
if (!_delegates) { | ||
_delegates = [NSHashTable weakObjectsHashTable]; | ||
} | ||
|
||
[_delegates addObject:listener]; | ||
} | ||
|
||
- (void)removeListener:(id<IGListAdapterDelegate>)listener { | ||
[_delegates removeObject:listener]; | ||
} | ||
|
||
- (void)announceCellDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index { | ||
for (id<IGListAdapterDelegate> delegate in [_delegates allObjects]) { | ||
[delegate listAdapter:listAdapter willDisplayObject:object atIndex:index]; | ||
} | ||
} | ||
|
||
- (void)announceCellEndDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index { | ||
for (id<IGListAdapterDelegate> delegate in [_delegates allObjects]) { | ||
[delegate listAdapter:listAdapter didEndDisplayingObject:object atIndex:index]; | ||
} | ||
} | ||
|
||
@end |
19 changes: 19 additions & 0 deletions
19
Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
|
||
@class IGListAdapter; | ||
@class IGListSectionController; | ||
@class IGListAdapterDelegateAnnouncer; | ||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <XCTest/XCTest.h> | ||
|
||
#import <OCMock/OCMock.h> | ||
|
||
#import <IGListKit/IGListKit.h> | ||
|
||
#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<IGListTestCaseDataSource> dataSource1; | ||
@property (nonatomic, strong) id<IGListTestCaseDataSource> 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../Source/IGListKit/IGListAdapterDelegateAnnouncer.m |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../Source/IGListKit/IGListAdapterDelegateAnnouncer.h |