Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan Nystrom committed Feb 15, 2017
1 parent 7a94167 commit 0ae5a5c
Show file tree
Hide file tree
Showing 21 changed files with 917 additions and 63 deletions.
90 changes: 90 additions & 0 deletions IGListKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

141 changes: 84 additions & 57 deletions Source/IGListAdapterUpdater.m

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions Source/IGListBindable.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* 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 <Foundation/Foundation.h>

@protocol IGListDiffable;

@protocol IGListBindable <NSObject>

- (void)bindViewModel:(id)viewModel;

@end
56 changes: 56 additions & 0 deletions Source/IGListDiffingSectionController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* 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 <UIKit/UIKit.h>

#import <IGListKit/IGListMacros.h>
#import <IGListKit/IGListSectionType.h>
#import <IGListKit/IGListSectionController.h>

@protocol IGListDiffable;
@protocol IGListBindable;

@class IGListDiffingSectionController;

NS_ASSUME_NONNULL_BEGIN

@protocol IGListDiffingSectionControllerDataSource <NSObject>

- (NSArray<id<IGListDiffable>> *)sectionController:(IGListDiffingSectionController *)sectionController
viewModelsForObject:(id)object;

- (UICollectionViewCell<IGListBindable> *)sectionController:(IGListDiffingSectionController *)sectionController
cellForViewModel:(id)viewModel atIndex:(NSInteger)index;

- (CGSize)sectionController:(IGListDiffingSectionController *)sectionController
sizeForViewModel:(id)viewModel;

@end

@protocol IGListDiffingSectionControllerSelectionDelegate

- (void)sectionController:(IGListDiffingSectionController *)sectionController
didSelectItemAtIndex:(NSInteger)index
viewModel:(id)viewModel;

@end

@interface IGListDiffingSectionController : IGListSectionController<IGListSectionType>

@property (nonatomic, weak, nullable) id<IGListDiffingSectionControllerDataSource> dataSource;

@property (nonatomic, weak, nullable) id<IGListDiffingSectionControllerSelectionDelegate> selectionDelegate;

@property (nonatomic, strong, readonly) NSArray<id<IGListDiffable>> *viewModels;

- (void)updateAnimated:(BOOL)animated completion:(nullable void (^)())completion;

@end

NS_ASSUME_NONNULL_END
115 changes: 115 additions & 0 deletions Source/IGListDiffingSectionController.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* 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 "IGListDiffingSectionController.h"

#import <IGListKit/IGListAssert.h>
#import <IGListKit/IGListDiffable.h>
#import <IGListKit/IGListDiff.h>
#import <IGListKit/IGListBindable.h>

typedef NS_ENUM(NSInteger, IGListDiffingSectionState) {
IGListDiffingSectionStateIdle = 0,
IGListDiffingSectionStateUpdateQueued,
IGListDiffingSectionStateUpdateApplied
};

@interface IGListDiffingSectionController()

@property (nonatomic, strong, readwrite) NSArray<id<IGListDiffable>> *viewModels;

@property (nonatomic, strong) id object;
@property (nonatomic, assign) IGListDiffingSectionState state;

@end

@implementation IGListDiffingSectionController

#pragma mark - Public API

- (void)updateAnimated:(BOOL)animated completion:(void (^)())completion {
IGAssertMainThread();

if (self.state != IGListDiffingSectionStateIdle) {
return;
}
self.state = IGListDiffingSectionStateUpdateQueued;

__block IGListIndexSetResult *result = nil;
__block NSArray<id<IGListDiffable>> *oldViewModels = nil;

id<IGListCollectionContext> collectionContext = self.collectionContext;

[collectionContext performBatchAnimated:YES updates:^{
if (self.state != IGListDiffingSectionStateUpdateQueued) {
return;
}

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

[collectionContext deleteInSectionController:self atIndexes:result.deletes];
[collectionContext insertInSectionController:self atIndexes:result.inserts];

for (IGListMoveIndex *move in result.moves) {
[collectionContext moveInSectionController:self fromIndex:move.from toIndex:move.to];
}

self.state = IGListDiffingSectionStateUpdateApplied;
} completion:^(BOOL finished) {
self.state = IGListDiffingSectionStateIdle;

// "reload" cells after updating since the cells can't be moved and reloaded at the same time.
// this lets the cell do an animated move and then update its contents
[result.updates enumerateIndexesUsingBlock:^(NSUInteger oldUpdatedIndex, BOOL *stop) {
id identifier = [oldViewModels[oldUpdatedIndex] diffIdentifier];
const NSInteger indexAfterUpdate = [result newIndexForIdentifier:identifier];
if (indexAfterUpdate != NSNotFound) {
UICollectionViewCell<IGListBindable> *cell = [collectionContext cellForItemAtIndex:indexAfterUpdate
sectionController:self];
[cell bindViewModel:self.viewModels[indexAfterUpdate]];
}
}];
}];
}

