diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index b56a693021..a1e91b0fd1 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -371,6 +371,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 */; }; @@ -789,6 +792,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 = ""; }; @@ -1179,6 +1184,8 @@ 257754661BED245B00737CA5 /* TextKit */ = { isa = PBXGroup; children = ( + B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */, + B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */, 257754B71BEE458D00737CA5 /* ASTextKitHelpers.mm */, 257754BB1BEE458E00737CA5 /* ASTextKitCoreTextAdditions.h */, 257754B81BEE458E00737CA5 /* ASTextKitCoreTextAdditions.m */, @@ -1416,6 +1423,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 */, @@ -1778,6 +1786,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 */, @@ -1889,6 +1898,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 */, @@ -2242,6 +2252,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)", @@ -2276,6 +2287,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; 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..3d5c3d9288 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..e8ae908593 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(); @@ -851,6 +856,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } ASPerformBlockWithoutAnimation(!animated, ^{ + [_layoutFacilitator collectionViewWillPerformBatchUpdates]; [super performBatchUpdates:^{ for (dispatch_block_t block in _batchUpdateBlocks) { block(); @@ -865,16 +871,17 @@ 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 collectionViewWillEditCellsAtIndexPaths:indexPaths batched:YES]; [_batchUpdateBlocks addObject:^{ [super insertItemsAtIndexPaths:indexPaths]; }]; } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; [UIView performWithoutAnimation:^{ [super insertItemsAtIndexPaths:indexPaths]; }]; @@ -884,16 +891,17 @@ 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 collectionViewWillEditCellsAtIndexPaths:indexPaths batched:YES]; [_batchUpdateBlocks addObject:^{ [super deleteItemsAtIndexPaths:indexPaths]; }]; } else { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; [UIView performWithoutAnimation:^{ [super deleteItemsAtIndexPaths:indexPaths]; }]; @@ -903,16 +911,17 @@ 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 collectionViewWillEditSectionsAtIndexSet:indexSet batched:YES]; [_batchUpdateBlocks addObject:^{ [super insertSections:indexSet]; }]; } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:NO]; [UIView performWithoutAnimation:^{ [super insertSections:indexSet]; }]; @@ -922,16 +931,17 @@ 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 collectionViewWillEditSectionsAtIndexSet:indexSet batched:YES]; [_batchUpdateBlocks addObject:^{ [super deleteSections:indexSet]; }]; } else { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:NO]; [UIView performWithoutAnimation:^{ [super deleteSections:indexSet]; }]; diff --git a/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h b/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h index 719c98419e..514df97448 100644 --- a/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h +++ b/AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h @@ -17,13 +17,28 @@ /** * 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 + * + * @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: when isBatched, used in combination with -collectionViewWillPerformBatchUpdates */ -- (void)collectionViewEditingSectionsAtIndexSet:(NSIndexSet *)indexes; +- (void)collectionViewWillEditSectionsAtIndexSet:(NSIndexSet *)indexes batched:(BOOL)batched; + +/** + * Informs the delegate that the collectionView is about to call performBatchUpdates + */ +- (void)collectionViewWillPerformBatchUpdates; @end diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index ab3b40b524..0d26f1bbb8 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,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)recursivelyFetchData; +/** + * @abstract Triggers a recursive call to fetchData when the node has an interfaceState of ASInterfaceStateFetchData + */ +- (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..bdde67d6ab 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1727,6 +1727,13 @@ static BOOL ShouldUseNewRenderingRange = YES; // subclass override } +- (void)setNeedsDataFetch +{ + if (ASInterfaceStateIncludesFetchData(_interfaceState)) { + [self recursivelyFetchData]; + } +} + // TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or enterInterfaceState: - (void)recursivelyFetchData { diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 0ab9957cc3..319b5ff62d 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -29,6 +29,32 @@ inline BOOL ASInterfaceStateIncludesFetchData(ASInterfaceState interfaceState) return ((interfaceState & ASInterfaceStateFetchData) == ASInterfaceStateFetchData); } +inline BOOL ASInterfaceStateIncludesMeasureLayout(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateMeasureLayout) == ASInterfaceStateMeasureLayout); +} + +inline NSString * _Nonnull NSStringFromASInterfaceState(ASInterfaceState interfaceState) +{ + NSMutableArray *states = [NSMutableArray array]; + if (interfaceState == ASInterfaceStateNone) { + [states addObject:@"No state"]; + } + if (ASInterfaceStateIncludesMeasureLayout(interfaceState)) { + [states addObject:@"MeasureLayout"]; + } + if (ASInterfaceStateIncludesFetchData(interfaceState)) { + [states addObject:@" | FetchData"]; + } + if (ASInterfaceStateIncludesDisplay(interfaceState)) { + [states addObject:@" | Display"]; + } + if (ASInterfaceStateIncludesVisible(interfaceState)) { + [states addObject:@" | Visible"]; + } + return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]]; +} + NS_ASSUME_NONNULL_BEGIN ASDISPLAYNODE_EXTERN_C_BEGIN diff --git a/AsyncDisplayKit/Details/ASCollectionInternal.h b/AsyncDisplayKit/Details/ASCollectionInternal.h index a0aff1e573..55e99d6a36 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, weak, readwrite) ASCollectionNode *collectionNode; +@property (nonatomic, strong, readonly) ASDataController *dataController; @property (nonatomic, strong, readonly) ASRangeController *rangeController; @end diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index f1ffb377e9..4bac260403 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -100,7 +100,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) } _flags; ASDisplayNodeExtraIvars _extra; - + #if TIME_DISPLAYNODE_OPS @public NSTimeInterval _debugTimeToCreateView; 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/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; } } } 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 diff --git a/AsyncDisplayKitTests/ASTextKitTests.mm b/AsyncDisplayKitTests/ASTextKitTests.mm index 426116d156..ef16e9c03d 100644 --- a/AsyncDisplayKitTests/ASTextKitTests.mm +++ b/AsyncDisplayKitTests/ASTextKitTests.mm @@ -20,7 +20,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]; @@ -30,12 +32,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(); @@ -70,10 +75,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]; } @@ -85,7 +91,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 @@ -132,7 +138,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)); } - (void)testRectsForRangeBeyondTruncationSizeReturnsNonZeroNumberOfRects 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) {