[ASCellNode] Fix selection / highlight implementation

This commit is contained in:
Hannah Trosi
2016-06-25 00:22:28 -07:00
parent 42a1227d69
commit 2e4b1ea053
6 changed files with 216 additions and 37 deletions

View File

@@ -12,7 +12,7 @@
#import "ASCellNode.h" #import "ASCellNode.h"
@protocol ASCellNodeLayoutDelegate <NSObject> @protocol ASCellNodeInteractionDelegate <NSObject>
/** /**
* Notifies the delegate that the specified cell node has done a relayout. * Notifies the delegate that the specified cell node has done a relayout.
@@ -27,14 +27,19 @@
*/ */
- (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged; - (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged;
/*
* Methods to be called whenever the selection or highlight state changes
* on ASCellNode. UIKit internally stores these values to update reusable cells.
*/
- (void)nodeSelectedStateDidChange:(ASCellNode *)node;
- (void)nodeHighlightedStateDidChange:(ASCellNode *)node;
@end @end
@interface ASCellNode () @interface ASCellNode ()
/* @property (nonatomic, weak) id <ASCellNodeInteractionDelegate> interactionDelegate;
* A delegate to be notified (on main thread) after a relayout.
*/
@property (nonatomic, weak) id<ASCellNodeLayoutDelegate> layoutDelegate;
/* /*
* Back-pointer to the containing scrollView instance, set only for visible cells. Used for Cell Visibility Event callbacks. * Back-pointer to the containing scrollView instance, set only for visible cells. Used for Cell Visibility Event callbacks.

View File

@@ -81,14 +81,16 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) {
@property (nonatomic) UITableViewCellSelectionStyle selectionStyle; @property (nonatomic) UITableViewCellSelectionStyle selectionStyle;
/** /**
* A Boolean value that indicates whether the node is selected. * A Boolean value that is synchronized with the underlying collection or tableView cell property.
* Setting this value is equivalent to calling selectItem / deselectItem on the collection or table.
*/ */
@property (nonatomic, assign) BOOL selected; @property (nonatomic, assign, getter=isSelected) BOOL selected;
/** /**
* A Boolean value that indicates whether the node is highlighted. * A Boolean value that is synchronized with the underlying collection or tableView cell property.
* Setting this value is equivalent to calling highlightItem / unHighlightItem on the collection or table.
*/ */
@property (nonatomic, assign) BOOL highlighted; @property (nonatomic, assign, getter=isHighlighted) BOOL highlighted;
/* /*
* ASCellNode must forward touch events in order for UITableView and UICollectionView tap handling to work. Overriding * ASCellNode must forward touch events in order for UITableView and UICollectionView tap handling to work. Overriding
@@ -148,6 +150,9 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) {
*/ */
@property (nonatomic, assign) UIEdgeInsets textInsets; @property (nonatomic, assign) UIEdgeInsets textInsets;
- (BOOL)selected ASDISPLAYNODE_DEPRECATED;
- (BOOL)highlighted ASDISPLAYNODE_DEPRECATED;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@@ -36,7 +36,7 @@
@end @end
@implementation ASCellNode @implementation ASCellNode
@synthesize layoutDelegate = _layoutDelegate; @synthesize interactionDelegate = _interactionDelegate;
- (instancetype)init - (instancetype)init
{ {
@@ -170,14 +170,40 @@
- (void)didRelayoutFromOldSize:(CGSize)oldSize toNewSize:(CGSize)newSize - (void)didRelayoutFromOldSize:(CGSize)oldSize toNewSize:(CGSize)newSize
{ {
if (_layoutDelegate != nil) { if (_interactionDelegate != nil) {
ASPerformBlockOnMainThread(^{ ASPerformBlockOnMainThread(^{
BOOL sizeChanged = !CGSizeEqualToSize(oldSize, newSize); BOOL sizeChanged = !CGSizeEqualToSize(oldSize, newSize);
[_layoutDelegate nodeDidRelayout:self sizeChanged:sizeChanged]; [_interactionDelegate nodeDidRelayout:self sizeChanged:sizeChanged];
}); });
} }
} }
- (void)setSelected:(BOOL)selected
{
if (_selected != selected) {
_selected = selected;
[_interactionDelegate nodeSelectedStateDidChange:self];
}
}
- (void)setHighlighted:(BOOL)highlighted
{
if (_highlighted != highlighted) {
_highlighted = highlighted;
[_interactionDelegate nodeHighlightedStateDidChange:self];
}
}
- (BOOL)selected
{
return self.isSelected;
}
- (BOOL)highlighted
{
return self.isSelected;
}
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" #pragma clang diagnostic ignored "-Wobjc-missing-super-calls"

View File

@@ -42,20 +42,39 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
- (void)setNode:(ASCellNode *)node - (void)setNode:(ASCellNode *)node
{ {
_node = node; _node = node;
if (node.selected != self.selected) {
node.selected = self.selected; node.selected = self.selected;
}
if (node.highlighted != self.highlighted) {
node.highlighted = self.highlighted; node.highlighted = self.highlighted;
}
} }
- (void)setSelected:(BOOL)selected - (void)setSelected:(BOOL)selected
{ {
if (selected != self.selected) {
[super setSelected:selected]; [super setSelected:selected];
}
if (selected != _node.selected) {
_node.selected = selected; _node.selected = selected;
}
} }
- (void)setHighlighted:(BOOL)highlighted - (void)setHighlighted:(BOOL)highlighted
{ {
if (highlighted != self.highlighted) {
[super setHighlighted:highlighted]; [super setHighlighted:highlighted];
}
if (highlighted != _node.highlighted) {
_node.highlighted = highlighted; _node.highlighted = highlighted;
}
}
- (void)prepareForReuse
{
// Need to clear node pointer before UIKit calls setSelected:NO / setHighlighted:NO on its cells
self.node = nil;
[super prepareForReuse];
} }
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
@@ -98,7 +117,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
#pragma mark - #pragma mark -
#pragma mark ASCollectionView. #pragma mark ASCollectionView.
@interface ASCollectionView () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASDataControllerSource, ASCellNodeLayoutDelegate, ASDelegateProxyInterceptor, ASBatchFetchingScrollView, ASDataControllerEnvironmentDelegate> { @interface ASCollectionView () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASDataControllerSource, ASCellNodeInteractionDelegate, ASDelegateProxyInterceptor, ASBatchFetchingScrollView, ASDataControllerEnvironmentDelegate> {
ASCollectionViewProxy *_proxyDataSource; ASCollectionViewProxy *_proxyDataSource;
ASCollectionViewProxy *_proxyDelegate; ASCollectionViewProxy *_proxyDelegate;
@@ -864,8 +883,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
return ^{ return ^{
__typeof__(self) strongSelf = weakSelf; __typeof__(self) strongSelf = weakSelf;
[node enterHierarchyState:ASHierarchyStateRangeManaged]; [node enterHierarchyState:ASHierarchyStateRangeManaged];
if (node.layoutDelegate == nil) { if (node.interactionDelegate == nil) {
node.layoutDelegate = strongSelf; node.interactionDelegate = strongSelf;
} }
return node; return node;
}; };
@@ -879,8 +898,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
ASCellNode *node = block(); ASCellNode *node = block();
[node enterHierarchyState:ASHierarchyStateRangeManaged]; [node enterHierarchyState:ASHierarchyStateRangeManaged];
if (node.layoutDelegate == nil) { if (node.interactionDelegate == nil) {
node.layoutDelegate = strongSelf; node.interactionDelegate = strongSelf;
} }
return node; return node;
}; };
@@ -1127,6 +1146,25 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
} }
#pragma mark - ASCellNodeDelegate #pragma mark - ASCellNodeDelegate
- (void)nodeSelectedStateDidChange:(ASCellNode *)node
{
NSIndexPath *indexPath = [self indexPathForNode:node];
if (indexPath) {
if (node.isSelected) {
[self selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
} else {
[self deselectItemAtIndexPath:indexPath animated:NO];
}
}
}
- (void)nodeHighlightedStateDidChange:(ASCellNode *)node
{
NSIndexPath *indexPath = [self indexPathForNode:node];
if (indexPath) {
[self cellForItemAtIndexPath:indexPath].highlighted = node.isHighlighted;
}
}
- (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged - (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged
{ {

View File

@@ -66,20 +66,39 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
- (void)setNode:(ASCellNode *)node - (void)setNode:(ASCellNode *)node
{ {
_node = node; _node = node;
if (node.selected != self.selected) {
node.selected = self.selected; node.selected = self.selected;
}
if (node.highlighted != self.highlighted) {
node.highlighted = self.highlighted; node.highlighted = self.highlighted;
}
} }
- (void)setSelected:(BOOL)selected animated:(BOOL)animated - (void)setSelected:(BOOL)selected animated:(BOOL)animated
{ {
if (selected != self.selected) {
[super setSelected:selected animated:animated]; [super setSelected:selected animated:animated];
}
if (selected != _node.selected) {
_node.selected = selected; _node.selected = selected;
}
} }
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{ {
if (highlighted != self.highlighted) {
[super setHighlighted:highlighted animated:animated]; [super setHighlighted:highlighted animated:animated];
}
if (highlighted != _node.highlighted) {
_node.highlighted = highlighted; _node.highlighted = highlighted;
}
}
- (void)prepareForReuse
{
// Need to clear node pointer before UIKit calls setSelected:NO / setHighlighted:NO on its cells
self.node = nil;
[super prepareForReuse];
} }
@end @end
@@ -91,7 +110,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
- (instancetype)_initWithTableView:(ASTableView *)tableView; - (instancetype)_initWithTableView:(ASTableView *)tableView;
@end @end
@interface ASTableView () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASDataControllerSource, _ASTableViewCellDelegate, ASCellNodeLayoutDelegate, ASDelegateProxyInterceptor, ASBatchFetchingScrollView, ASDataControllerEnvironmentDelegate> @interface ASTableView () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASDataControllerSource, _ASTableViewCellDelegate, ASCellNodeInteractionDelegate, ASDelegateProxyInterceptor, ASBatchFetchingScrollView, ASDataControllerEnvironmentDelegate>
{ {
ASTableViewProxy *_proxyDataSource; ASTableViewProxy *_proxyDataSource;
ASTableViewProxy *_proxyDelegate; ASTableViewProxy *_proxyDelegate;
@@ -1046,8 +1065,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
return ^{ return ^{
__typeof__(self) strongSelf = weakSelf; __typeof__(self) strongSelf = weakSelf;
[node enterHierarchyState:ASHierarchyStateRangeManaged]; [node enterHierarchyState:ASHierarchyStateRangeManaged];
if (node.layoutDelegate == nil) { if (node.interactionDelegate == nil) {
node.layoutDelegate = strongSelf; node.interactionDelegate = strongSelf;
} }
return node; return node;
}; };
@@ -1059,8 +1078,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
__typeof__(self) strongSelf = weakSelf; __typeof__(self) strongSelf = weakSelf;
ASCellNode *node = block(); ASCellNode *node = block();
[node enterHierarchyState:ASHierarchyStateRangeManaged]; [node enterHierarchyState:ASHierarchyStateRangeManaged];
if (node.layoutDelegate == nil) { if (node.interactionDelegate == nil) {
node.layoutDelegate = strongSelf; node.interactionDelegate = strongSelf;
} }
return node; return node;
}; };
@@ -1126,7 +1145,27 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
} }
} }
#pragma mark - ASCellNodeLayoutDelegate #pragma mark - ASCellNodeDelegate
- (void)nodeSelectedStateDidChange:(ASCellNode *)node
{
NSIndexPath *indexPath = [self indexPathForNode:node];
if (indexPath) {
if (node.isSelected) {
[self selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
} else {
[self deselectRowAtIndexPath:indexPath animated:NO];
}
}
}
- (void)nodeHighlightedStateDidChange:(ASCellNode *)node
{
NSIndexPath *indexPath = [self indexPathForNode:node];
if (indexPath) {
[self cellForRowAtIndexPath:indexPath].highlighted = node.isHighlighted;
}
}
- (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged - (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged
{ {

View File

@@ -13,6 +13,22 @@
#import "ASCollectionDataController.h" #import "ASCollectionDataController.h"
#import "ASCollectionViewFlowLayoutInspector.h" #import "ASCollectionViewFlowLayoutInspector.h"
@interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode
@property (nonatomic, assign) NSUInteger setSelectedCounter;
@end
@implementation ASTextCellNodeWithSetSelectedCounter
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
_setSelectedCounter++;
}
@end
@interface ASCollectionViewTestDelegate : NSObject <ASCollectionViewDataSource, ASCollectionViewDelegate> @interface ASCollectionViewTestDelegate : NSObject <ASCollectionViewDataSource, ASCollectionViewDelegate>
@property (nonatomic, assign) NSInteger numberOfSections; @property (nonatomic, assign) NSInteger numberOfSections;
@@ -32,7 +48,7 @@
} }
- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath { - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath {
ASTextCellNode *textCellNode = [ASTextCellNode new]; ASTextCellNodeWithSetSelectedCounter *textCellNode = [ASTextCellNodeWithSetSelectedCounter new];
textCellNode.text = indexPath.description; textCellNode.text = indexPath.description;
return textCellNode; return textCellNode;
@@ -41,7 +57,7 @@
- (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { - (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath {
return ^{ return ^{
ASTextCellNode *textCellNode = [ASTextCellNode new]; ASTextCellNodeWithSetSelectedCounter *textCellNode = [ASTextCellNodeWithSetSelectedCounter new];
textCellNode.text = indexPath.description; textCellNode.text = indexPath.description;
return textCellNode; return textCellNode;
}; };
@@ -134,4 +150,54 @@
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
} }
- (void)testSelection
{
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[window setRootViewController:testController];
[window makeKeyAndVisible];
[testController.collectionView reloadDataImmediately];
[testController.collectionView layoutIfNeeded];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
ASCellNode *node = [testController.collectionView nodeForItemAtIndexPath:indexPath];
// selecting node should select cell
node.selected = YES;
XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath], @"Selecting node should update cell selection.");
// deselecting node should deselect cell
node.selected = NO;
XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] isEqualToArray:@[]], @"Deselecting node should update cell selection.");
// selecting cell via collectionView should select node
[testController.collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
XCTAssertTrue(node.isSelected == YES, @"Selecting cell should update node selection.");
// deselecting cell via collectionView should deselect node
[testController.collectionView deselectItemAtIndexPath:indexPath animated:NO];
XCTAssertTrue(node.isSelected == NO, @"Deselecting cell should update node selection.");
// selecting cell should select node
UICollectionViewCell *cell = [testController.collectionView cellForItemAtIndexPath:indexPath];
cell.selected = YES;
XCTAssertTrue(node.isSelected == YES, @"Selecting cell should update node selection.");
// reload cell (-prepareForReuse is called) & check that selected state is preserved
[testController.collectionView setContentOffset:CGPointMake(0,testController.collectionView.bounds.size.height)];
[testController.collectionView layoutIfNeeded];
[testController.collectionView setContentOffset:CGPointMake(0,0)];
[testController.collectionView layoutIfNeeded];
XCTAssertTrue(node.isSelected == YES, @"Reloaded cell should preserve state.");
// deselecting cell should deselect node
cell = [testController.collectionView cellForItemAtIndexPath:indexPath];
cell.selected = NO;
XCTAssertTrue(node.isSelected == NO, @"Deselecting cell should update node selection.");
// check setSelected not called extra times
XCTAssertTrue([(ASTextCellNodeWithSetSelectedCounter *)node setSelectedCounter] == 6, @"setSelected: should not be called on node multiple times.");
}
@end @end