mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-09-06 20:54:04 +00:00
Add first-pass view model support to collection node. #trivial (#356)
* Add first-pass view model support for collection node. Much more to come! * Address issues * Update the gorram license header * Dear lord
This commit is contained in:
parent
c297060113
commit
83111de0cc
@ -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.
|
||||
|
@ -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.
|
||||
*
|
||||
@ -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.
|
||||
|
@ -590,6 +590,12 @@
|
||||
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];
|
||||
|
@ -207,6 +207,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
|
||||
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;
|
||||
@ -450,6 +451,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
|
||||
_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) {
|
||||
@ -1611,6 +1613,16 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
|
||||
|
||||
#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;
|
||||
|
@ -1629,6 +1629,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
|
||||
|
||||
#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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -31,7 +31,8 @@
|
||||
ASCellNode *_node;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNodeBlock:(ASCellNodeBlock)nodeBlock
|
||||
- (instancetype)initWithViewModel:(id)viewModel
|
||||
nodeBlock:(ASCellNodeBlock)nodeBlock
|
||||
supplementaryElementKind:(NSString *)supplementaryElementKind
|
||||
constrainedSize:(ASSizeRange)constrainedSize
|
||||
owningNode:(id<ASRangeManagingNode>)owningNode
|
||||
@ -40,6 +41,7 @@
|
||||
NSAssert(nodeBlock != nil, @"Node block must not be nil");
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_viewModel = viewModel;
|
||||
_nodeBlock = nodeBlock;
|
||||
_supplementaryElementKind = [supplementaryElementKind copy];
|
||||
_constrainedSize = constrainedSize;
|
||||
@ -62,6 +64,7 @@
|
||||
node.owningNode = _owningNode;
|
||||
node.collectionElement = self;
|
||||
ASTraitCollectionPropagateDown(node, _traitCollection);
|
||||
node.viewModel = _viewModel;
|
||||
_node = node;
|
||||
}
|
||||
return _node;
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -384,6 +384,8 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray<ASCollectionElement *> *
|
||||
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];
|
||||
@ -396,7 +398,8 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray<ASCollectionElement *> *
|
||||
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
|
||||
|
@ -27,10 +27,15 @@
|
||||
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];
|
||||
|
||||
@ -46,6 +51,7 @@
|
||||
@selector(numberOfSectionsInCollectionNode:),
|
||||
@selector(collectionNode:numberOfItemsInSection:),
|
||||
@selector(collectionNode:nodeBlockForItemAtIndexPath:),
|
||||
@selector(collectionNode:viewModelForItemAtIndexPath:),
|
||||
nil];
|
||||
[mockDataSource setExpectationOrderMatters:YES];
|
||||
|
||||
@ -59,48 +65,138 @@
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)testInitialDataLoadingCallPattern
|
||||
#pragma mark - Test Methods
|
||||
|
||||
- (void)testInitialDataLoading
|
||||
{
|
||||
/// 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];
|
||||
}
|
||||
|
||||
[window layoutIfNeeded];
|
||||
[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++) {
|
||||
// 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];
|
||||
|
||||
// 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
|
||||
|
@ -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>
|
||||
|
@ -1,18 +1,18 @@
|
||||
//
|
||||
// ItemNode.m
|
||||
// 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 "ItemNode.h"
|
||||
@ -50,7 +50,7 @@ const CGFloat kSoldOutGBHeight = 50.0;
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_viewModel = viewModel;
|
||||
self.viewModel = viewModel;
|
||||
[self setup];
|
||||
[self updateLabels];
|
||||
[self updateBackgroundColor];
|
||||
|
Loading…
x
Reference in New Issue
Block a user