#pragma mark - IGListSectionType

- (NSInteger)numberOfItems {
return self.viewModels.count;
}

- (CGSize)sizeForItemAtIndex:(NSInteger)index {
return [self.dataSource sectionController:self sizeForViewModel:self.viewModels[index]];
}

- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index {
id<IGListDiffable> viewModel = self.viewModels[index];
UICollectionViewCell<IGListBindable> *cell = [self.dataSource sectionController:self cellForViewModel:viewModel atIndex:index];
[cell bindViewModel:viewModel];
return cell;
}

- (void)didUpdateToObject:(id)object {
id oldObject = self.object;
self.object = object;

if (oldObject == nil) {
self.viewModels = [self.dataSource sectionController:self viewModelsForObject:object];
} else {
[self updateAnimated:YES completion:nil];
}
}

- (void)didSelectItemAtIndex:(NSInteger)index {
[self.selectionDelegate sectionController:self didSelectItemAtIndex:index viewModel:self.viewModels[index]];
}

@end
2 changes: 2 additions & 0 deletions Source/IGListKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ FOUNDATION_EXPORT const unsigned char IGListKitVersionString[];
#import <IGListKit/IGListAdapterDelegate.h>
#import <IGListKit/IGListAdapterUpdater.h>
#import <IGListKit/IGListAdapterUpdaterDelegate.h>
#import <IGListKit/IGListDiffingSectionController.h>
#import <IGListKit/IGListBindable.h>
#import <IGListKit/IGListCollectionContext.h>
#import <IGListKit/IGListCollectionView.h>
#import <IGListKit/IGListDisplayDelegate.h>
Expand Down
22 changes: 16 additions & 6 deletions Source/Internal/IGListAdapterUpdaterInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
#import <IGListKit/IGListMoveIndexPath.h>

#import "IGListAdapterUpdater.h"
#import "IGListBatchUpdatesCollection.h"

