From c5af8028b61ba559f29af11b6f979af94aad92cc Mon Sep 17 00:00:00 2001 From: Niels van Hoorn Date: Wed, 27 Jan 2016 17:19:28 +0100 Subject: [PATCH 01/15] Added Prefix Headers to framework --- AsyncDisplayKit.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 04868896b6..fd6c8605dd 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -2218,6 +2218,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREFIX_HEADER = "AsyncDisplayKit/AsyncDisplayKit-Prefix.pch"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", @@ -2252,6 +2253,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREFIX_HEADER = "AsyncDisplayKit/AsyncDisplayKit-Prefix.pch"; INFOPLIST_FILE = "$(SRCROOT)/AsyncDisplayKit-iOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; From 263bb311f2cfd0f7c5b718d3f3c26f52b3b38eaa Mon Sep 17 00:00:00 2001 From: Bin Liu Date: Wed, 27 Jan 2016 10:50:30 -0800 Subject: [PATCH 02/15] expose beginUpdates and endUpdates --- AsyncDisplayKit/ASCollectionNode+Beta.h | 2 ++ AsyncDisplayKit/ASCollectionNode.mm | 10 ++++++++++ AsyncDisplayKit/ASCollectionView.mm | 5 +++++ AsyncDisplayKit/Details/ASCollectionInternal.h | 2 ++ 4 files changed, 19 insertions(+) diff --git a/AsyncDisplayKit/ASCollectionNode+Beta.h b/AsyncDisplayKit/ASCollectionNode+Beta.h index 11ea3ac2fd..e05e740ba2 100644 --- a/AsyncDisplayKit/ASCollectionNode+Beta.h +++ b/AsyncDisplayKit/ASCollectionNode+Beta.h @@ -14,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN @interface ASCollectionNode (Beta) - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(nullable id)layoutFacilitator; +- (void)beginUpdates; +- (void)endUpdatesAnimated:(BOOL)animated; @end diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index 5bed1a1bbf..204c434621 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -187,6 +187,16 @@ [self.view clearFetchedData]; } +- (void)beginUpdates +{ + [self.view.dataController beginUpdates]; +} + +- (void)endUpdatesAnimated:(BOOL)animated +{ + [self.view.dataController endUpdatesAnimated:animated completion:nil]; +} + #pragma mark - ASCollectionView Forwards - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 2b588d088b..62294e3415 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -396,6 +396,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; #pragma mark Assertions. +- (ASDataController *)dataController +{ + return _dataController; +} + - (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/Details/ASCollectionInternal.h b/AsyncDisplayKit/Details/ASCollectionInternal.h index a0aff1e573..5bf70dfd35 100644 --- a/AsyncDisplayKit/Details/ASCollectionInternal.h +++ b/AsyncDisplayKit/Details/ASCollectionInternal.h @@ -8,11 +8,13 @@ #import "ASCollectionView.h" #import "ASCollectionNode.h" +#import "ASDataController.h" #import "ASRangeController.h" @interface ASCollectionView () - (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(id)layoutFacilitator ownedByNode:(BOOL)ownedByNode; +@property (nonatomic, strong, readonly) ASDataController *dataController; @property (nonatomic, weak, readwrite) ASCollectionNode *collectionNode; @property (nonatomic, strong, readonly) ASRangeController *rangeController; @end From f6be279c60ddd54c51b4f0e3966114a17a2fa485 Mon Sep 17 00:00:00 2001 From: Bin Liu Date: Wed, 27 Jan 2016 12:50:41 -0800 Subject: [PATCH 03/15] Added more facilitator methods --- AsyncDisplayKit/ASCollectionNode.mm | 4 ++-- AsyncDisplayKit/ASCollectionView.mm | 14 +++++++++---- ...SCollectionViewLayoutFacilitatorProtocol.h | 21 +++++++++++++++++++ .../Details/ASCollectionInternal.h | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index 204c434621..3d5c3d9288 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -189,12 +189,12 @@ - (void)beginUpdates { - [self.view.dataController beginUpdates]; + [self.view.dataController beginUpdates]; } - (void)endUpdatesAnimated:(BOOL)animated { - [self.view.dataController endUpdatesAnimated:animated completion:nil]; + [self.view.dataController endUpdatesAnimated:animated completion:nil]; } #pragma mark - ASCollectionView Forwards diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 62294e3415..7cae446342 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -398,7 +398,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (ASDataController *)dataController { - return _dataController; + return _dataController; } - (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion @@ -856,6 +856,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } ASPerformBlockWithoutAnimation(!animated, ^{ + [_layoutFacilitator collectionViewWillPerformBatchUpdates]; [super performBatchUpdates:^{ for (dispatch_block_t block in _batchUpdateBlocks) { block(); @@ -870,17 +871,18 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - [_layoutFacilitator collectionViewEditingCellsAtIndexPaths:indexPaths]; if (!self.asyncDataSource || _superIsPendingDataLoad) { return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewBatchingCellEditsAtIndexPaths:indexPaths]; [_batchUpdateBlocks addObject:^{ [super insertItemsAtIndexPaths:indexPaths]; }]; } else { [UIView performWithoutAnimation:^{ + [_layoutFacilitator collectionViewEditingCellsAtIndexPaths:indexPaths]; [super insertItemsAtIndexPaths:indexPaths]; }]; } @@ -895,11 +897,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewBatchingCellEditsAtIndexPaths:indexPaths]; [_batchUpdateBlocks addObject:^{ [super deleteItemsAtIndexPaths:indexPaths]; }]; } else { [UIView performWithoutAnimation:^{ + [_layoutFacilitator collectionViewEditingCellsAtIndexPaths:indexPaths]; [super deleteItemsAtIndexPaths:indexPaths]; }]; } @@ -908,17 +912,18 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - [_layoutFacilitator collectionViewEditingSectionsAtIndexSet:indexSet]; if (!self.asyncDataSource || _superIsPendingDataLoad) { return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewBatchingSectionEditsAtIndexes:indexSet]; [_batchUpdateBlocks addObject:^{ [super insertSections:indexSet]; }]; } else { [UIView performWithoutAnimation:^{ + [_layoutFacilitator collectionViewEditingSectionsAtIndexSet:indexSet]; [super insertSections:indexSet]; }]; } @@ -927,17 +932,18 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - [_layoutFacilitator collectionViewEditingSectionsAtIndexSet:indexSet]; if (!self.asyncDataSource || _superIsPendingDataLoad) { return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } if (_performingBatchUpdates) { + [_layoutFacilitator collectionViewBatchingSectionEditsAtIndexes:indexSet]; [_batchUpdateBlocks addObject:^{ [super deleteSections:indexSet]; }]; } else { [UIView performWithoutAnimation:^{ + [_layoutFacilitator collectionViewEditingSectionsAtIndexSet:indexSet]; [super deleteSections:indexSet]; }]; } diff --git a/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h b/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h index 719c98419e..9e0ff567e9 100644 --- a/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h +++ b/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h @@ -25,6 +25,27 @@ */ - (void)collectionViewEditingSectionsAtIndexSet:(NSIndexSet *)indexes; +/** + * Inform that the collectionView is adding some cell updates into a batch, + * and will perform these batch updates at a later point + * + * NOTE: used in combination with -collectionViewWillPerformBatchUpdates + */ +- (void)collectionViewBatchingCellEditsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Inform that the collectionView is adding some section updates into a batch, + * and will perform these batch updates at a later point + * + * NOTE: used in combination with -collectionViewWillPerformBatchUpdates + */ +- (void)collectionViewBatchingSectionEditsAtIndexes:(NSIndexSet *)indexes; + +/** + * Informs the delegate that the collectionView is about to call performBatchUpdates + */ +- (void)collectionViewWillPerformBatchUpdates; + @end #endif /* ASCollectionViewLayoutFacilitatorProtocol_h */ diff --git a/AsyncDisplayKit/Details/ASCollectionInternal.h b/AsyncDisplayKit/Details/ASCollectionInternal.h index 5bf70dfd35..55e99d6a36 100644 --- a/AsyncDisplayKit/Details/ASCollectionInternal.h +++ b/AsyncDisplayKit/Details/ASCollectionInternal.h @@ -14,7 +14,7 @@ @interface ASCollectionView () - (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(id)layoutFacilitator ownedByNode:(BOOL)ownedByNode; -@property (nonatomic, strong, readonly) ASDataController *dataController; @property (nonatomic, weak, readwrite) ASCollectionNode *collectionNode; +@property (nonatomic, strong, readonly) ASDataController *dataController; @property (nonatomic, strong, readonly) ASRangeController *rangeController; @end From 733c65d1eb409b0836ebe2b1f4881d0cef88453d Mon Sep 17 00:00:00 2001 From: Luke Zhao Date: Wed, 27 Jan 2016 13:49:00 -0800 Subject: [PATCH 04/15] fix bug: constraint size not updated for frontsizeadjuster --- AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h | 1 + AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m | 2 -- AsyncDisplayKit/TextKit/ASTextKitRenderer.mm | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h index b386eb97f3..84ba5fa367 100644 --- a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h +++ b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.h @@ -10,6 +10,7 @@ @interface ASTextKitFontSizeAdjuster : NSObject +@property (nonatomic, assign) CGSize constrainedSize; - (instancetype)initWithContext:(ASTextKitContext *)context minimumScaleFactor:(CGFloat)minimumScaleFactor diff --git a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m index 9ba7bb6ddb..731c19c208 100644 --- a/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m +++ b/AsyncDisplayKit/TextKit/ASTextKitFontSizeAdjuster.m @@ -13,7 +13,6 @@ { __weak ASTextKitContext *_context; CGFloat _minimumScaleFactor; - CGSize _constrainedSize; } - (instancetype)initWithContext:(ASTextKitContext *)context @@ -28,7 +27,6 @@ return self; } - - (CGSize)sizeForAttributedString:(NSAttributedString *)attrString { return [attrString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index 90527cc6f9..5c0c144452 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -125,11 +125,12 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() _sizeIsCalculated = NO; _constrainedSize = constrainedSize; // If the context isn't created yet, it will be initialized with the appropriate size when next accessed. - if (_context) { + if (_context || _fontSizeAdjuster) { // If we're updating an existing context, make sure to use the same inset logic used during initialization. // This codepath allows us to reuse the CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:constrainedSize]; - _context.constrainedSize = shadowConstrainedSize; + if (_context) _context.constrainedSize = shadowConstrainedSize; + if (_fontSizeAdjuster) _fontSizeAdjuster.constrainedSize = shadowConstrainedSize; } } } From ec7a3599bd30b3b1c4fb8890b1385d5957e0281e Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 14:52:37 -0800 Subject: [PATCH 05/15] Add `setNeedsDataFetch` method to queue off screen `fetchData` calls --- AsyncDisplayKit/ASDisplayNode.h | 8 +++++- AsyncDisplayKit/ASDisplayNode.mm | 14 ++++++++-- AsyncDisplayKit/ASDisplayNodeExtras.h | 27 +++++++++++++++++++ .../Private/ASDisplayNodeInternal.h | 2 ++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index ab3b40b524..e462e0a76b 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -442,7 +442,6 @@ NS_ASSUME_NONNULL_BEGIN * * @see displaySuspended and setNeedsDisplay */ - - (void)recursivelyClearContents; /** @@ -465,6 +464,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)recursivelyFetchData; +/** + * @abstract Marks the node as needing to call fetchData + * @discussion If the node is outside of the preload range, it is queued to call fetchData the next time it enters the range. + * Otherwise, fetchData is called immediately if the node is currently within the preload range. + */ +- (void)setNeedsDataFetch; + /** * @abstract Toggle displaying a placeholder over the node that covers content until the node and all subnodes are * displayed. diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 9c7cbe6d75..9921de366a 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1727,6 +1727,15 @@ static BOOL ShouldUseNewRenderingRange = YES; // subclass override } +- (void)setNeedsDataFetch +{ + if (ASInterfaceStateIncludesFetchData(_interfaceState)) { + [self fetchData]; + } else { + _needsDataFetch = YES; + } +} + // TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or enterInterfaceState: - (void)recursivelyFetchData { @@ -1793,9 +1802,10 @@ static BOOL ShouldUseNewRenderingRange = YES; BOOL nowFetchData = ASInterfaceStateIncludesFetchData(newState); BOOL wasFetchData = ASInterfaceStateIncludesFetchData(oldState); - if (nowFetchData != wasFetchData) { - if (nowFetchData) { + if (nowFetchData != wasFetchData || _needsDataFetch) { + if (nowFetchData || _needsDataFetch) { [self fetchData]; + _needsDataFetch = NO; } else { if ([self supportsRangeManagedInterfaceState]) { [self clearFetchedData]; diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 0ab9957cc3..e453c84ea7 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -29,6 +29,33 @@ inline BOOL ASInterfaceStateIncludesFetchData(ASInterfaceState interfaceState) return ((interfaceState & ASInterfaceStateFetchData) == ASInterfaceStateFetchData); } +inline BOOL ASInterfaceStateIncludesMeasureLayout(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateMeasureLayout) == ASInterfaceStateMeasureLayout); +} + +inline NSString * _Nonnull NSStringFromASInterfaceState(ASInterfaceState interfaceState) +{ + NSMutableString *result = [NSMutableString stringWithString:@"{ "]; + if (interfaceState == ASInterfaceStateNone) { + [result appendString:@"No state"]; + } + if (ASInterfaceStateIncludesMeasureLayout(interfaceState)) { + [result appendString:@"MeasureLayout"]; + } + if (ASInterfaceStateIncludesFetchData(interfaceState)) { + [result appendString:@" - FetchData"]; + } + if (ASInterfaceStateIncludesDisplay(interfaceState)) { + [result appendString:@" - Display"]; + } + if (ASInterfaceStateIncludesVisible(interfaceState)) { + [result appendString:@" - Visible"]; + } + [result appendString:@" }"]; + return result; +} + NS_ASSUME_NONNULL_BEGIN ASDISPLAYNODE_EXTERN_C_BEGIN diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index f1ffb377e9..ade418dd89 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -100,6 +100,8 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) } _flags; ASDisplayNodeExtraIvars _extra; + + BOOL _needsDataFetch; #if TIME_DISPLAYNODE_OPS @public From b26337c44930cd1c8166ebba4f7f005f977d60d4 Mon Sep 17 00:00:00 2001 From: Bin Liu Date: Wed, 27 Jan 2016 15:05:56 -0800 Subject: [PATCH 06/15] Levi's comments --- AsyncDisplayKit/ASCollectionView.mm | 17 ++++++------ ...SCollectionViewLayoutFacilitatorProtocol.h | 26 +++++++------------ 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 7cae446342..e8ae908593 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -876,13 +876,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewBatchingCellEditsAtIndexPaths:indexPaths]; + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:YES]; [_batchUpdateBlocks addObject:^{ [super insertItemsAtIndexPaths:indexPaths]; }]; } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; [UIView performWithoutAnimation:^{ - [_layoutFacilitator collectionViewEditingCellsAtIndexPaths:indexPaths]; [super insertItemsAtIndexPaths:indexPaths]; }]; } @@ -891,19 +891,18 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - [_layoutFacilitator collectionViewEditingCellsAtIndexPaths:indexPaths]; if (!self.asyncDataSource || _superIsPendingDataLoad) { return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewBatchingCellEditsAtIndexPaths:indexPaths]; + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:YES]; [_batchUpdateBlocks addObject:^{ [super deleteItemsAtIndexPaths:indexPaths]; }]; } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; [UIView performWithoutAnimation:^{ - [_layoutFacilitator collectionViewEditingCellsAtIndexPaths:indexPaths]; [super deleteItemsAtIndexPaths:indexPaths]; }]; } @@ -917,13 +916,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewBatchingSectionEditsAtIndexes:indexSet]; + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:YES]; [_batchUpdateBlocks addObject:^{ [super insertSections:indexSet]; }]; } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:NO]; [UIView performWithoutAnimation:^{ - [_layoutFacilitator collectionViewEditingSectionsAtIndexSet:indexSet]; [super insertSections:indexSet]; }]; } @@ -937,13 +936,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } if (_performingBatchUpdates) { - [_layoutFacilitator collectionViewBatchingSectionEditsAtIndexes:indexSet]; + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:YES]; [_batchUpdateBlocks addObject:^{ [super deleteSections:indexSet]; }]; } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:NO]; [UIView performWithoutAnimation:^{ - [_layoutFacilitator collectionViewEditingSectionsAtIndexSet:indexSet]; [super deleteSections:indexSet]; }]; } diff --git a/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h b/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h index 9e0ff567e9..514df97448 100644 --- a/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h +++ b/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h @@ -17,29 +17,23 @@ /** * Inform that the collectionView is editing the cells at a list of indexPaths + * + * @param indexPaths, an array of NSIndexPath objects of cells being/will be edited. + * @param isBatched, indicates whether the editing operation will be batched by the collectionView + * + * NOTE: when isBatched, used in combination with -collectionViewWillPerformBatchUpdates */ -- (void)collectionViewEditingCellsAtIndexPaths:(NSArray *)indexPaths; +- (void)collectionViewWillEditCellsAtIndexPaths:(NSArray *)indexPaths batched:(BOOL)isBatched; /** * Inform that the collectionView is editing the sections at a set of indexes - */ -- (void)collectionViewEditingSectionsAtIndexSet:(NSIndexSet *)indexes; - -/** - * Inform that the collectionView is adding some cell updates into a batch, - * and will perform these batch updates at a later point * - * NOTE: used in combination with -collectionViewWillPerformBatchUpdates - */ -- (void)collectionViewBatchingCellEditsAtIndexPaths:(NSArray *)indexPaths; - -/** - * Inform that the collectionView is adding some section updates into a batch, - * and will perform these batch updates at a later point + * @param indexes, an NSIndexSet of section indexes being/will be edited. + * @param isBatched, indicates whether the editing operation will be batched by the collectionView * - * NOTE: used in combination with -collectionViewWillPerformBatchUpdates + * NOTE: when isBatched, used in combination with -collectionViewWillPerformBatchUpdates */ -- (void)collectionViewBatchingSectionEditsAtIndexes:(NSIndexSet *)indexes; +- (void)collectionViewWillEditSectionsAtIndexSet:(NSIndexSet *)indexes batched:(BOOL)batched; /** * Informs the delegate that the collectionView is about to call performBatchUpdates From 585b1215a9b774e994de89e4ab807ab2d6a20d6c Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 17:19:11 -0800 Subject: [PATCH 07/15] Recursively fetchData for requests --- AsyncDisplayKit/ASDisplayNode.mm | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 9921de366a..32d02650ee 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1730,7 +1730,7 @@ static BOOL ShouldUseNewRenderingRange = YES; - (void)setNeedsDataFetch { if (ASInterfaceStateIncludesFetchData(_interfaceState)) { - [self fetchData]; + [self recursivelyFetchData]; } else { _needsDataFetch = YES; } @@ -1802,10 +1802,13 @@ static BOOL ShouldUseNewRenderingRange = YES; BOOL nowFetchData = ASInterfaceStateIncludesFetchData(newState); BOOL wasFetchData = ASInterfaceStateIncludesFetchData(oldState); - if (nowFetchData != wasFetchData || _needsDataFetch) { - if (nowFetchData || _needsDataFetch) { + // When a node has been queued up for a data fetch via setNeedsDataFetch, override the default behavior + if (_needsDataFetch) { + [self recursivelyFetchData]; + _needsDataFetch = NO; + } else if (nowFetchData != wasFetchData) { + if (nowFetchData) { [self fetchData]; - _needsDataFetch = NO; } else { if ([self supportsRangeManagedInterfaceState]) { [self clearFetchedData]; From bb02a2d487085af1e20fe4876b1be0be93efcc12 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 17:19:40 -0800 Subject: [PATCH 08/15] Clean up interface state printing style --- AsyncDisplayKit/ASDisplayNodeExtras.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index e453c84ea7..7cdecda107 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -44,13 +44,13 @@ inline NSString * _Nonnull NSStringFromASInterfaceState(ASInterfaceState interfa [result appendString:@"MeasureLayout"]; } if (ASInterfaceStateIncludesFetchData(interfaceState)) { - [result appendString:@" - FetchData"]; + [result appendString:@" | FetchData"]; } if (ASInterfaceStateIncludesDisplay(interfaceState)) { - [result appendString:@" - Display"]; + [result appendString:@" | Display"]; } if (ASInterfaceStateIncludesVisible(interfaceState)) { - [result appendString:@" - Visible"]; + [result appendString:@" | Visible"]; } [result appendString:@" }"]; return result; From 411d0b3debdcfd4003ba5b5f75e881c4686d7cac Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 18:24:59 -0800 Subject: [PATCH 09/15] Add tests for setNeedsDataFetch and out of range fetch behavior --- AsyncDisplayKitTests/ASDisplayNodeTests.m | 56 +++++++++++++++++------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index e343e115b3..9f34114231 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -13,6 +13,7 @@ #import "_ASDisplayLayer.h" #import "_ASDisplayView.h" #import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNodeTestsHelper.h" #import "UIView+ASConvenience.h" #import "ASCellNode.h" @@ -1715,22 +1716,49 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point // the fetch data interface state. - (void)testInterfaceStateForCellNode { - ASCellNode *cellNode = [ASCellNode new]; - ASTestDisplayNode *node = [ASTestDisplayNode new]; - XCTAssert(node.interfaceState == ASInterfaceStateNone); - XCTAssert(!node.hasFetchedData); + ASCellNode *cellNode = [ASCellNode new]; + ASTestDisplayNode *node = [ASTestDisplayNode new]; + XCTAssert(node.interfaceState == ASInterfaceStateNone); + XCTAssert(!node.hasFetchedData); - // Simulate range handler updating cell node. - [cellNode addSubnode:node]; - [cellNode enterInterfaceState:ASInterfaceStateFetchData]; - XCTAssert(node.hasFetchedData); - XCTAssert(node.interfaceState == ASInterfaceStateFetchData); + // Simulate range handler updating cell node. + [cellNode addSubnode:node]; + [cellNode enterInterfaceState:ASInterfaceStateFetchData]; + XCTAssert(node.hasFetchedData); + XCTAssert(node.interfaceState == ASInterfaceStateFetchData); - // If the node goes into a view it should not adopt the `InHierarchy` state. - ASTestWindow *window = [ASTestWindow new]; - [window addSubview:cellNode.view]; - XCTAssert(node.hasFetchedData); - XCTAssert(node.interfaceState == ASInterfaceStateInHierarchy); + // If the node goes into a view it should not adopt the `InHierarchy` state. + ASTestWindow *window = [ASTestWindow new]; + [window addSubview:cellNode.view]; + XCTAssert(node.hasFetchedData); + XCTAssert(node.interfaceState == ASInterfaceStateInHierarchy); +} + +- (void)testSetNeedsDataFetchImmediateState +{ + ASCellNode *cellNode = [ASCellNode new]; + ASTestDisplayNode *node = [ASTestDisplayNode new]; + [cellNode addSubnode:node]; + [cellNode enterInterfaceState:ASInterfaceStateFetchData]; + node.hasFetchedData = NO; + [cellNode setNeedsDataFetch]; + XCTAssert(node.hasFetchedData); +} + +- (void)testFetchDataExitingAndEnteringRange +{ + ASCellNode *cellNode = [ASCellNode new]; + ASTestDisplayNode *node = [ASTestDisplayNode new]; + [cellNode addSubnode:node]; + [cellNode setHierarchyState:ASHierarchyStateRangeManaged]; + + // Simulate enter range, fetch data, exit range + [cellNode enterInterfaceState:ASInterfaceStateFetchData]; + [cellNode exitInterfaceState:ASInterfaceStateFetchData]; + node.hasFetchedData = NO; + [cellNode enterInterfaceState:ASInterfaceStateFetchData]; + + XCTAssert(node.hasFetchedData); } - (void)testInitWithViewClass From ee23830318dd61ba92862f0ee0743f30eee9cfde Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 18:25:39 -0800 Subject: [PATCH 10/15] Remove setNeedsDataFetch queuing, as interfaceState always performs fetch on re-enter --- AsyncDisplayKit/ASDisplayNode.mm | 8 +------- AsyncDisplayKit/Private/ASDisplayNodeInternal.h | 2 -- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 32d02650ee..bdde67d6ab 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1731,8 +1731,6 @@ static BOOL ShouldUseNewRenderingRange = YES; { if (ASInterfaceStateIncludesFetchData(_interfaceState)) { [self recursivelyFetchData]; - } else { - _needsDataFetch = YES; } } @@ -1802,11 +1800,7 @@ static BOOL ShouldUseNewRenderingRange = YES; BOOL nowFetchData = ASInterfaceStateIncludesFetchData(newState); BOOL wasFetchData = ASInterfaceStateIncludesFetchData(oldState); - // When a node has been queued up for a data fetch via setNeedsDataFetch, override the default behavior - if (_needsDataFetch) { - [self recursivelyFetchData]; - _needsDataFetch = NO; - } else if (nowFetchData != wasFetchData) { + if (nowFetchData != wasFetchData) { if (nowFetchData) { [self fetchData]; } else { diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index ade418dd89..4bac260403 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -101,8 +101,6 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) ASDisplayNodeExtraIvars _extra; - BOOL _needsDataFetch; - #if TIME_DISPLAYNODE_OPS @public NSTimeInterval _debugTimeToCreateView; From 55e289836ed0c47ad9738a3f85704bf53ca40f20 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 18:34:21 -0800 Subject: [PATCH 11/15] Clean up output of NSStringFromASInterfaceState --- AsyncDisplayKit/ASDisplayNodeExtras.h | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 7cdecda107..319b5ff62d 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -36,24 +36,23 @@ inline BOOL ASInterfaceStateIncludesMeasureLayout(ASInterfaceState interfaceStat inline NSString * _Nonnull NSStringFromASInterfaceState(ASInterfaceState interfaceState) { - NSMutableString *result = [NSMutableString stringWithString:@"{ "]; + NSMutableArray *states = [NSMutableArray array]; if (interfaceState == ASInterfaceStateNone) { - [result appendString:@"No state"]; + [states addObject:@"No state"]; } if (ASInterfaceStateIncludesMeasureLayout(interfaceState)) { - [result appendString:@"MeasureLayout"]; + [states addObject:@"MeasureLayout"]; } if (ASInterfaceStateIncludesFetchData(interfaceState)) { - [result appendString:@" | FetchData"]; + [states addObject:@" | FetchData"]; } if (ASInterfaceStateIncludesDisplay(interfaceState)) { - [result appendString:@" | Display"]; + [states addObject:@" | Display"]; } if (ASInterfaceStateIncludesVisible(interfaceState)) { - [result appendString:@" | Visible"]; + [states addObject:@" | Visible"]; } - [result appendString:@" }"]; - return result; + return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]]; } NS_ASSUME_NONNULL_BEGIN From 05af98b57875cc5a8eede71ebbdf47561b227a22 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 19:58:36 -0800 Subject: [PATCH 12/15] Update documentation --- AsyncDisplayKit/ASDisplayNode.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index e462e0a76b..524b977fb7 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -465,9 +465,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)recursivelyFetchData; /** - * @abstract Marks the node as needing to call fetchData - * @discussion If the node is outside of the preload range, it is queued to call fetchData the next time it enters the range. - * Otherwise, fetchData is called immediately if the node is currently within the preload range. + * @abstract Triggers a call to fetchData for nodes in the preload range + * @discussion Recursively calls fetchData immediately if the node is currently within the preload range. */ - (void)setNeedsDataFetch; From 203952aca736920f31c9d9f87c1d45be31209300 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 27 Jan 2016 20:06:46 -0800 Subject: [PATCH 13/15] Update documentation --- AsyncDisplayKit/ASDisplayNode.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 524b977fb7..0d26f1bbb8 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -465,8 +465,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)recursivelyFetchData; /** - * @abstract Triggers a call to fetchData for nodes in the preload range - * @discussion Recursively calls fetchData immediately if the node is currently within the preload range. + * @abstract Triggers a recursive call to fetchData when the node has an interfaceState of ASInterfaceStateFetchData */ - (void)setNeedsDataFetch; From 90ac40020fd487c0054741a7bf0125872c306365 Mon Sep 17 00:00:00 2001 From: Samuel Hsiung Date: Wed, 27 Jan 2016 13:37:16 -0800 Subject: [PATCH 14/15] Use NSForegroundColorAttributeName for links in ASTextNodes by subclassing NSLayoutManager --- AsyncDisplayKit.xcodeproj/project.pbxproj | 10 ++++ AsyncDisplayKit/TextKit/ASLayoutManager.h | 13 +++++ AsyncDisplayKit/TextKit/ASLayoutManager.m | 41 +++++++++++++++ AsyncDisplayKit/TextKit/ASTextKitContext.mm | 4 +- AsyncDisplayKitTests/ASTextKitTests.mm | 55 ++++++++++++++++++--- 5 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 AsyncDisplayKit/TextKit/ASLayoutManager.h create mode 100644 AsyncDisplayKit/TextKit/ASLayoutManager.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index a7d2732172..6036150366 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -369,6 +369,9 @@ B13CA0F81C519EBA00E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B13CA0F61C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h */; }; B13CA1001C52004900E031AB /* ASCollectionNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = B13CA0FF1C52004900E031AB /* ASCollectionNode+Beta.h */; }; B13CA1011C52004900E031AB /* ASCollectionNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = B13CA0FF1C52004900E031AB /* ASCollectionNode+Beta.h */; }; + B30BF6521C5964B0004FCD53 /* ASLayoutManager.h in Headers */ = {isa = PBXBuildFile; fileRef = B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */; }; + B30BF6531C5964B0004FCD53 /* ASLayoutManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */; }; + B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */; }; B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3A19ABD43F004DAFF1 /* ASCellNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; B35061F51B010EFD0018CF92 /* ASCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; B35061F61B010EFD0018CF92 /* ASCollectionView.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A501A1139C100143C57 /* ASCollectionView.mm */; }; @@ -785,6 +788,8 @@ B0F880591BEAEC7500D17647 /* ASTableNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableNode.m; sourceTree = ""; }; B13CA0F61C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewLayoutFacilitatorProtocol.h; sourceTree = ""; }; B13CA0FF1C52004900E031AB /* ASCollectionNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionNode+Beta.h"; sourceTree = ""; }; + B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutManager.h; path = TextKit/ASLayoutManager.h; sourceTree = ""; }; + B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASLayoutManager.m; path = TextKit/ASLayoutManager.m; sourceTree = ""; }; B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B35061DD1B010EDF0018CF92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "../AsyncDisplayKit-iOS/Info.plist"; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; @@ -1173,6 +1178,8 @@ 257754661BED245B00737CA5 /* TextKit */ = { isa = PBXGroup; children = ( + B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */, + B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */, 257754B71BEE458D00737CA5 /* ASTextKitHelpers.mm */, 257754BB1BEE458E00737CA5 /* ASTextKitCoreTextAdditions.h */, 257754B81BEE458E00737CA5 /* ASTextKitCoreTextAdditions.m */, @@ -1409,6 +1416,7 @@ 055F1A3419ABD3E3004DAFF1 /* ASTableView.h in Headers */, 251B8EF71BBB3D690087C538 /* ASCollectionDataController.h in Headers */, 257754C11BEE458E00737CA5 /* ASTextKitHelpers.h in Headers */, + B30BF6521C5964B0004FCD53 /* ASLayoutManager.h in Headers */, 0574D5E219C110940097DC25 /* ASTableViewProtocols.h in Headers */, 058D0A51195D05CB00B7D73C /* ASTextNode.h in Headers */, 058D0A81195D05F900B7D73C /* ASThread.h in Headers */, @@ -1771,6 +1779,7 @@ 205F0E1E1B373A2C007741D0 /* ASCollectionViewLayoutController.mm in Sources */, 058D0A13195D050800B7D73C /* ASControlNode.m in Sources */, 464052211A3F83C40061C0BA /* ASDataController.mm in Sources */, + B30BF6531C5964B0004FCD53 /* ASLayoutManager.m in Sources */, 05A6D05B19D0EB64002DD95E /* ASDealloc2MainObject.m in Sources */, ACF6ED211B17843500DA7C62 /* ASDimension.mm in Sources */, 058D0A28195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm in Sources */, @@ -1881,6 +1890,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */, 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, 9B92C8861BC2EB7600EE46B2 /* ASCollectionViewFlowLayoutInspector.m in Sources */, 9B92C8851BC2EB6E00EE46B2 /* ASCollectionDataController.mm in Sources */, diff --git a/AsyncDisplayKit/TextKit/ASLayoutManager.h b/AsyncDisplayKit/TextKit/ASLayoutManager.h new file mode 100644 index 0000000000..ec70890c95 --- /dev/null +++ b/AsyncDisplayKit/TextKit/ASLayoutManager.h @@ -0,0 +1,13 @@ +/* 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 + +@interface ASLayoutManager : NSLayoutManager + +@end diff --git a/AsyncDisplayKit/TextKit/ASLayoutManager.m b/AsyncDisplayKit/TextKit/ASLayoutManager.m new file mode 100644 index 0000000000..b517403b0a --- /dev/null +++ b/AsyncDisplayKit/TextKit/ASLayoutManager.m @@ -0,0 +1,41 @@ +/* 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 "ASLayoutManager.h" + +@implementation ASLayoutManager + +- (void)showCGGlyphs:(const CGGlyph *)glyphs + positions:(const CGPoint *)positions + count:(NSUInteger)glyphCount + font:(UIFont *)font + matrix:(CGAffineTransform)textMatrix + attributes:(NSDictionary *)attributes + inContext:(CGContextRef)graphicsContext +{ + + // NSLayoutManager has a hard coded internal color for hyperlinks which ignores + // NSForegroundColorAttributeName. To get around this, we force the fill color + // in the current context to match NSForegroundColorAttributeName. + UIColor *foregroundColor = attributes[NSForegroundColorAttributeName]; + + if (foregroundColor) + { + CGContextSetFillColorWithColor(graphicsContext, foregroundColor.CGColor); + } + + [super showCGGlyphs:glyphs + positions:positions + count:glyphCount + font:font + matrix:textMatrix + attributes:attributes + inContext:graphicsContext]; +} + +@end diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.mm b/AsyncDisplayKit/TextKit/ASTextKitContext.mm index 2b682f9f26..a998bc2a2f 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.mm @@ -12,6 +12,8 @@ #import "ASTextKitContext.h" +#import "ASLayoutManager.h" + @implementation ASTextKitContext { // All TextKit operations (even non-mutative ones) must be executed serially. @@ -35,7 +37,7 @@ std::lock_guard l(__static_mutex); // Create the TextKit component stack with our default configuration. _textStorage = (attributedString ? [[NSTextStorage alloc] initWithAttributedString:attributedString] : [[NSTextStorage alloc] init]); - _layoutManager = layoutManagerFactory ? layoutManagerFactory() : [[NSLayoutManager alloc] init]; + _layoutManager = layoutManagerFactory ? layoutManagerFactory() : [[ASLayoutManager alloc] init]; _layoutManager.usesFontLeading = NO; [_textStorage addLayoutManager:_layoutManager]; _textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize]; diff --git a/AsyncDisplayKitTests/ASTextKitTests.mm b/AsyncDisplayKitTests/ASTextKitTests.mm index 90ef7443a1..96f3a5cbff 100644 --- a/AsyncDisplayKitTests/ASTextKitTests.mm +++ b/AsyncDisplayKitTests/ASTextKitTests.mm @@ -18,7 +18,9 @@ @end -static UITextView *UITextViewWithAttributes(const ASTextKitAttributes &attributes, const CGSize constrainedSize) +static UITextView *UITextViewWithAttributes(const ASTextKitAttributes &attributes, + const CGSize constrainedSize, + NSDictionary *linkTextAttributes) { UITextView *textView = [[UITextView alloc] initWithFrame:{ .size = constrainedSize }]; textView.backgroundColor = [UIColor clearColor]; @@ -28,12 +30,15 @@ static UITextView *UITextViewWithAttributes(const ASTextKitAttributes &attribute textView.textContainerInset = UIEdgeInsetsZero; textView.layoutManager.usesFontLeading = NO; textView.attributedText = attributes.attributedString; + textView.linkTextAttributes = linkTextAttributes; return textView; } -static UIImage *UITextViewImageWithAttributes(const ASTextKitAttributes &attributes, const CGSize constrainedSize) +static UIImage *UITextViewImageWithAttributes(const ASTextKitAttributes &attributes, + const CGSize constrainedSize, + NSDictionary *linkTextAttributes) { - UITextView *textView = UITextViewWithAttributes(attributes, constrainedSize); + UITextView *textView = UITextViewWithAttributes(attributes, constrainedSize, linkTextAttributes); UIGraphicsBeginImageContextWithOptions(constrainedSize, NO, 0); CGContextRef context = UIGraphicsGetCurrentContext(); @@ -68,10 +73,11 @@ static UIImage *ASTextKitImageWithAttributes(const ASTextKitAttributes &attribut return snapshot; } -static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize constrainedSize) +// linkTextAttributes are only applied to UITextView +static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize constrainedSize, NSDictionary *linkTextAttributes) { FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] init]; - UIImage *labelImage = UITextViewImageWithAttributes(attributes, constrainedSize); + UIImage *labelImage = UITextViewImageWithAttributes(attributes, constrainedSize, linkTextAttributes); UIImage *textKitImage = ASTextKitImageWithAttributes(attributes, constrainedSize); return [controller compareReferenceImage:labelImage toImage:textKitImage error:nil]; } @@ -83,7 +89,7 @@ static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize ASTextKitAttributes attributes { .attributedString = [[NSAttributedString alloc] initWithString:@"hello" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}] }; - XCTAssert(checkAttributes(attributes, { 100, 100 })); + XCTAssert(checkAttributes(attributes, { 100, 100 }, nil)); } - (void)testChangingAPropertyChangesHash @@ -130,7 +136,42 @@ static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize ASTextKitAttributes attributes { .attributedString = attrStr }; - XCTAssert(checkAttributes(attributes, { 100, 100 })); + XCTAssert(checkAttributes(attributes, { 100, 100 }, nil)); +} + +- (void)testLinkInTextUsesForegroundColor +{ + NSDictionary *linkTextAttributes = @{ NSForegroundColorAttributeName : [UIColor redColor], + // UITextView adds underline by default and we can't get rid of it + // so we have to choose a style and color and match it in the text kit version + // for this test + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle), + NSUnderlineColorAttributeName: [UIColor redColor], + }; + NSDictionary *textAttributes = @{NSFontAttributeName : [UIFont systemFontOfSize:12], + }; + + NSString *prefixString = @"click "; + NSString *linkString = @"this link"; + NSString *textString = [prefixString stringByAppendingString:linkString]; + + NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:textString attributes:textAttributes]; + NSURL *linkURL = [NSURL URLWithString:@"https://github.com/facebook/AsyncDisplayKit/issues/967"]; + NSRange selectedRange = (NSRange){prefixString.length, linkString.length}; + + [attrStr addAttribute:NSLinkAttributeName value:linkURL range:selectedRange]; + + for (NSString *attributeName in linkTextAttributes.keyEnumerator) { + [attrStr addAttribute:attributeName + value:linkTextAttributes[NSUnderlineStyleAttributeName] + range:selectedRange]; + } + + ASTextKitAttributes textKitattributes { + .attributedString = attrStr + }; + + XCTAssert(checkAttributes(textKitattributes, { 100, 100 }, linkTextAttributes)); } @end From 8a8843a32a5a7058fd452f931e5160020b194b17 Mon Sep 17 00:00:00 2001 From: Maxim Bunkov Date: Thu, 28 Jan 2016 10:27:58 +0500 Subject: [PATCH 15/15] Added additionalTruncationMessage Unit test --- AsyncDisplayKitTests/ASTextNodeTests.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AsyncDisplayKitTests/ASTextNodeTests.m b/AsyncDisplayKitTests/ASTextNodeTests.m index 7409e6d6de..a5c03ca0b9 100644 --- a/AsyncDisplayKitTests/ASTextNodeTests.m +++ b/AsyncDisplayKitTests/ASTextNodeTests.m @@ -101,6 +101,13 @@ static BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CGFloat delta) XCTAssertTrue([_textNode.truncationAttributedString isEqualToAttributedString:truncation], @"Failed to set truncation message"); } +- (void)testSettingAdditionalTruncationMessage +{ + NSAttributedString *additionalTruncationMessage = [[NSAttributedString alloc] initWithString:@"read more" attributes:nil]; + _textNode.additionalTruncationMessage = additionalTruncationMessage; + XCTAssertTrue([_textNode.additionalTruncationMessage isEqualToAttributedString:additionalTruncationMessage], @"Failed to set additionalTruncationMessage message"); +} + - (void)testCalculatedSizeIsGreaterThanOrEqualToConstrainedSize { for (NSInteger i = 10; i < 500; i += 50) {