Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit b557075

Browse files
committed
Merge pull request #655 from nguyenhuy/relayout_editing_mode
Relayout table view cell nodes if there is a mismatch between content view size and the node's constrained size
2 parents 6e04bfb + 094d257 commit b557075

3 files changed

Lines changed: 192 additions & 55 deletions

File tree

AsyncDisplayKit/ASTableView.h

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -266,17 +266,6 @@
266266

267267
@optional
268268

269-
/**
270-
* Provides the constrained size range for measuring the node at the index path.
271-
*
272-
* @param tableView The sender.
273-
*
274-
* @param indexPath The index path of the node.
275-
*
276-
* @returns A constrained size range for layout the node at this index path.
277-
*/
278-
- (ASSizeRange)tableView:(ASTableView *)tableView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath;
279-
280269
/**
281270
* Indicator to lock the data source for data fetching in async mode.
282271
* We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistence or exception

AsyncDisplayKit/ASTableView.mm

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#import "ASDisplayNodeInternal.h"
1717
#import "ASBatchFetching.h"
1818
#import "ASInternalHelpers.h"
19+
#import "ASLayout.h"
1920

2021
//#define LOG(...) NSLog(__VA_ARGS__)
2122
#define LOG(...)
@@ -104,22 +105,31 @@ - (id)forwardingTargetForSelector:(SEL)aSelector
104105
#pragma mark -
105106
#pragma mark ASCellNode<->UITableViewCell bridging.
106107

108+
@class _ASTableViewCell;
109+
107110
@protocol _ASTableViewCellDelegate <NSObject>
108-
- (void)tableViewCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath didTransitionToState:(UITableViewCellStateMask)state;
111+
- (void)willLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell;
109112
@end
110113

111114
@interface _ASTableViewCell : UITableViewCell
112115
@property (nonatomic, weak) id<_ASTableViewCellDelegate> delegate;
113-
@property (nonatomic) NSIndexPath *indexPath;
116+
@property (nonatomic, weak) ASCellNode *node;
114117
@end
115118

116119
@implementation _ASTableViewCell
117120
// TODO add assertions to prevent use of view-backed UITableViewCell properties (eg .textLabel)
118121

122+
- (void)layoutSubviews
123+
{
124+
[_delegate willLayoutSubviewsOfTableViewCell:self];
125+
[super layoutSubviews];
126+
}
127+
119128
- (void)didTransitionToState:(UITableViewCellStateMask)state
120129
{
130+
[self setNeedsLayout];
131+
[self layoutIfNeeded];
121132
[super didTransitionToState:state];
122-
[_delegate tableViewCell:self atIndexPath:_indexPath didTransitionToState:state];
123133
}
124134

125135
@end
@@ -146,8 +156,6 @@ @interface ASTableView () <ASRangeControllerDelegate, ASDataControllerSource, _A
146156
NSIndexPath *_contentOffsetAdjustmentTopVisibleRow;
147157
CGFloat _contentOffsetAdjustment;
148158

149-
BOOL _asyncDataSourceImplementsConstrainedSizeForNode;
150-
151159
CGFloat _maxWidthForNodesConstrainedSize;
152160
BOOL _ignoreMaxWidthChange;
153161
}
@@ -271,12 +279,10 @@ - (void)setAsyncDataSource:(id<ASTableViewDataSource>)asyncDataSource
271279
super.dataSource = nil;
272280
_asyncDataSource = nil;
273281
_proxyDataSource = nil;
274-
_asyncDataSourceImplementsConstrainedSizeForNode = NO;
275282
} else {
276283
_asyncDataSource = asyncDataSource;
277284
_proxyDataSource = [[_ASTableViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self];
278285
super.dataSource = (id<UITableViewDataSource>)_proxyDataSource;
279-
_asyncDataSourceImplementsConstrainedSizeForNode = ([_asyncDataSource respondsToSelector:@selector(tableView:constrainedSizeForNodeAtIndexPath:)] ? 1 : 0);
280286
}
281287
}
282288

@@ -507,7 +513,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
507513
ASCellNode *node = [_dataController nodeAtIndexPath:indexPath];
508514
[_rangeController configureContentView:cell.contentView forCellNode:node];
509515

510-
cell.indexPath = indexPath;
516+
cell.node = node;
511517
cell.backgroundColor = node.backgroundColor;
512518
cell.selectionStyle = node.selectionStyle;
513519

@@ -798,11 +804,6 @@ - (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPat
798804

799805
- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
800806
{
801-
if (_asyncDataSourceImplementsConstrainedSizeForNode) {
802-
return [_asyncDataSource tableView:self constrainedSizeForNodeAtIndexPath:indexPath];
803-
}
804-
805-
// Default size range
806807
return ASSizeRangeMake(CGSizeMake(_maxWidthForNodesConstrainedSize, 0),
807808
CGSizeMake(_maxWidthForNodesConstrainedSize, FLT_MAX));
808809
}
@@ -845,22 +846,31 @@ - (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController
845846

846847
#pragma mark - _ASTableViewCellDelegate
847848

848-
- (void)tableViewCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath didTransitionToState:(UITableViewCellStateMask)state
849+
- (void)willLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell
849850
{
850-
[self beginUpdates];
851-
ASCellNode *node = [_dataController nodeAtIndexPath:indexPath];
851+
CGFloat contentViewWidth = tableViewCell.contentView.bounds.size.width;
852+
ASCellNode *node = tableViewCell.node;
853+
ASSizeRange constrainedSize = node.constrainedSizeForCalculatedLayout;
852854

853-
ASSizeRange constrainedSize = [self dataController:_dataController constrainedSizeForNodeAtIndexPath:indexPath];
854-
if (state != UITableViewCellStateDefaultMask) {
855-
// Edit control or delete confirmation was shown and size of content view was changed.
856-
// The new size should be taken into consideration.
857-
constrainedSize.min.width = MIN(cell.contentView.frame.size.width, constrainedSize.min.width);
858-
constrainedSize.max.width = MIN(cell.contentView.frame.size.width, constrainedSize.max.width);
855+
// Table view cells should always fill its content view width.
856+
// Normally the content view width equals to the constrained size width (which equals to the table view width).
857+
// If there is a mismatch between these values, for example after the table view entered or left editing mode,
858+
// content view width is preferred and used to re-measure the cell node.
859+
if (contentViewWidth != constrainedSize.max.width) {
860+
constrainedSize.min.width = contentViewWidth;
861+
constrainedSize.max.width = contentViewWidth;
862+
863+
// Re-measurement is done on main to ensure thread affinity. In the worst case, this is as fast as UIKit's implementation.
864+
//
865+
// Unloaded nodes *could* be re-measured off the main thread, but only with the assumption that content view width
866+
// is the same for all cells (because there is no easy way to get that individual value before the node being assigned to a _ASTableViewCell).
867+
// Also, in many cases, some nodes may not need to be re-measured at all, such as when user enters and then immediately leaves editing mode.
868+
// To avoid premature optimization and making such assumption, as well as to keep ASTableView simple, re-measurement is strictly done on main.
869+
[self beginUpdates];
870+
CGSize calculatedSize = [[node measureWithSizeRange:constrainedSize] size];
871+
node.frame = CGRectMake(0, 0, calculatedSize.width, calculatedSize.height);
872+
[self endUpdates];
859873
}
860-
861-
[node measureWithSizeRange:constrainedSize];
862-
node.frame = CGRectMake(0, 0, node.calculatedSize.width, node.calculatedSize.height);
863-
[self endUpdates];
864874
}
865875

866876
@end

AsyncDisplayKitTests/ASTableViewTests.m

Lines changed: 156 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import <XCTest/XCTest.h>
1010

1111
#import "ASTableView.h"
12+
#import "ASDisplayNode+Subclasses.h"
1213

1314
#define NumberOfSections 10
1415
#define NumberOfRowsPerSection 20
@@ -56,9 +57,24 @@ - (void)dealloc
5657

5758
@end
5859

60+
@interface ASTestTextCellNode : ASTextCellNode
61+
/** Calculated by counting how many times -layoutSpecThatFits: is called on the main thread. */
62+
@property (atomic) int numberOfLayoutsOnMainThread;
63+
@end
64+
65+
@implementation ASTestTextCellNode
66+
67+
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
68+
{
69+
if ([NSThread isMainThread]) {
70+
_numberOfLayoutsOnMainThread++;
71+
}
72+
return [super layoutSpecThatFits:constrainedSize];
73+
}
74+
75+
@end
76+
5977
@interface ASTableViewFilledDataSource : NSObject <ASTableViewDataSource, ASTableViewDelegate>
60-
/** Calculated by counting how many times a constrained size is asked for the first node on main thread. */
61-
@property (atomic) int numberOfRelayouts;
6278
@end
6379