typedef NS_ENUM (NSInteger, IGListBatchUpdateState) {
IGListBatchUpdateStateIdle,
IGListBatchUpdateStateQueuedBatchUpdate,
IGListBatchUpdateStateExecutingBatchUpdateBlock,
IGListBatchUpdateStateExecutedBatchUpdateBlock,
};

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -32,19 +40,21 @@ FOUNDATION_EXTERN void convertReloadToDeleteInsert(NSMutableIndexSet *reloads,

@property (nonatomic, assign) BOOL queuedUpdateIsAnimated;

@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *deleteIndexPaths;
@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *insertIndexPaths;
@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *reloadIndexPaths;
@property (nonatomic, strong, readonly) NSMutableSet<IGListMoveIndexPath *> *moveIndexPaths;
@property (nonatomic, strong, readonly) NSMutableIndexSet *reloadSections;
//@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *deleteIndexPaths;
//@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *insertIndexPaths;
//@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *reloadIndexPaths;
//@property (nonatomic, strong, readonly) NSMutableSet<IGListMoveIndexPath *> *moveIndexPaths;
//@property (nonatomic, strong, readonly) NSMutableIndexSet *reloadSections;
@property (nonatomic, strong) IGListBatchUpdatesCollection *updateCollection;

@property (nonatomic, copy, nullable) IGListObjectTransitionBlock objectTransitionBlock;
@property (nonatomic, copy, nullable) NSMutableArray<IGListItemUpdateBlock> *itemUpdateBlocks;

@property (nonatomic, copy, nullable) IGListReloadUpdateBlock reloadUpdates;
@property (nonatomic, assign, getter=hasQueuedReloadData) BOOL queuedReloadData;

@property (nonatomic, assign) BOOL batchUpdateOrReloadInProgress;
//@property (nonatomic, assign) BOOL batchUpdateOrReloadInProgress;
@property (nonatomic, assign) IGListBatchUpdateState state;

- (void)performReloadDataWithCollectionView:(UICollectionView *)collectionView;
- (void)performBatchUpdatesWithCollectionView:(UICollectionView *)collectionView;
Expand Down
28 changes: 28 additions & 0 deletions Source/Internal/IGListBatchUpdatesCollection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// IGListBatchUpdatesCollection.h
// IGListKit
//
// Created by Ryan Nystrom on 1/26/17.
// Copyright © 2017 Instagram. All rights reserved.
//

#import <Foundation/Foundation.h>

@class IGListMoveIndex;
@class IGListMoveIndexPath;

@interface IGListBatchUpdatesCollection : NSObject

//@property (nonatomic, strong, readonly) NSMutableIndexSet *sectionInserts;
@property (nonatomic, strong, readonly) NSMutableIndexSet *sectionReloads;
//@property (nonatomic, strong, readonly) NSMutableIndexSet *sectionDeletes;
//@property (nonatomic, strong, readonly) NSMutableSet<IGListMoveIndex *> *sectionMoves;
@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *itemInserts;
@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *itemDeletes;
@property (nonatomic, strong, readonly) NSMutableSet<NSIndexPath *> *itemReloads;
@property (nonatomic, strong, readonly) NSMutableSet<IGListMoveIndexPath *> *itemMoves;
@property (nonatomic, strong, readonly) NSMutableArray<void (^)(BOOL)> *completionBlocks;

//- (BOOL)hasChanges;

@end
34 changes: 34 additions & 0 deletions Source/Internal/IGListBatchUpdatesCollection.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// IGListBatchUpdatesCollection.m
// IGListKit
//
// Created by Ryan Nystrom on 1/26/17.
// Copyright © 2017 Instagram. All rights reserved.
//

#import "IGListBatchUpdatesCollection.h"

@implementation IGListBatchUpdatesCollection

- (instancetype)init {
if (self = [super init]) {
_sectionReloads = [NSMutableIndexSet new];
_itemInserts = [NSMutableSet new];
_itemMoves = [NSMutableSet new];
_itemReloads = [NSMutableSet new];
_itemDeletes = [NSMutableSet new];
_completionBlocks = [NSMutableArray new];
}
return self;
}

//- (BOOL)hasChanges {
// return self.sectionReloads.count > 0
// || self.itemInserts.count > 0
// || self.itemMoves.count > 0
// || self.itemReloads.count > 0
// || self.itemDeletes.count > 0
// || self.completionBlocks.count > 0;
//}

@end
71 changes: 71 additions & 0 deletions Tests/IGListAdapterUpdaterTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,75 @@ - (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isDefaultYES_
[mockDelegate verify];
}

- (void)test_ {
IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]];
self.dataSource.sections = @[object];

__block BOOL reloadDataCompletionExecuted = NO;
[self.updater reloadDataWithCollectionView:self.collectionView reloadUpdateBlock:^{} completion:^(BOOL finished) {
reloadDataCompletionExecuted = YES;
}];

XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView animated:YES itemUpdates:^{
object.objects = @[@2, @1, @4, @5];
[self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[
[NSIndexPath indexPathForItem:2 inSection:0],
[NSIndexPath indexPathForItem:3 inSection:0],
]];
[self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[
[NSIndexPath indexPathForItem:0 inSection:0],
]];
[self.updater moveItemInCollectionView:self.collectionView
fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0]
toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
} completion:^(BOOL finished) {
XCTAssertTrue(reloadDataCompletionExecuted);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_2 {
[self.collectionView removeFromSuperview];

IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]];
self.dataSource.sections = @[object];

__block BOOL objectTransitionBlockExecuted = NO;
__block BOOL completionBlockExecuted = NO;
[self.updater performUpdateWithCollectionView:self.collectionView
fromObjects:self.dataSource.sections
toObjects:self.dataSource.sections
animated:YES
objectTransitionBlock:^(NSArray *toObjects) {
objectTransitionBlockExecuted = YES;
}
completion:^(BOOL finished) {
completionBlockExecuted = YES;
}];

XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView animated:YES itemUpdates:^{
object.objects = @[@2, @1, @4, @5];
[self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[
[NSIndexPath indexPathForItem:2 inSection:0],
[NSIndexPath indexPathForItem:3 inSection:0],
]];
[self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[
[NSIndexPath indexPathForItem:0 inSection:0],
]];
[self.updater moveItemInCollectionView:self.collectionView
fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0]
toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
} completion:^(BOOL finished) {
XCTAssertTrue(objectTransitionBlockExecuted);
XCTAssertTrue(completionBlockExecuted);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:15 handler:nil];
}

@end
Loading

0 comments on commit 0ae5a5c

Please sign in to comment.