From 90ac40020fd487c0054741a7bf0125872c306365 Mon Sep 17 00:00:00 2001 From: Samuel Hsiung Date: Wed, 27 Jan 2016 13:37:16 -0800 Subject: [PATCH] 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