Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan Nystrom committed Jan 27, 2017
1 parent a7b537b commit c60a552
Show file tree
Hide file tree
Showing 21 changed files with 900 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.

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

/**
Other naming options
- IGListDiffingSectionController
- IGListBindingSectionController
- IGListAutoSectionController
*/

NS_ASSUME_NONNULL_BEGIN

@protocol IGListAutoSectionControllerDataSource <NSObject>

- (NSArray<id<IGListDiffable>> *)viewModelsForObject:(id)object;
- (UICollectionViewCell<IGListBindable> *)cellForViewModel:(id)viewModel atIndex:(NSInteger)index;
- (CGSize)sizeForViewModel:(id)viewModel;

@end

@interface IGListAutoSectionController : IGListSectionController<IGListSectionType>

/**
func transform(object: IGListDiffable) -> [IGListDiffable]
func cellClass(viewModel: IGListDiffable) -> Class (of type UICollectionView<IGListBindable>)
how to add "extra" stuff to the cell, e.g. delegates. options:
- override superclass method (don't call super tho)
- use IGListDisplayDelegate (adds lots of boilerplate)
- new, optional delegate w/ bindable method
*/

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

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

@end

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

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

typedef NS_ENUM(NSInteger, IGListAutoSectionState) {
IGListAutoSectionStateIdle = 0,
IGListAutoSectionStateUpdateQueued,
IGListAutoSectionStateUpdateApplied
};

@interface IGListAutoSectionController()

@property (nonatomic, strong) id object;
@property (nonatomic, strong) NSArray<id<IGListDiffable>> *viewModels;
@property (nonatomic, assign) IGListAutoSectionState state;

@end

@implementation IGListAutoSectionController

#pragma mark - Public API

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

if (self.state != IGListAutoSectionStateIdle) {
return;
}
self.state = IGListAutoSectionStateUpdateQueued;

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

id<IGListCollectionContext> collectionContext = self.collectionContext;

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

oldViewModels = self.viewModels;
self.viewModels = [self.dataSource 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 = IGListAutoSectionStateUpdateApplied;
} completion:^(BOOL finished) {
self.state = IGListAutoSectionStateIdle;

// "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 sizeForViewModel:self.viewModels[index]];
}

- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index {
id<IGListDiffable> viewModel = self.viewModels[index];
UICollectionViewCell<IGListBindable> *cell = [self.dataSource 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 viewModelsForObject:object];
} else {
[self updateAnimated:YES completion:nil];
}
// self.viewModels = [self.dataSource viewModelsForObject:object];

// atm this will queue an update next turn which isn't ideal. really we want to unload this update right here and now
// maybe we can update performBatchAnimated:completion to fire if we're inside an update block?
// [self updateAnimated:YES completion:nil];
}

- (void)didSelectItemAtIndex:(NSInteger)index {
// would be nice to have a "selection delegate" here
}

@end
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
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/IGListAutoSectionController.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 c60a552

Please sign in to comment.