6480
@implementation ASTableViewFilledDataSource
@@ -75,22 +91,12 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger
7591

7692
- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath
7793
{
78-
ASTextCellNode *textCellNode = [ASTextCellNode new];
94+
ASTestTextCellNode *textCellNode = [ASTestTextCellNode new];
7995
textCellNode.text = indexPath.description;
8096

8197
return textCellNode;
8298
}
8399

84-
- (ASSizeRange)tableView:(ASTableView *)tableView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
85-
{
86-
if ([NSThread isMainThread] && indexPath.section == 0 && indexPath.row == 0) {
87-
_numberOfRelayouts++;
88-
}
89-
CGFloat maxWidth = tableView.bounds.size.width;
90-
return ASSizeRangeMake(CGSizeMake(maxWidth, 0),
91-
CGSizeMake(maxWidth, FLT_MAX));
92-
}
93-
94100
@end
95101

96102
@interface ASTableViewTests : XCTestCase
@@ -260,7 +266,7 @@ - (void)testRelayoutAllRowsWithZeroSizeInitially
260266

261267
- (void)triggerSizeChangeAndAssertRelayoutAllRowsForTableView:(ASTableView *)tableView newSize:(CGSize)newSize
262268
{
263-
XCTestExpectation *nodesMeasuredUsingNewConstrainedSizeExpectation = [self expectationWithDescription:@"nodesMeasuredUsingNewConstrainedSizeExpectation"];
269+
XCTestExpectation *nodesMeasuredUsingNewConstrainedSizeExpectation = [self expectationWithDescription:@"nodesMeasuredUsingNewConstrainedSize"];
264270

265271
[tableView beginUpdates];
266272

@@ -270,13 +276,11 @@ - (void)triggerSizeChangeAndAssertRelayoutAllRowsForTableView:(ASTableView *)tab
270276
[tableView layoutIfNeeded];
271277

272278
[tableView endUpdatesAnimated:NO completion:^(BOOL completed) {
273-
int numberOfRelayouts = ((ASTableViewFilledDataSource *)(tableView.asyncDataSource)).numberOfRelayouts;
274-
XCTAssertEqual(numberOfRelayouts, 1);
275-
276279
for (int section = 0; section < NumberOfSections; section++) {
277280
for (int row = 0; row < NumberOfRowsPerSection; row++) {
278281
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
279-
ASCellNode *node = [tableView nodeForRowAtIndexPath:indexPath];
282+
ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath];
283+
XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1);
280284
XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, newSize.width);
281285
}
282286
}
@@ -290,4 +294,138 @@ - (void)triggerSizeChangeAndAssertRelayoutAllRowsForTableView:(ASTableView *)tab
290294
}];
291295
}
292296

