Skip to content

Commit

Permalink
Add first-pass view model support to collection node. #trivial (Textu…
Browse files Browse the repository at this point in the history
…reGroup#356)

* Add first-pass view model support for collection node. Much more to come!

* Address issues

* Update the gorram license header

* Dear lord
  • Loading branch information
Adlai-Holler authored and bernieperez committed Apr 25, 2018
1 parent c4dfcd2 commit 288a75c
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 40 deletions.
9 changes: 9 additions & 0 deletions Source/ASCellNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) {
*/
@property (atomic, readonly, nullable) NSIndexPath *indexPath;

/**
* BETA: API is under development. We will attempt to provide an easy migration pathway for any changes.
*
* The view-model currently assigned to this node, if any.
*
* This property may be set off the main thread, but this method will never be invoked concurrently on the
*/
@property (atomic, nullable) id viewModel;

/**
* The backing view controller, or @c nil if the node wasn't initialized with backing view controller
* @note This property must be accessed on the main thread.
Expand Down
22 changes: 22 additions & 0 deletions Source/ASCollectionNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,17 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (nullable __kindof ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT;

/**
* Retrieves the view-model for the item at the given index path, if any.
*
* @param indexPath The index path of the requested item.
*
* @return The view-model for the given item, or @c nil if no item exists at the specified path or no view-model was provided.
*
* @warning This API is beta and subject to change. We'll try to provide an easy migration path.
*/
- (nullable id)viewModelForItemAtIndexPath:(NSIndexPath *)indexPath AS_WARN_UNUSED_RESULT;

/**
* Retrieve the index path for the item with the given node.
*
Expand Down Expand Up @@ -503,6 +514,17 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode;

/**
* --BETA--
* Asks the data source for a view-model for the item at the given index path.
*
* @param collectionNode The sender.
* @param indexPath The index path of the item.
*
* @return An object that contains all the data for this item.
*/
- (nullable id)collectionNode:(ASCollectionNode *)collectionNode viewModelForItemAtIndexPath:(NSIndexPath *)indexPath;

/**
* Similar to -collectionNode:nodeForItemAtIndexPath:
* This method takes precedence over collectionNode:nodeForItemAtIndexPath: if implemented.
Expand Down
6 changes: 6 additions & 0 deletions Source/ASCollectionNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ - (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath
return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].node;
}

- (id)viewModelForItemAtIndexPath:(NSIndexPath *)indexPath
{
[self reloadDataInitiallyIfNeeded];
return [self.dataController.pendingMap elementForItemAtIndexPath:indexPath].viewModel;
}

- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
{
return [self.dataController.pendingMap indexPathForElement:cellNode.collectionElement];
Expand Down
12 changes: 12 additions & 0 deletions Source/ASCollectionView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ @interface ASCollectionView () <ASRangeControllerDataSource, ASRangeControllerDe
unsigned int collectionViewNumberOfItemsInSection:1;
unsigned int collectionNodeNodeForItem:1;
unsigned int collectionNodeNodeBlockForItem:1;
unsigned int viewModelForItem:1;
unsigned int collectionNodeNodeForSupplementaryElement:1;
unsigned int collectionNodeNodeBlockForSupplementaryElement:1;
unsigned int collectionNodeSupplementaryElementKindsInSection:1;
Expand Down Expand Up @@ -450,6 +451,7 @@ - (void)setAsyncDataSource:(id<ASCollectionDataSource>)asyncDataSource
_asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeForSupplementaryElementOfKind:atIndexPath:)];
_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForSupplementaryElementOfKind:atIndexPath:)];
_asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:supplementaryElementKindsInSection:)];
_asyncDataSourceFlags.viewModelForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:viewModelForItemAtIndexPath:)];

_asyncDataSourceFlags.interop = [_asyncDataSource conformsToProtocol:@protocol(ASCollectionDataSourceInterop)];
if (_asyncDataSourceFlags.interop) {
Expand Down Expand Up @@ -1611,6 +1613,16 @@ - (void)_beginBatchFetching

#pragma mark - ASDataControllerSource

- (id)dataController:(ASDataController *)dataController viewModelForItemAtIndexPath:(NSIndexPath *)indexPath
{
if (!_asyncDataSourceFlags.viewModelForItem) {
return nil;
}

GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil);
return [_asyncDataSource collectionNode:collectionNode viewModelForItemAtIndexPath:indexPath];
}

- (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath
{
ASCellNodeBlock block = nil;
Expand Down
6 changes: 6 additions & 0 deletions Source/ASTableView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,12 @@ - (void)rangeController:(ASRangeController *)rangeController didUpdateWithChange

#pragma mark - ASDataControllerSource

- (id)dataController:(ASDataController *)dataController viewModelForItemAtIndexPath:(NSIndexPath *)indexPath
{
// Not currently supported for tables. Will be added when the collection API stabilizes.
return nil;
}

- (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath {
ASCellNodeBlock block = nil;

Expand Down
4 changes: 3 additions & 1 deletion Source/Details/ASCollectionElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ AS_SUBCLASSING_RESTRICTED
@property (nonatomic, assign) ASSizeRange constrainedSize;
@property (nonatomic, readonly, weak) id<ASRangeManagingNode> owningNode;
@property (nonatomic, assign) ASPrimitiveTraitCollection traitCollection;
@property (nonatomic, readonly, nullable) id viewModel;

- (instancetype)initWithNodeBlock:(ASCellNodeBlock)nodeBlock
- (instancetype)initWithViewModel:(nullable id)viewModel
nodeBlock:(ASCellNodeBlock)nodeBlock
supplementaryElementKind:(nullable NSString *)supplementaryElementKind
constrainedSize:(ASSizeRange)constrainedSize
owningNode:(id<ASRangeManagingNode>)owningNode
Expand Down
5 changes: 4 additions & 1 deletion Source/Details/ASCollectionElement.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ @implementation ASCollectionElement {
ASCellNode *_node;
}

- (instancetype)initWithNodeBlock:(ASCellNodeBlock)nodeBlock
- (instancetype)initWithViewModel:(id)viewModel
nodeBlock:(ASCellNodeBlock)nodeBlock
supplementaryElementKind:(NSString *)supplementaryElementKind
constrainedSize:(ASSizeRange)constrainedSize
owningNode:(id<ASRangeManagingNode>)owningNode
Expand All @@ -40,6 +41,7 @@ - (instancetype)initWithNodeBlock:(ASCellNodeBlock)nodeBlock
NSAssert(nodeBlock != nil, @"Node block must not be nil");
self = [super init];
if (self) {
_viewModel = viewModel;
_nodeBlock = nodeBlock;
_supplementaryElementKind = [supplementaryElementKind copy];
_constrainedSize = constrainedSize;
Expand All @@ -62,6 +64,7 @@ - (ASCellNode *)node
node.owningNode = _owningNode;
node.collectionElement = self;
ASTraitCollectionPropagateDown(node, _traitCollection);
node.viewModel = _viewModel;
_node = node;
}
return _node;
Expand Down
2 changes: 2 additions & 0 deletions Source/Details/ASDataController.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ extern NSString * const ASCollectionInvalidUpdateException;
*/
- (BOOL)dataController:(ASDataController *)dataController presentedSizeForElement:(ASCollectionElement *)element matchesSize:(CGSize)size;

- (nullable id)dataController:(ASDataController *)dataController viewModelForItemAtIndexPath:(NSIndexPath *)indexPath;

@optional

/**
Expand Down
5 changes: 4 additions & 1 deletion Source/Details/ASDataController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map
id<ASDataControllerSource> dataSource = self.dataSource;
id<ASRangeManagingNode> node = self.node;
for (NSIndexPath *indexPath in indexPaths) {
id viewModel = [dataSource dataController:self viewModelForItemAtIndexPath:indexPath];

ASCellNodeBlock nodeBlock;
if (isRowKind) {
nodeBlock = [dataSource dataController:self nodeBlockAtIndexPath:indexPath];
Expand All @@ -396,7 +398,8 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map
constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath];
}

ASCollectionElement *element = [[ASCollectionElement alloc] initWithNodeBlock:nodeBlock
ASCollectionElement *element = [[ASCollectionElement alloc] initWithViewModel:viewModel
nodeBlock:nodeBlock
supplementaryElementKind:isRowKind ? nil : kind
constrainedSize:constrainedSize
owningNode:node
Expand Down
132 changes: 114 additions & 18 deletions Tests/ASCollectionModernDataSourceTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ @implementation ASCollectionModernDataSourceTests {
UIWindow *window;
UIViewController *viewController;
ASCollectionNode *collectionNode;
NSMutableArray<NSMutableArray *> *sections;
}

- (void)setUp {
[super setUp];
// Default is 2 sections: 2 items in first, 1 item in second.
sections = [NSMutableArray array];
[sections addObject:[NSMutableArray arrayWithObjects:[NSObject new], [NSObject new], nil]];
[sections addObject:[NSMutableArray arrayWithObjects:[NSObject new], nil]];
window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
viewController = [[UIViewController alloc] init];

Expand All @@ -46,6 +51,7 @@ - (void)setUp {
@selector(numberOfSectionsInCollectionNode:),
@selector(collectionNode:numberOfItemsInSection:),
@selector(collectionNode:nodeBlockForItemAtIndexPath:),
@selector(collectionNode:viewModelForItemAtIndexPath:),
nil];
[mockDataSource setExpectationOrderMatters:YES];

Expand All @@ -59,48 +65,138 @@ - (void)tearDown
[super tearDown];
}

- (void)testInitialDataLoadingCallPattern
#pragma mark - Test Methods

- (void)testInitialDataLoading
{
[self loadInitialData];
}

- (void)testReloadingAnItem
{
[self loadInitialData];

// Reload at (0, 0)
NSIndexPath *reloadedPath = [NSIndexPath indexPathForItem:0 inSection:0];

sections[reloadedPath.section][reloadedPath.item] = [NSObject new];
[self performUpdateInvalidatingItems:@[ reloadedPath ] block:^{
[collectionNode reloadItemsAtIndexPaths:@[ reloadedPath ]];
}];
}

- (void)testInsertingAnItem
{
[self loadInitialData];

// Insert at (1, 0)
NSIndexPath *insertedPath = [NSIndexPath indexPathForItem:0 inSection:1];

[sections[insertedPath.section] insertObject:[NSObject new] atIndex:insertedPath.item];
[self performUpdateInvalidatingItems:@[ insertedPath ] block:^{
[collectionNode insertItemsAtIndexPaths:@[ insertedPath ]];
}];
}

#pragma mark - Helpers

- (void)loadInitialData
{
/// BUG: these methods are called twice in a row i.e. this for-loop shouldn't be here. https://github.com/TextureGroup/Texture/issues/351
for (int i = 0; i < 2; i++) {
NSArray *counts = @[ @2 ];
[self expectDataSourceMethodsWithCounts:counts];
// It reads all the counts
[self expectDataSourceCountMethods];

// It reads the contents for each item.
for (NSInteger section = 0; section < sections.count; section++) {
NSArray *items = sections[section];

// For each item:
for (NSInteger i = 0; i < items.count; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section];
[self expectContentMethodsForItemAtIndexPath:indexPath];
}
}
}

[window layoutIfNeeded];
}

#pragma mark - Helpers
// Assert item counts & content:
[self assertCollectionNodeContent];
}

/**
* Adds expectations for the sequence:
*
* numberOfSectionsInCollectionNode:
* for section in countsArray
* numberOfItemsInSection:
* for item < itemCount
* nodeBlockForItemAtIndexPath:
*/
- (void)expectDataSourceMethodsWithCounts:(NSArray<NSNumber *> *)counts
- (void)expectDataSourceCountMethods
{
// -numberOfSectionsInCollectionNode
OCMExpect([mockDataSource numberOfSectionsInCollectionNode:collectionNode])
.andReturn(counts.count);
.andReturn(sections.count);

// For each section:
// Note: Skip fast enumeration for readability.
for (NSInteger section = 0; section < counts.count; section++) {
NSInteger itemCount = counts[section].integerValue;
for (NSInteger section = 0; section < sections.count; section++) {
NSInteger itemCount = sections[section].count;
OCMExpect([mockDataSource collectionNode:collectionNode numberOfItemsInSection:section])
.andReturn(itemCount);

// For each item:
for (NSInteger i = 0; i < itemCount; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section];
OCMExpect([mockDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath])
.andReturn((ASCellNodeBlock)^{ return [[ASCellNode alloc] init]; });
}
}

// Expects viewModelForItemAtIndexPath: and nodeBlockForItemAtIndexPath:
- (void)expectContentMethodsForItemAtIndexPath:(NSIndexPath *)indexPath
{
id viewModel = sections[indexPath.section][indexPath.item];
OCMExpect([mockDataSource collectionNode:collectionNode viewModelForItemAtIndexPath:indexPath])
.andReturn(viewModel);
OCMExpect([mockDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath])
.andReturn((ASCellNodeBlock)^{ return [ASCellNode new]; });
}

- (void)assertCollectionNodeContent
{
// Assert section count
XCTAssertEqual(collectionNode.numberOfSections, sections.count);

for (NSInteger section = 0; section < sections.count; section++) {
NSArray *items = sections[section];
// Assert item count
XCTAssertEqual([collectionNode numberOfItemsInSection:section], items.count);
for (NSInteger item = 0; item < items.count; item++) {
// Assert view model
// Could use pointer equality but the error message is less readable.
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
id viewModel = sections[indexPath.section][indexPath.item];
XCTAssertEqualObjects(viewModel, [collectionNode viewModelForItemAtIndexPath:indexPath]);
ASCellNode *node = [collectionNode nodeForItemAtIndexPath:indexPath];
XCTAssertEqualObjects(node.viewModel, viewModel);
}
}
}

/**
* Updates the collection node, with expectations and assertions about the call-order and the correctness of the
* new data. You should update the data source _before_ calling this method.
*
* invalidatedIndexPaths are the items we expect to get refetched (reloaded/inserted).
*/
- (void)performUpdateInvalidatingItems:(NSArray<NSIndexPath *> *)invalidatedIndexPaths block:(void(^)())update
{
// When we do an edit, it'll read the new counts
[self expectDataSourceCountMethods];

// Then it'll load the contents for inserted/reloaded items.
for (NSIndexPath *indexPath in invalidatedIndexPaths) {
[self expectContentMethodsForItemAtIndexPath:indexPath];
}

[collectionNode performBatchUpdates:update completion:nil];

[self assertCollectionNodeContent];
}

@end
18 changes: 9 additions & 9 deletions examples/CatDealsCollectionView/Sample/ItemNode.h
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
//
// ItemNode.h
// Sample
// Texture
//
// Copyright (c) 2014-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.
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//

#import <AsyncDisplayKit/AsyncDisplayKit.h>
Expand Down
Loading

0 comments on commit 288a75c

Please sign in to comment.