Skip to content

Commit

Permalink
Merge branch 'master' into scroll-bottom-inset
Browse files Browse the repository at this point in the history
  • Loading branch information
rnystrom committed Sep 21, 2017
2 parents f7faca6 + 4f970d2 commit d47da08
Show file tree
Hide file tree
Showing 20 changed files with 439 additions and 35 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,12 +5,20 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag
3.2.0 (upcoming release)
-----

### Enhancements

- 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)

### 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)

- Fix bug with `-[IGListAdapter scrollToObject:supplementaryKinds:scrollDirection:scrollPosition:animated:]` where the content inset of the collection view was incorrectly being applied to the final offset. [Ryan Nystrom](https://github.com/rnystrom) [(#tbd)](https://github.com/Instagram/IGListKit/pull/tbd)

- Avoid crash when invalidating the layout while inside `-[UICollectionView performBatchUpdates:completion:]. [Ryan Nystrom](https://github.com/rnystrom) [(#tbd)](https://github.com/Instagram/IGListKit/pull/tbd)

- Duplicate view models in `IGListBindingSectionController` gets filtered out. [Weyert de Boer](https://github.com/weyert) [(#916)](https://github.com/Instagram/IGListKit/pull/916)

3.1.1
-----

Expand Down
8 changes: 8 additions & 0 deletions IGListKit.xcodeproj/project.pbxproj
Expand Up @@ -258,6 +258,9 @@
29DA5CA81EA7D37000113926 /* IGListTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA61EA7D37000113926 /* IGListTestCase.m */; };
29EA6C491DB43A8000957A88 /* IGTestNibCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */; };
64D8007E592D0292BE4FC21D /* Pods_IGListKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68CC152B514785B3113DDD4A /* Pods_IGListKitTests.framework */; };
7BC0C61B1F5C401F00A06ADD /* IGListArrayUtilsInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BC0C61A1F5C401F00A06ADD /* IGListArrayUtilsInternal.h */; };
7BC0C61C1F5C402600A06ADD /* IGListArrayUtilsInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BC0C61A1F5C401F00A06ADD /* IGListArrayUtilsInternal.h */; };
7BC0C61D1F5C402600A06ADD /* IGListArrayUtilsInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BC0C61A1F5C401F00A06ADD /* IGListArrayUtilsInternal.h */; };
821BC4C01DB8C9D500172ED0 /* IGListSingleStoryboardItemControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */; };
821BC4C41DB8CEF800172ED0 /* IGTestStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 821BC4C21DB8CAE900172ED0 /* IGTestStoryboard.storyboard */; };
821BC4CB1DB8D60100172ED0 /* IGTestStoryboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4C81DB8D5B200172ED0 /* IGTestStoryboardViewController.m */; };
Expand Down Expand Up @@ -498,6 +501,7 @@
4883B4E449DE18EC9C37773B /* Pods-IGListKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IGListKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-IGListKitTests/Pods-IGListKitTests.debug.xcconfig"; sourceTree = "<group>"; };
68CC152B514785B3113DDD4A /* Pods_IGListKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6DBA851B0E1DFDC2E7BDA472 /* Pods-IGListKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IGListKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-IGListKitTests/Pods-IGListKitTests.release.xcconfig"; sourceTree = "<group>"; };
7BC0C61A1F5C401F00A06ADD /* IGListArrayUtilsInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListArrayUtilsInternal.h; sourceTree = "<group>"; };
821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSingleStoryboardItemControllerTests.m; sourceTree = "<group>"; };
821BC4C21DB8CAE900172ED0 /* IGTestStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = IGTestStoryboard.storyboard; sourceTree = "<group>"; };
821BC4C71DB8D5B200172ED0 /* IGTestStoryboardViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestStoryboardViewController.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -700,6 +704,7 @@
0B3B92931E08D7F5008390ED /* IGListIndexSetResultInternal.h */,
0B3B92941E08D7F5008390ED /* IGListMoveIndexInternal.h */,
0B3B92951E08D7F5008390ED /* IGListMoveIndexPathInternal.h */,
7BC0C61A1F5C401F00A06ADD /* IGListArrayUtilsInternal.h */,
);
path = Internal;
sourceTree = "<group>";
Expand Down Expand Up @@ -932,6 +937,7 @@
294652BA1EA927750063BDD9 /* IGListDebuggingUtilities.h in Headers */,
0B3B93351E08D7F5008390ED /* IGListDisplayHandler.h in Headers */,
0B3B92D31E08D7F5008390ED /* IGListIndexPathResult.h in Headers */,
7BC0C61C1F5C402600A06ADD /* IGListArrayUtilsInternal.h in Headers */,
0B3B93251E08D7F5008390ED /* IGListSupplementaryViewSource.h in Headers */,
0B3B92F31E08D7F5008390ED /* NSString+IGListDiffable.h in Headers */,
0B3B932D1E08D7F5008390ED /* IGListAdapterInternal.h in Headers */,
Expand Down Expand Up @@ -1014,6 +1020,7 @@
0B3B92D01E08D7F5008390ED /* IGListExperiments.h in Headers */,
296AC95C1EA518D3005137E2 /* IGListReloadIndexPath.h in Headers */,
0B3B93321E08D7F5008390ED /* IGListAdapterUpdaterInternal.h in Headers */,
7BC0C61B1F5C401F00A06ADD /* IGListArrayUtilsInternal.h in Headers */,
DA5F484B1E8E9D7000DAE6DA /* IGListAdapter+UICollectionView.h in Headers */,
0B3B93381E08D7F5008390ED /* IGListSectionControllerInternal.h in Headers */,
2926586F1E75E0830041B56D /* IGListBindingSectionControllerSelectionDelegate.h in Headers */,
Expand Down Expand Up @@ -1058,6 +1065,7 @@
0B3B934B1E08D82E008390ED /* IGListMoveIndexPathInternal.h in Headers */,
0B3B93521E08D839008390ED /* IGListIndexSetResult.h in Headers */,
0B3B934C1E08D839008390ED /* IGListAssert.h in Headers */,
7BC0C61D1F5C402600A06ADD /* IGListArrayUtilsInternal.h in Headers */,
292658551E7498220041B56D /* IGListKit.h in Headers */,
0B3B93561E08D839008390ED /* IGListMoveIndexPath.h in Headers */,
0B3B93511E08D839008390ED /* IGListIndexPathResult.h in Headers */,
Expand Down
33 changes: 33 additions & 0 deletions Source/Common/Internal/IGListArrayUtilsInternal.h
@@ -0,0 +1,33 @@
/**
* 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.
*/

#ifndef IGListArrayUtilsInternal_h
#define IGListArrayUtilsInternal_h

static NSArray *objectsWithDuplicateIdentifiersRemoved(NSArray<id<IGListDiffable>> *objects) {
if (objects == nil) {
return nil;
}

NSMutableSet *identifiers = [NSMutableSet new];
NSMutableArray *uniqueObjects = [NSMutableArray new];
for (id<IGListDiffable> object in objects) {
id diffIdentifier = [object diffIdentifier];
if (diffIdentifier != nil
&& ![identifiers containsObject:diffIdentifier]) {
[identifiers addObject:diffIdentifier];
[uniqueObjects addObject:object];
} else {
IGLKLog(@"WARNING: Object %@ already appeared in objects array", object);
}
}
return uniqueObjects;
}

#endif /* IGListArrayUtilsInternal_h */
43 changes: 38 additions & 5 deletions Source/IGListAdapter.m
Expand Up @@ -18,6 +18,8 @@

@implementation IGListAdapter {
NSMapTable<UICollectionReusableView *, IGListSectionController *> *_viewSectionControllerMap;
// An array of blocks to execute once batch updates are finished
NSMutableArray<void (^)()> *_queuedCompletionBlocks;
}

- (void)dealloc {
Expand Down Expand Up @@ -317,6 +319,8 @@ - (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletio
NSArray *fromObjects = self.sectionMap.objects;
NSArray *newObjects = [dataSource objectsForListAdapter:self];

[self enterBatchUpdates];

__weak __typeof__(self) weakSelf = self;
[self.updater performUpdateWithCollectionView:collectionView
fromObjects:fromObjects
Expand All @@ -335,6 +339,8 @@ - (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletio
if (completion) {
completion(finished);
}

[weakSelf exitBatchUpdates];
}];
}

Expand Down Expand Up @@ -702,6 +708,26 @@ - (void)removeMapForView:(UICollectionReusableView *)view {
[_viewSectionControllerMap removeObjectForKey:view];
}

- (void)deferBlockBetweenBatchUpdates:(void (^)())block {
IGAssertMainThread();
if (_queuedCompletionBlocks == nil) {
block();
} else {
[_queuedCompletionBlocks addObject:block];
}
}

- (void)enterBatchUpdates {
_queuedCompletionBlocks = [NSMutableArray new];
}

- (void)exitBatchUpdates {
for (void (^block)() in _queuedCompletionBlocks) {
block();
}
_queuedCompletionBlocks = nil;
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
Expand Down Expand Up @@ -964,6 +990,8 @@ - (void)performBatchAnimated:(BOOL)animated updates:(void (^)(id<IGListBatchCont
IGParameterAssert(updates != nil);
UICollectionView *collectionView = self.collectionView;
IGAssert(collectionView != nil, @"Performing batch updates without a collection view.");

[self enterBatchUpdates];

__weak __typeof__(self) weakSelf = self;
[self.updater performUpdateWithCollectionView:collectionView animated:animated itemUpdates:^{
Expand All @@ -976,6 +1004,8 @@ - (void)performBatchAnimated:(BOOL)animated updates:(void (^)(id<IGListBatchCont
if (completion) {
completion(finished);
}

[weakSelf exitBatchUpdates];
}];
}

Expand Down Expand Up @@ -1004,11 +1034,14 @@ - (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionC
UICollectionViewLayoutInvalidationContext *context = [[[layout.class invalidationContextClass] alloc] init];
[context invalidateItemsAtIndexPaths:indexPaths];

void (^block)() = ^{
[layout invalidateLayoutWithContext:context];
};

[_collectionView performBatchUpdates:block completion:completion];
__weak __typeof__(_collectionView) weakCollectionView = _collectionView;

// do not call -[UICollectionView performBatchUpdates:completion:] while already updating. defer it until completed.
[self deferBlockBetweenBatchUpdates:^{
[weakCollectionView performBatchUpdates:^{
[layout invalidateLayoutWithContext:context];
} completion:completion];
}];
}

#pragma mark - IGListBatchContext
Expand Down
39 changes: 10 additions & 29 deletions Source/IGListAdapterUpdater.m
Expand Up @@ -18,6 +18,7 @@
#import "IGListIndexSetResultInternal.h"
#import "IGListMoveIndexPathInternal.h"
#import "IGListReloadIndexPath.h"
#import "IGListArrayUtilsInternal.h"

@implementation IGListAdapterUpdater

Expand All @@ -34,7 +35,6 @@ - (instancetype)init {
return self;
}


#pragma mark - Private API

- (BOOL)hasChanges {
Expand All @@ -56,7 +56,7 @@ - (void)performReloadDataWithCollectionView:(UICollectionView *)collectionView {
void (^reloadUpdates)() = self.reloadUpdates;
IGListBatchUpdates *batchUpdates = self.batchUpdates;
NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy];

[self cleanStateBeforeUpdates];

// item updates must not send mutations to the collection view while we are reloading
Expand All @@ -72,11 +72,11 @@ - (void)performReloadDataWithCollectionView:(UICollectionView *)collectionView {
for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) {
itemUpdateBlock();
}

// add any completion blocks from item updates. added after item blocks are executed in order to capture any
// re-entrant updates
[completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks];

self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock;

[self cleanStateAfterUpdates];
Expand All @@ -94,26 +94,6 @@ - (void)performReloadDataWithCollectionView:(UICollectionView *)collectionView {
self.state = IGListBatchUpdateStateIdle;
}

static NSArray *objectsWithDuplicateIdentifiersRemoved(NSArray<id<IGListDiffable>> *objects) {
if (objects == nil) {
return nil;
}

NSMutableSet *identifiers = [NSMutableSet new];
NSMutableArray *uniqueObjects = [NSMutableArray new];
for (id<IGListDiffable> object in objects) {
id diffIdentifier = [object diffIdentifier];
if (diffIdentifier != nil
&& ![identifiers containsObject:diffIdentifier]) {
[identifiers addObject:diffIdentifier];
[uniqueObjects addObject:object];
} else {
IGLKLog(@"WARNING: Object %@ already appeared in objects array", object);
}
}
return uniqueObjects;
}

- (void)performBatchUpdatesWithCollectionView:(UICollectionView *)collectionView {
IGAssertMainThread();
IGAssert(self.state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle");
Expand Down Expand Up @@ -151,7 +131,7 @@ - (void)performBatchUpdatesWithCollectionView:(UICollectionView *)collectionView
for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) {
itemUpdateBlock();
}

// add any completion blocks from item updates. added after item blocks are executed in order to capture any
// re-entrant updates
[completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks];
Expand Down Expand Up @@ -361,7 +341,7 @@ - (void)cleanStateBeforeUpdates {

// remove indexpath/item changes
self.objectTransitionBlock = nil;

// removes all object completion blocks. done before updates to start collecting completion blocks for coalesced
// or re-entrant object updates
[self.completionBlocks removeAllObjects];
Expand Down Expand Up @@ -471,7 +451,7 @@ - (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
IGAssertMainThread();
IGParameterAssert(collectionView != nil);
IGParameterAssert(itemUpdates != nil);

IGListBatchUpdates *batchUpdates = self.batchUpdates;
if (completion != nil) {
[batchUpdates.itemCompletionBlocks addObject:completion];
Expand All @@ -483,11 +463,11 @@ - (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
itemUpdates();
} else {
[batchUpdates.itemUpdateBlocks addObject:itemUpdates];

// disabled animations will always take priority
// reset to YES in -cleanupState
self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;

[self queueUpdateWithCollectionView:collectionView];
}
}
Expand Down Expand Up @@ -569,3 +549,4 @@ - (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSInde
}

@end

20 changes: 19 additions & 1 deletion Source/IGListBindingSectionController.m
Expand Up @@ -13,6 +13,9 @@
#import <IGListKit/IGListDiffable.h>
#import <IGListKit/IGListDiff.h>
#import <IGListKit/IGListBindable.h>
#import <IGListKit/IGListAdapterUpdater.h>

#import "IGListArrayUtilsInternal.h"

typedef NS_ENUM(NSInteger, IGListDiffingSectionState) {
IGListDiffingSectionStateIdle = 0,
Expand Down Expand Up @@ -58,7 +61,8 @@ - (void)updateAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
id<IGListDiffable> object = self.object;
IGAssert(object != nil, @"Expected IGListBindingSectionController object to be non-nil before updating.");

self.viewModels = [self.dataSource sectionController:self viewModelsForObject:object];
NSArray *newViewModels = [self.dataSource sectionController:self viewModelsForObject:object];
self.viewModels = objectsWithDuplicateIdentifiersRemoved(newViewModels);
result = IGListDiff(oldViewModels, self.viewModels, IGListDiffEquality);

[result.updates enumerateIndexesUsingBlock:^(NSUInteger oldUpdatedIndex, BOOL *stop) {
Expand Down Expand Up @@ -130,4 +134,18 @@ - (void)didDeselectItemAtIndex:(NSInteger)index {
}
}

- (void)didHighlightItemAtIndex:(NSInteger)index {
id<IGListBindingSectionControllerSelectionDelegate> selectionDelegate = self.selectionDelegate;
if ([selectionDelegate respondsToSelector:@selector(sectionController:didHighlightItemAtIndex:viewModel:)]) {
[selectionDelegate sectionController:self didHighlightItemAtIndex:index viewModel:self.viewModels[index]];
}
}

- (void)didUnhighlightItemAtIndex:(NSInteger)index {
id<IGListBindingSectionControllerSelectionDelegate> selectionDelegate = self.selectionDelegate;
if ([selectionDelegate respondsToSelector:@selector(sectionController:didUnhighlightItemAtIndex:viewModel:)]) {
[selectionDelegate sectionController:self didUnhighlightItemAtIndex:index viewModel:self.viewModels[index]];
}
}

@end
24 changes: 24 additions & 0 deletions Source/IGListBindingSectionControllerSelectionDelegate.h
Expand Up @@ -44,6 +44,30 @@ NS_SWIFT_NAME(ListBindingSectionControllerSelectionDelegate)
didDeselectItemAtIndex:(NSInteger)index
viewModel:(id)viewModel;

/**
Tells the delegate that a cell at a given index was highlighted.
@param sectionController The section controller the highlight occurred in.
@param index The index of the highlighted cell.
@param viewModel The view model that was bound to the cell.
*/
@optional
- (void)sectionController:(IGListBindingSectionController *)sectionController
didHighlightItemAtIndex:(NSInteger)index
viewModel:(id)viewModel;

/**
Tells the delegate that a cell at a given index was unhighlighted.
@param sectionController The section controller the unhighlight occurred in.
@param index The index of the unhighlighted cell.
@param viewModel The view model that was bound to the cell.
*/
@optional
- (void)sectionController:(IGListBindingSectionController *)sectionController
didUnhighlightItemAtIndex:(NSInteger)index
viewModel:(id)viewModel;

@end

NS_ASSUME_NONNULL_END

0 comments on commit d47da08

Please sign in to comment.