From 43af8838dfdcfc50b8145c56cfecb5f5ed9195a5 Mon Sep 17 00:00:00 2001 From: Maxime Ollivier Date: Thu, 21 Jan 2021 19:55:59 -0800 Subject: [PATCH] merge IGListUpdatingDelegateExperimental into IGListUpdatingDelegate Summary: Now that the new updater has shipped, lets update `IGListUpdatingDelegate` with the new methods: * `-performExperimentalUpdateAnimated` is the new section update method (renaming coming in the next diff) * `-performDataSourceChange` lets us safely update the `UICollectionView` dataSource Also, something I've been wanting to do for a long time, lets group related methods in `IGListUpdatingDelegate.h`. Reviewed By: Haud Differential Revision: D25884780 fbshipit-source-id: 5d9201ace8bf6b281d71ff03463cb7c911e7f967 --- Source/IGListKit/IGListAdapter.m | 164 +++++------------- Source/IGListKit/IGListAdapterUpdater.h | 3 +- Source/IGListKit/IGListAdapterUpdater.m | 10 -- Source/IGListKit/IGListReloadDataUpdater.m | 39 +++-- Source/IGListKit/IGListUpdatingDelegate.h | 85 +++++---- .../IGListUpdatingDelegateExperimental.h | 54 ------ .../Internal/IGListBatchUpdateTransaction.h | 1 - .../Internal/IGListUpdateTransactionBuilder.h | 1 - Tests/IGListAdapterTests.m | 10 +- 9 files changed, 127 insertions(+), 240 deletions(-) delete mode 100644 Source/IGListKit/IGListUpdatingDelegateExperimental.h diff --git a/Source/IGListKit/IGListAdapter.m b/Source/IGListKit/IGListAdapter.m index 3e865512..53f8ea09 100644 --- a/Source/IGListKit/IGListAdapter.m +++ b/Source/IGListKit/IGListAdapter.m @@ -15,7 +15,7 @@ #import "IGListDebugger.h" #import "IGListSectionControllerInternal.h" #import "IGListTransitionData.h" -#import "IGListUpdatingDelegateExperimental.h" +#import "IGListUpdatingDelegate.h" #import "UICollectionViewLayout+InteractiveReordering.h" #import "UIScrollView+IGListKit.h" @@ -24,8 +24,6 @@ // An array of blocks to execute once batch updates are finished NSMutableArray *_queuedCompletionBlocks; NSHashTable> *_updateListeners; - // Temporary property while we experiment with a new updater. - id _experimentalUpdater; } - (void)dealloc { @@ -56,7 +54,6 @@ _updater = updater; _viewController = viewController; - _experimentalUpdater = [updater conformsToProtocol:@protocol(IGListUpdatingDelegateExperimental)] ? (id)updater : nil; [IGListDebugger trackAdapter:self]; } @@ -97,35 +94,26 @@ _registeredSupplementaryViewIdentifiers = [NSMutableSet new]; _registeredSupplementaryViewNibNames = [NSMutableSet new]; - const BOOL settingFirstCollectionView = _collectionView == nil; + // We can't just swap out the collectionView, because we might have on-going or pending updates. + // `_updater` can take care of that by wrapping the change in `performDataSourceChange`. + [_updater performDataSourceChange:^{ + if (self->_collectionView.dataSource == self) { + // Since we're not going to sync the previous collectionView anymore, lets not be its dataSource. + self->_collectionView.dataSource = nil; + } + self->_collectionView = collectionView; + self->_collectionView.dataSource = self; - if (_experimentalUpdater) { - // We can't just swap out the collectionView, because we might have on-going or pending updates. - // `_experimentalUpdater` can take care of that by wrapping the change in `performDataSourceChange`. - - [_experimentalUpdater performDataSourceChange:^{ - if (self->_collectionView.dataSource == self) { - // Since we're not going to sync the previous collectionView anymore, lets not be its dataSource. - self->_collectionView.dataSource = nil; - } - self->_collectionView = collectionView; - self->_collectionView.dataSource = self; - - [self _updateCollectionViewDelegate]; - - // Sync the dataSource <> adapter for a couple of reasons: - // 1. We might not have synced on -setDataSource, so now is the time to try again. - // 2. Any in-flight `performUpdatesAnimated` were cancelled, so lets make sure we have the latest data. - [self _updateObjects]; - - // The sync between the collectionView <> adapter will happen automically, since - // we just changed the `collectionView.dataSource`. - }]; - } else { - _collectionView = collectionView; - _collectionView.dataSource = self; [self _updateCollectionViewDelegate]; - } + + // Sync the dataSource <> adapter for a couple of reasons: + // 1. We might not have synced on -setDataSource, so now is the time to try again. + // 2. Any in-flight `performUpdatesAnimated` were cancelled, so lets make sure we have the latest data. + [self _updateObjects]; + + // The sync between the collectionView <> adapter will happen automically, since + // we just changed the `collectionView.dataSource`. + }]; if (@available(iOS 10.0, tvOS 10, *)) { _collectionView.prefetchingEnabled = NO; @@ -133,10 +121,6 @@ [_collectionView.collectionViewLayout ig_hijackLayoutInteractiveReorderingMethodForAdapter:self]; [_collectionView.collectionViewLayout invalidateLayout]; - - if (!_experimentalUpdater && settingFirstCollectionView) { - [self _updateObjectsIfHasDataSource]; - } } } @@ -145,24 +129,19 @@ return; } - if (_experimentalUpdater) { - [_experimentalUpdater performDataSourceChange:^{ - self->_dataSource = dataSource; + [_updater performDataSourceChange:^{ + self->_dataSource = dataSource; - // Invalidate the collectionView internal section & item counts, as if its dataSource changed. - self->_collectionView.dataSource = nil; - self->_collectionView.dataSource = self; + // Invalidate the collectionView internal section & item counts, as if its dataSource changed. + self->_collectionView.dataSource = nil; + self->_collectionView.dataSource = self; - // Sync the dataSource <> adapter - [self _updateObjects]; + // Sync the dataSource <> adapter + [self _updateObjects]; - // The sync between the collectionView <> adapter will happen automically, since - // we just changed the `collectionView.dataSource`. - }]; - } else { - _dataSource = dataSource; - [self _updateObjectsIfHasDataSource]; - } + // The sync between the collectionView <> adapter will happen automically, since + // we just changed the `collectionView.dataSource`. + }]; } // reset and configure the delegate proxy whenever this property is set @@ -186,13 +165,6 @@ } } -- (void)_updateObjectsIfHasDataSource { - // This is to keep the existing logic while testing `experimentalUpdater` - if (_dataSource != nil) { - [self _updateObjects]; - } -} - - (void)_updateObjects { if (_collectionView == nil) { // If we don't have a collectionView, we can't do much. @@ -375,66 +347,8 @@ return; } - __weak __typeof__(self) weakSelf = self; - IGListUpdaterCompletion outerCompletionBlock = ^(BOOL finished){ - __typeof__(self) strongSelf = weakSelf; - if (strongSelf == nil) { - IGLK_BLOCK_CALL_SAFE(completion,finished); - return; - } - - // release the previous items - strongSelf.previousSectionMap = nil; - [strongSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated]; - IGLK_BLOCK_CALL_SAFE(completion,finished); - [strongSelf _exitBatchUpdates]; - }; - [self _enterBatchUpdates]; - if (_experimentalUpdater) { - [self _performExperimentalUpdatesWithUpdater:_experimentalUpdater - dataSource:dataSource - animated:animated - completion:outerCompletionBlock]; - } else { - [self _performRegularUpdatesWithUpdater:updater - dataSource:dataSource - animated:animated - completion:outerCompletionBlock]; - } -} -- (void)_performRegularUpdatesWithUpdater:(id)updater - dataSource:(id)dataSource - animated:(BOOL)animated - completion:(IGListUpdaterCompletion)completion { - NSArray *fromObjects = self.sectionMap.objects; - - __weak __typeof__(self) weakSelf = self; - IGListToObjectBlock toObjectsBlock = ^NSArray *{ - __typeof__(self) strongSelf = weakSelf; - if (strongSelf == nil) { - return nil; - } - return [dataSource objectsForListAdapter:strongSelf]; - }; - - [updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock] - fromObjects:fromObjects - toObjectsBlock:toObjectsBlock - animated:animated - objectTransitionBlock:^(NSArray *toObjects) { - // temporarily capture the item map that we are transitioning from in case - // there are any item deletes at the same - weakSelf.previousSectionMap = [weakSelf.sectionMap copy]; - [weakSelf _updateObjects:toObjects dataSource:dataSource]; - } completion:completion]; -} - -- (void)_performExperimentalUpdatesWithUpdater:(id)updater - dataSource:(id)dataSource - animated:(BOOL)animated - completion:(IGListUpdaterCompletion)completion { __weak __typeof__(self) weakSelf = self; IGListTransitionDataBlock dataBlock = ^IGListTransitionData *{ __typeof__(self) strongSelf = weakSelf; @@ -452,15 +366,29 @@ } // temporarily capture the item map that we are transitioning from in case // there are any item deletes at the same - strongSelf.previousSectionMap = [weakSelf.sectionMap copy]; + strongSelf.previousSectionMap = [strongSelf.sectionMap copy]; [strongSelf _updateWithData:data]; }; + IGListUpdaterCompletion outerCompletionBlock = ^(BOOL finished){ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + IGLK_BLOCK_CALL_SAFE(completion,finished); + return; + } + + // release the previous items + strongSelf.previousSectionMap = nil; + [strongSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated]; + IGLK_BLOCK_CALL_SAFE(completion,finished); + [strongSelf _exitBatchUpdates]; + }; + [updater performExperimentalUpdateAnimated:animated collectionViewBlock:[self _collectionViewBlock] dataBlock:dataBlock applyDataBlock:applyDataBlock - completion:completion]; + completion:outerCompletionBlock]; } - (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion { @@ -795,7 +723,7 @@ return; // will be called again when update block completes } - if (!shouldHide || !_experimentalUpdater) { + if (!shouldHide) { UIView *backgroundView = [self.dataSource emptyViewForListAdapter:self]; // don't do anything if the client is using the same view if (backgroundView != _collectionView.backgroundView) { diff --git a/Source/IGListKit/IGListAdapterUpdater.h b/Source/IGListKit/IGListAdapterUpdater.h index 629e4a74..e330abf4 100644 --- a/Source/IGListKit/IGListAdapterUpdater.h +++ b/Source/IGListKit/IGListAdapterUpdater.h @@ -9,7 +9,6 @@ #import #import -#import NS_ASSUME_NONNULL_BEGIN @@ -23,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN */ IGLK_SUBCLASSING_RESTRICTED NS_SWIFT_NAME(ListAdapterUpdater) -@interface IGListAdapterUpdater : NSObject +@interface IGListAdapterUpdater : NSObject @end diff --git a/Source/IGListKit/IGListAdapterUpdater.m b/Source/IGListKit/IGListAdapterUpdater.m index b5041ae8..448a856a 100644 --- a/Source/IGListKit/IGListAdapterUpdater.m +++ b/Source/IGListKit/IGListAdapterUpdater.m @@ -147,16 +147,6 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons return functions; } -- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - fromObjects:(NSArray *)fromObjects - toObjectsBlock:(IGListToObjectBlock)toObjectsBlock - animated:(BOOL)animated - objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock - completion:(IGListUpdatingCompletion)completion { - IGFailAssert(@"IGListExperimentalAdapterUpdater works with IGListUpdatingDelegateExperimental and doesn't implement the regular -performUpdateWithCollectionViewBlock method"); - completion(NO); -} - - (void)performExperimentalUpdateAnimated:(BOOL)animated collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock dataBlock:(IGListTransitionDataBlock)dataBlock diff --git a/Source/IGListKit/IGListReloadDataUpdater.m b/Source/IGListKit/IGListReloadDataUpdater.m index 851d95d9..7bc39aa3 100644 --- a/Source/IGListKit/IGListReloadDataUpdater.m +++ b/Source/IGListKit/IGListReloadDataUpdater.m @@ -15,15 +15,14 @@ return [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality]; } -- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - fromObjects:(NSArray *)fromObjects - toObjectsBlock:(IGListToObjectBlock)toObjectsBlock - animated:(BOOL)animated - objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock - completion:(IGListUpdatingCompletion)completion { - if (toObjectsBlock != nil) { - NSArray *toObjects = toObjectsBlock() ?: @[]; - objectTransitionBlock(toObjects); +- (void)performExperimentalUpdateAnimated:(BOOL)animated + collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + dataBlock:(IGListTransitionDataBlock)dataBlock + applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock + completion:(nullable IGListUpdatingCompletion)completion { + IGListTransitionData *sectionData = dataBlock ? dataBlock() : nil; + if (sectionData != nil) { + applyDataBlock(sectionData); } [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; if (completion) { @@ -42,6 +41,20 @@ } } +- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block { + // A `UICollectionView` dataSource change will automatically invalidate + // its data, so no need to do anything else. + block(); +} + +- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion { + reloadUpdateBlock(); + [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; + if (completion) { + completion(YES); + } +} + - (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { [self _synchronousReloadDataWithCollectionView:collectionView]; } @@ -62,14 +75,6 @@ [self _synchronousReloadDataWithCollectionView:collectionView]; } -- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion { - reloadUpdateBlock(); - [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; - if (completion) { - completion(YES); - } -} - - (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { [self _synchronousReloadDataWithCollectionView:collectionView]; } diff --git a/Source/IGListKit/IGListUpdatingDelegate.h b/Source/IGListKit/IGListUpdatingDelegate.h index 8b4505b4..70ce07fa 100644 --- a/Source/IGListKit/IGListUpdatingDelegate.h +++ b/Source/IGListKit/IGListUpdatingDelegate.h @@ -7,6 +7,7 @@ #import +@class IGListTransitionData; @protocol IGListDiffable; NS_ASSUME_NONNULL_BEGIN @@ -47,6 +48,18 @@ typedef UICollectionView * _Nullable (^IGListCollectionViewBlock)(void); NS_SWIFT_NAME(ListDataSourceChangeBlock) typedef void (^IGListDataSourceChangeBlock)(void); +/// A block that returns the `IGListTransitionData` needed before an update. +NS_SWIFT_NAME(ListTransitionDataBlock) +typedef IGListTransitionData * _Nullable (^IGListTransitionDataBlock)(void); + +/** + A block to be called when the adapter applies changes to the collection view. + + @param data The new data that contains the from/to objects. + */ +NS_SWIFT_NAME(ListTransitionDataApplyBlock) +typedef void (^IGListTransitionDataApplyBlock)(IGListTransitionData *data); + /** Implement this protocol in order to handle both section and row based update events. Implementation should forward or coalesce these events to a backing store or collection. @@ -69,29 +82,59 @@ NS_SWIFT_NAME(ListUpdatingDelegate) - (NSPointerFunctions *)objectLookupPointerFunctions; /** - Tells the delegate to perform a section transition from an old array of objects to a new one. + Perform a **section** update from an old array of objects to a new one. - @param collectionViewBlock A block returning the collecion view to perform updates on. - @param fromObjects The previous objects in the collection view. Objects must conform to `IGListDiffable`. - @param toObjectsBlock A block returning the new objects in the collection view. Objects must conform to `IGListDiffable`. @param animated A flag indicating if the transition should be animated. - @param objectTransitionBlock A block that must be called when the adapter applies changes to the collection view. + @param collectionViewBlock A block returning the collecion view to perform updates on. + @param dataBlock A block that returns the section information (ex: from and to objects) + @param applyDataBlock A block that must be called when the adapter applies changes to the collection view. @param completion A completion block to execute when the update is finished. @note Implementations determine how to transition between objects. You can perform a diff on the objects, reload each section, or simply call `-reloadData` on the collection view. In the end, the collection view must be setup with a section for each object in the `toObjects` array. - The `objectTransitionBlock` block should be called prior to making any `UICollectionView` updates, passing in the `toObjects` + The `applyDataBlock` block should be called prior to making any `UICollectionView` updates, passing in the `toObjects` that the updater is applying. */ +- (void)performExperimentalUpdateAnimated:(BOOL)animated + collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + dataBlock:(IGListTransitionDataBlock)dataBlock + applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock + completion:(nullable IGListUpdatingCompletion)completion; + +/** + Perform an **item** update block in the collection view. + + @param collectionViewBlock A block returning the collecion view to perform updates on. + @param animated A flag indicating if the transition should be animated. + @param itemUpdates A block containing all of the updates. + @param completion A completion block to execute when the update is finished. + */ - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - fromObjects:(nullable NSArray> *)fromObjects - toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock animated:(BOOL)animated - objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock + itemUpdates:(IGListItemUpdateBlock)itemUpdates completion:(nullable IGListUpdatingCompletion)completion; +/** + Perform a `[UICollectionView setDataSource:...]` swap within this block. It gives the updater the chance to cancel or + execute any on-going updates. The block should be executed synchronously. + + @param block The block that will actuallty change the `dataSource` + */ +- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block; + +/** + Completely reload data in the collection. + + @param collectionViewBlock A block returning the collecion view to reload. + @param reloadUpdateBlock A block that must be called when the adapter reloads the collection view. + @param completion A completion block to execute when the reload is finished. + */ +- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock + completion:(nullable IGListUpdatingCompletion)completion; + /** Tells the delegate to perform item inserts at the given index paths. @@ -145,17 +188,6 @@ NS_SWIFT_NAME(ListUpdatingDelegate) fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex; -/** - Completely reload data in the collection. - - @param collectionViewBlock A block returning the collecion view to reload. - @param reloadUpdateBlock A block that must be called when the adapter reloads the collection view. - @param completion A completion block to execute when the reload is finished. - */ -- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock - completion:(nullable IGListUpdatingCompletion)completion; - /** Completely reload each section in the collection view. @@ -164,19 +196,6 @@ NS_SWIFT_NAME(ListUpdatingDelegate) */ - (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections; -/** - Perform an item update block in the collection view. - - @param collectionViewBlock A block returning the collecion view to perform updates on. - @param animated A flag indicating if the transition should be animated. - @param itemUpdates A block containing all of the updates. - @param completion A completion block to execute when the update is finished. - */ -- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - animated:(BOOL)animated - itemUpdates:(IGListItemUpdateBlock)itemUpdates - completion:(nullable IGListUpdatingCompletion)completion; - @end NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/IGListUpdatingDelegateExperimental.h b/Source/IGListKit/IGListUpdatingDelegateExperimental.h deleted file mode 100644 index d7888a07..00000000 --- a/Source/IGListKit/IGListUpdatingDelegateExperimental.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) Facebook, Inc. and its 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 "IGListUpdatingDelegate.h" - -@class IGListTransitionData; - -NS_ASSUME_NONNULL_BEGIN - -/// A block that returns the `IGListTransitionData` needed before an update. -NS_SWIFT_NAME(ListTransitionDataBlock) -typedef IGListTransitionData * _Nullable (^IGListTransitionDataBlock)(void); - -/** - A block to be called when the adapter applies changes to the collection view. - - @param data The new data that contains the from/to objects. - */ -NS_SWIFT_NAME(ListTransitionDataApplyBlock) -typedef void (^IGListTransitionDataApplyBlock)(IGListTransitionData *data); - -/** - Temporary experimental version of `IGListUpdatingDelegate` - */ -NS_SWIFT_NAME(ListUpdatingDelegateExperimental) -@protocol IGListUpdatingDelegateExperimental - -/** - Experimental version of `performUpdateWithCollectionViewBlock` that uses `IGListTransitionData` to make updates safer. - The adapter will use this method instead of the regular `performUpdateWithCollectionViewBlock` if implemented. - */ -- (void)performExperimentalUpdateAnimated:(BOOL)animated - collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - dataBlock:(IGListTransitionDataBlock)dataBlock - applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock - completion:(nullable IGListUpdatingCompletion)completion; - -/** - Perform a `[UICollectionView setDataSource:...]` swap within this block. It gives the updater the chance to cancel or - execute any on-going updates. The block will be executed synchronously. - - @param block The block that will actuallty change the `dataSource` - */ -- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/Internal/IGListBatchUpdateTransaction.h b/Source/IGListKit/Internal/IGListBatchUpdateTransaction.h index 72449e67..1d059647 100644 --- a/Source/IGListKit/Internal/IGListBatchUpdateTransaction.h +++ b/Source/IGListKit/Internal/IGListBatchUpdateTransaction.h @@ -9,7 +9,6 @@ #import #import -#import #import "IGListUpdateTransactable.h" diff --git a/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h b/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h index 595a031b..771c4bef 100644 --- a/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h +++ b/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h @@ -9,7 +9,6 @@ #import #import -#import #import "IGListUpdateTransactable.h" diff --git a/Tests/IGListAdapterTests.m b/Tests/IGListAdapterTests.m index d852d795..8cdcd594 100644 --- a/Tests/IGListAdapterTests.m +++ b/Tests/IGListAdapterTests.m @@ -176,8 +176,8 @@ ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = background; __block BOOL executed = NO; [self.adapter reloadDataWithCompletion:^(BOOL finished) { - XCTAssertTrue(self.adapter.collectionView.backgroundView.hidden, @"Background view should be hidden"); - XCTAssertEqualObjects(background, self.adapter.collectionView.backgroundView, @"Background view not correctly assigned"); + UIView *backgroundViewAfterReload = self.adapter.collectionView.backgroundView; + XCTAssertTrue(!backgroundViewAfterReload || backgroundViewAfterReload.hidden, @"Background view should be hidden"); self.dataSource.objects = @[]; [self.adapter reloadDataWithCompletion:^(BOOL finished2) { @@ -358,7 +358,8 @@ self.dataSource.objects = @[@1]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertTrue(self.collectionView.backgroundView.hidden); + UIView *backgroundView = self.adapter.collectionView.backgroundView; + XCTAssertTrue(!backgroundView || backgroundView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(1)]; sectionController.items = 0; [self.adapter deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; @@ -380,7 +381,8 @@ self.dataSource.objects = @[@1, @2]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertTrue(self.collectionView.backgroundView.hidden); + UIView *backgroundView = self.adapter.collectionView.backgroundView; + XCTAssertTrue(!backgroundView || backgroundView.hidden); IGListTestSection *firstSectionController = [self.adapter sectionControllerForObject:@(1)]; IGListTestSection *secondSectionController = [self.adapter sectionControllerForObject:@(2)]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];