297+
- (void)testRelayoutVisibleRowsWhenEditingModeIsChanged
298+
{
299+
CGSize tableViewSize = CGSizeMake(100, 500);
300+
ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height)
301+
style:UITableViewStylePlain
302+
asyncDataFetching:YES];
303+
ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new];
304+
305+
tableView.asyncDelegate = dataSource;
306+
tableView.asyncDataSource = dataSource;
307+
308+
XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"];
309+
[tableView reloadDataWithCompletion:^{
310+
for (int section = 0; section < NumberOfSections; section++) {
311+
for (int row = 0; row < NumberOfRowsPerSection; row++) {
312+
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
313+
ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath];
314+
XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0);
315+
XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width);
316+
}
317+
}
318+
[reloadDataExpectation fulfill];
319+
}];
320+
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
321+
if (error) {
322+
XCTFail(@"Expectation failed: %@", error);
323+
}
324+
}];
325+
326+
NSArray *visibleNodes = [tableView visibleNodes];
327+
XCTAssertGreaterThan(visibleNodes.count, 0);
328+
329+
// Cause table view to enter editing mode.
330+
// Visibile nodes should be re-measured on main thread with the new (smaller) content view width.
331+
// Other nodes are untouched.
332+
XCTestExpectation *relayoutAfterEnablingEditingExpectation = [self expectationWithDescription:@"relayoutAfterEnablingEditing"];
333+
[tableView beginUpdates];
334+
[tableView setEditing:YES];
335+
[tableView endUpdatesAnimated:YES completion:^(BOOL completed) {
336+
for (int section = 0; section < NumberOfSections; section++) {
337+
for (int row = 0; row < NumberOfRowsPerSection; row++) {
338+
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
339+
ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath];
340+
if ([visibleNodes containsObject:node]) {
341+
XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1);
342+
XCTAssertLessThan(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width);
343+
} else {
344+
XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0);
345+
XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width);
346+
}
347+
}
348+
}
349+
[relayoutAfterEnablingEditingExpectation fulfill];
350+
}];
351+
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
352+
if (error) {
353+
XCTFail(@"Expectation failed: %@", error);
354+
}
355+
}];
356+
357+
// Cause table view to leave editing mode.
358+
// Visibile nodes should be re-measured again.
359+
// All nodes should have max constrained width equals to the table view width.
360+
XCTestExpectation *relayoutAfterDisablingEditingExpectation = [self expectationWithDescription:@"relayoutAfterDisablingEditing"];
361+
[tableView beginUpdates];
362+
[tableView setEditing:NO];
363+
[tableView endUpdatesAnimated:YES completion:^(BOOL completed) {
364+
for (int section = 0; section < NumberOfSections; section++) {
365+
for (int row = 0; row < NumberOfRowsPerSection; row++) {
366+
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
367+
ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath];
368+
BOOL visible = [visibleNodes containsObject:node];
369+
XCTAssertEqual(node.numberOfLayoutsOnMainThread, visible ? 2: 0);
370+
XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width);
371+
}
372+
}
373+
[relayoutAfterDisablingEditingExpectation fulfill];
374+
}];
375+
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
376+
if (error) {
377+
XCTFail(@"Expectation failed: %@", error);
378+
}
379+
}];
380+
}
381+
382+
- (void)testRelayoutRowsAfterEditingModeIsChangedAndTheyBecomeVisible
383+
{
384+
CGSize tableViewSize = CGSizeMake(100, 500);
385+
ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height)
386+
style:UITableViewStylePlain
387+
asyncDataFetching:YES];
388+
ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new];
389+
390+
tableView.asyncDelegate = dataSource;
391+
tableView.asyncDataSource = dataSource;
392+
393+
XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"];
394+
[tableView reloadDataWithCompletion:^{
395+
for (int section = 0; section < NumberOfSections; section++) {
396+
for (int row = 0; row < NumberOfRowsPerSection; row++) {
397+
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
398+
ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath];
399+
XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0);
400+
XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width);
401+
}
402+
}
403+
[reloadDataExpectation fulfill];
404+
}];
405+
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
406+
if (error) {
407+
XCTFail(@"Expectation failed: %@", error);
408+
}
409+
}];
410+
411+
// Cause table view to enter editing mode and then scroll to the bottom.
412+
// The last node should be re-measured on main thread with the new (smaller) content view width.
413+
NSIndexPath *lastRowIndexPath = [NSIndexPath indexPathForRow:(NumberOfRowsPerSection - 1) inSection:(NumberOfSections - 1)];
414+
XCTestExpectation *relayoutExpectation = [self expectationWithDescription:@"relayout"];
415+
[tableView beginUpdates];
416+
[tableView setEditing:YES];
417+
[tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX) animated:YES];
418+
[tableView endUpdatesAnimated:YES completion:^(BOOL completed) {
419+
ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:lastRowIndexPath];
420+
XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1);
421+
XCTAssertLessThan(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width);
422+
[relayoutExpectation fulfill];
423+
}];
424+
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
425+
if (error) {
426+
XCTFail(@"Expectation failed: %@", error);
427+
}
428+
}];
429+
}
430+
293431
@end

0 commit comments

Comments
 (0)