diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 3c6668706..30f3f6e94 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -158,7 +158,7 @@ 697796611D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6977965E1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm */; }; 697B315A1CFE4B410049936F /* ASEditableTextNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 697B31591CFE4B410049936F /* ASEditableTextNodeTests.m */; }; 698371DB1E4379CD00437585 /* ASNodeController+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = 698371D91E4379CD00437585 /* ASNodeController+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 698371DC1E4379CD00437585 /* ASNodeController+Beta.m in Sources */ = {isa = PBXBuildFile; fileRef = 698371DA1E4379CD00437585 /* ASNodeController+Beta.m */; }; + 698371DC1E4379CD00437585 /* ASNodeController+Beta.mm in Sources */ = {isa = PBXBuildFile; fileRef = 698371DA1E4379CD00437585 /* ASNodeController+Beta.mm */; }; 698C8B621CAB49FC0052DC3F /* ASLayoutElementExtensibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 698C8B601CAB49FC0052DC3F /* ASLayoutElementExtensibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 698DFF441E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 698DFF431E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; 698DFF471E36B7E9002891F1 /* ASLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 698DFF461E36B7E9002891F1 /* ASLayoutSpecUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -228,6 +228,7 @@ ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */; }; ACF6ED621B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */; }; ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */; }; + AE440175210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = AE440174210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm */; }; AE6987C11DD04E1000B9E458 /* ASPagerNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.m */; }; AEEC47E41C21D3D200EC1693 /* ASVideoNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E31C21D3D200EC1693 /* ASVideoNodeTests.m */; }; B13CA0F81C519EBA00E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B13CA0F61C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -716,7 +717,7 @@ 6977965E1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayoutSpec+Subclasses.mm"; sourceTree = ""; }; 697B31591CFE4B410049936F /* ASEditableTextNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASEditableTextNodeTests.m; sourceTree = ""; }; 698371D91E4379CD00437585 /* ASNodeController+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASNodeController+Beta.h"; sourceTree = ""; }; - 698371DA1E4379CD00437585 /* ASNodeController+Beta.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASNodeController+Beta.m"; sourceTree = ""; }; + 698371DA1E4379CD00437585 /* ASNodeController+Beta.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASNodeController+Beta.mm"; sourceTree = ""; }; 698C8B601CAB49FC0052DC3F /* ASLayoutElementExtensibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementExtensibility.h; sourceTree = ""; }; 698DFF431E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutSpecUtilities.h; sourceTree = ""; }; 698DFF461E36B7E9002891F1 /* ASLayoutSpecUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutSpecUtilities.h; sourceTree = ""; }; @@ -814,6 +815,7 @@ ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASOverlayLayoutSpecSnapshotTests.mm; sourceTree = ""; }; ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRatioLayoutSpecSnapshotTests.mm; sourceTree = ""; }; ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + AE440174210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextKitFontSizeAdjusterTests.mm; sourceTree = ""; }; AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPagerNodeTests.m; sourceTree = ""; }; AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDefaultPlayButton.h; sourceTree = ""; }; AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDefaultPlayButton.m; sourceTree = ""; }; @@ -1197,7 +1199,7 @@ 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */, 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */, 698371D91E4379CD00437585 /* ASNodeController+Beta.h */, - 698371DA1E4379CD00437585 /* ASNodeController+Beta.m */, + 698371DA1E4379CD00437585 /* ASNodeController+Beta.mm */, DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */, DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */, 25E327541C16819500A2170C /* ASPagerNode.h */, @@ -1253,7 +1255,6 @@ 058D09C5195D04C000B7D73C /* Tests */ = { isa = PBXGroup; children = ( - CC35CEC520DD87280006448D /* ASCollectionsTests.m */, DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */, AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.m */, 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */, @@ -1264,6 +1265,7 @@ CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, ACF6ED531B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm */, CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, + CC35CEC520DD87280006448D /* ASCollectionsTests.m */, 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */, 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */, CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.m */, @@ -1314,8 +1316,10 @@ 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */, CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */, 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */, + AE440174210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm */, 254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */, 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */, + C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.m */, CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */, 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.m */, 058D0A36195D057000B7D73C /* ASTextNodeTests.m */, @@ -1325,7 +1329,6 @@ 4496D0721FA9EA6B001CC8D5 /* ASTraitCollectionTests.m */, CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.m */, AEEC47E31C21D3D200EC1693 /* ASVideoNodeTests.m */, - C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.m */, CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.m */, 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.m */, CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */, @@ -2265,6 +2268,7 @@ buildActionMask = 2147483647; files = ( CCEDDDD9200C518800FFCD0A /* ASConfigurationTests.m in Sources */, + AE440175210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm in Sources */, E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */, 4496D0731FA9EA6B001CC8D5 /* ASTraitCollectionTests.m in Sources */, 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m in Sources */, @@ -2374,7 +2378,7 @@ DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */, CCA282C11E9EAE010037E8B7 /* ASTip.m in Sources */, B350624C1B010EFD0018CF92 /* _ASPendingState.mm in Sources */, - 698371DC1E4379CD00437585 /* ASNodeController+Beta.m in Sources */, + 698371DC1E4379CD00437585 /* ASNodeController+Beta.mm in Sources */, CC6AA2DB1E9F03B900978E87 /* ASDisplayNode+Ancestry.m in Sources */, 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */, 254C6B861BF94F8A003EC431 /* ASTextKitContext.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f87a289..01ecb97ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,13 @@ - Reduced binary size by disabling exception support (which we don't use.) [Adlai Holler](https://github.com/Adlai-Holler) - Create and set delegate for clip corner layers within ASDisplayNode [Michael Schneider](https://github.com/maicki) [#1029](https://github.com/TextureGroup/Texture/pull/1029) - Improve locking situation in ASVideoPlayerNode [Michael Schneider](https://github.com/maicki) [#1042](https://github.com/TextureGroup/Texture/pull/1042) - +- Optimize ASDisplayNode -> ASNodeController reference by removing weak proxy and objc associated objects. [Adlai Holler](https://github.com/Adlai-Holler) +- Remove CA transaction signpost injection because it causes more transactions and is too chatty. [Adlai Holler](https://github.com/Adlai-Holler) +- Optimize display node accessibility by not creating attributed & non-attributed copies of hint, label, and value. [Adlai Holler](https://github.com/Adlai-Holler) +- Add an experimental feature that reuses CTFramesetter objects in ASTextNode2 to improve performance. [Adlai Holler](https://github.com/Adlai-Holler) +- Add NS_DESIGNATED_INITIALIZER to ASViewController initWithNode: [Michael Schneider](https://github.com/maicki) [#1054](https://github.com/TextureGroup/Texture/pull/1054) +- Optimize text stack by removing unneeded copying. [Adlai Holler](https://github.com/Adlai-Holler) +- Remove double scaling of lineHeightMultiple & paragraphSpacing attributes in ASTextKitFontSizeAdjuster. [Eric Jensen](https://github.com/ejensen) ## 2.7 - Fix pager node for interface coalescing. [Max Wang](https://github.com/wsdwsd0829) [#877](https://github.com/TextureGroup/Texture/pull/877) diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 71b591729..29c4375a4 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -21,6 +21,7 @@ "exp_network_image_queue", "exp_dealloc_queue_v2", "exp_collection_teardown", + "exp_framesetter_cache" ] } } diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 347d41c42..0fc41d40a 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -47,6 +47,7 @@ #import #import #import +#import #import #import #import @@ -876,6 +877,18 @@ - (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag _automaticallyRelayoutOnLayoutMarginsChanges = flag; } +- (void)__setNodeController:(ASNodeController *)controller +{ + // See docs for why we don't lock. + if (controller.shouldInvertStrongReference) { + _strongNodeController = controller; + _weakNodeController = nil; + } else { + _weakNodeController = controller; + _strongNodeController = nil; + } +} + #pragma mark - UIResponder #define HANDLE_NODE_RESPONDER_METHOD(__sel) \ diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index cc02d8a68..448deb87a 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -27,6 +27,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalNetworkImageQueue = 1 << 5, // exp_network_image_queue ASExperimentalDeallocQueue = 1 << 6, // exp_dealloc_queue_v2 ASExperimentalCollectionTeardown = 1 << 7, // exp_collection_teardown + ASExperimentalFramesetterCache = 1 << 8, // exp_framesetter_cache ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.m b/Source/ASExperimentalFeatures.m index dea872b36..5b33fe50b 100644 --- a/Source/ASExperimentalFeatures.m +++ b/Source/ASExperimentalFeatures.m @@ -23,7 +23,8 @@ @"exp_infer_layer_defaults", @"exp_network_image_queue", @"exp_dealloc_queue_v2", - @"exp_collection_teardown"])); + @"exp_collection_teardown", + @"exp_framesetter_cache"])); if (flags == ASExperimentalFeatureAll) { return allNames; diff --git a/Source/ASNodeController+Beta.m b/Source/ASNodeController+Beta.mm similarity index 59% rename from Source/ASNodeController+Beta.m rename to Source/ASNodeController+Beta.mm index 5f5fcddf6..6dcd1a079 100644 --- a/Source/ASNodeController+Beta.m +++ b/Source/ASNodeController+Beta.mm @@ -16,38 +16,18 @@ // #import +#import #import #import -#import #define _node (_shouldInvertStrongReference ? _weakNode : _strongNode) -@interface ASDisplayNode (ASNodeControllerOwnership) - -// This property exists for debugging purposes. Don't use __nodeController in production code. -@property (nonatomic, readonly) ASNodeController *__nodeController; - -// These setters are mutually exclusive. Setting one will clear the relationship of the other. -- (void)__setNodeControllerStrong:(ASNodeController *)nodeController; -- (void)__setNodeControllerWeak:(ASNodeController *)nodeController; - -@end - @implementation ASNodeController { ASDisplayNode *_strongNode; __weak ASDisplayNode *_weakNode; } -- (instancetype)init -{ - self = [super init]; - if (self) { - - } - return self; -} - - (void)loadNode { self.node = [[ASDisplayNode alloc] init]; @@ -66,15 +46,14 @@ - (void)setupReferencesWithNode:(ASDisplayNode *)node if (_shouldInvertStrongReference) { // The node should own the controller; weak reference from controller to node. _weakNode = node; - [node __setNodeControllerStrong:self]; _strongNode = nil; } else { // The controller should own the node; weak reference from node to controller. _strongNode = node; - [node __setNodeControllerWeak:self]; _weakNode = nil; } + [node __setNodeController:self]; [node addInterfaceStateDelegate:self]; } @@ -111,40 +90,10 @@ - (void)interfaceStateDidChange:(ASInterfaceState)newState @end -@implementation ASDisplayNode (ASNodeControllerOwnership) - -- (ASNodeController *)__nodeController -{ - ASNodeController *nodeController = nil; - id object = objc_getAssociatedObject(self, @selector(__nodeController)); - - if ([object isKindOfClass:[ASWeakProxy class]]) { - nodeController = (ASNodeController *)[(ASWeakProxy *)object target]; - } else { - nodeController = (ASNodeController *)object; - } - - return nodeController; -} - -- (void)__setNodeControllerStrong:(ASNodeController *)nodeController -{ - objc_setAssociatedObject(self, @selector(__nodeController), nodeController, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (void)__setNodeControllerWeak:(ASNodeController *)nodeController -{ - // Associated objects don't support weak references. Since assign can become a dangling pointer, use ASWeakProxy. - ASWeakProxy *nodeControllerProxy = [ASWeakProxy weakProxyWithTarget:nodeController]; - objc_setAssociatedObject(self, @selector(__nodeController), nodeControllerProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -@end - @implementation ASDisplayNode (ASNodeController) - (ASNodeController *)nodeController { - return self.__nodeController; + return _weakNodeController ?: _strongNodeController; } @end diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 6f5c2232a..2cc4fc6b3 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -254,29 +254,6 @@ - (void)drain @end -#if AS_KDEBUG_ENABLE -/** - * This is real, private CA API. Valid as of iOS 10. - */ -typedef enum { - kCATransactionPhasePreLayout, - kCATransactionPhasePreCommit, - kCATransactionPhasePostCommit, -} CATransactionPhase; - -@interface CATransaction (Private) -+ (void)addCommitHandler:(void(^)(void))block forPhase:(CATransactionPhase)phase; -+ (int)currentState; -@end -#endif - -#pragma mark - ASAbstractRunLoopQueue - -@interface ASAbstractRunLoopQueue (Private) -+ (void)load; -+ (void)registerCATransactionObservers; -@end - @implementation ASAbstractRunLoopQueue - (instancetype)init @@ -289,51 +266,6 @@ - (instancetype)init return self; } -#if AS_KDEBUG_ENABLE -+ (void)load -{ - [self registerCATransactionObservers]; -} - -+ (void)registerCATransactionObservers -{ - static BOOL privateCAMethodsExist; - static dispatch_block_t preLayoutHandler; - static dispatch_block_t preCommitHandler; - static dispatch_block_t postCommitHandler; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - privateCAMethodsExist = [CATransaction respondsToSelector:@selector(addCommitHandler:forPhase:)]; - privateCAMethodsExist &= [CATransaction respondsToSelector:@selector(currentState)]; - if (!privateCAMethodsExist) { - NSLog(@"Private CA methods are gone."); - } - preLayoutHandler = ^{ - ASSignpostStartCustom(ASSignpostCATransactionLayout, 0, [CATransaction currentState]); - }; - preCommitHandler = ^{ - int state = [CATransaction currentState]; - ASSignpostEndCustom(ASSignpostCATransactionLayout, 0, state, ASSignpostColorDefault); - ASSignpostStartCustom(ASSignpostCATransactionCommit, 0, state); - }; - postCommitHandler = ^{ - ASSignpostEndCustom(ASSignpostCATransactionCommit, 0, [CATransaction currentState], ASSignpostColorDefault); - // Can't add new observers inside an observer. rdar://problem/31253952 - dispatch_async(dispatch_get_main_queue(), ^{ - [self registerCATransactionObservers]; - }); - }; - }); - - if (privateCAMethodsExist) { - [CATransaction addCommitHandler:preLayoutHandler forPhase:kCATransactionPhasePreLayout]; - [CATransaction addCommitHandler:preCommitHandler forPhase:kCATransactionPhasePreCommit]; - [CATransaction addCommitHandler:postCommitHandler forPhase:kCATransactionPhasePostCommit]; - } -} - -#endif // AS_KDEBUG_ENABLE - @end #pragma mark - ASRunLoopQueue diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 5d51adbff..f71363d68 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -1346,36 +1346,29 @@ + (void)_registerAttributedText:(NSAttributedString *)str } #endif -+ (id)allocWithZone:(struct _NSZone *)zone +// All direct descendants of ASTextNode get their superclass replaced by ASTextNode2. ++ (void)initialize { - // If they're not experimenting, just forward. - if (!ASActivateExperimentalFeature(ASExperimentalTextNode)) { - return [super allocWithZone:zone]; - } - - // We are plain ASTextNode. Just swap in an ASTextNode2 instead. - if (self == [ASTextNode class]) { - return (ASTextNode *)[ASTextNode2 allocWithZone:zone]; - } - - // We are descended from ASTextNode. We need to change the superclass for the - // ASTextNode subclass to ASTextNode2. - // Walk up the class hierarchy until we find ASTextNode. - // Note: This may be called on multiple threads simultaneously. - Class s; - for (Class c = self; c != Nil && c != [ASTextNode class]; c = s) { - s = class_getSuperclass(c); - if (s == [ASTextNode class]) { + // Texture requires that node subclasses call [super initialize] + [super initialize]; + + if (class_getSuperclass(self) == [ASTextNode class] + && ASActivateExperimentalFeature(ASExperimentalTextNode)) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - // Direct descendent. Update superclass of c and end. - class_setSuperclass(c, [ASTextNode2 class]); + class_setSuperclass(self, [ASTextNode2 class]); #pragma clang diagnostic pop - break; - } } +} - return [super allocWithZone:zone]; +// For direct allocations of ASTextNode itself, we override allocWithZone: ++ (id)allocWithZone:(struct _NSZone *)zone +{ + if (ASActivateExperimentalFeature(ASExperimentalTextNode)) { + return (ASTextNode *)[ASTextNode2 allocWithZone:zone]; + } else { + return [super allocWithZone:zone]; + } } @end diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 7d3d8d7b3..3ab1d2f35 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -236,12 +236,17 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize ASLockScopeSelf(); - ASTextContainer *container = [_textContainer copy]; - NSAttributedString *attributedText = self.attributedText; - container.size = constrainedSize; + ASTextContainer *container; + if (!CGSizeEqualToSize(container.size, constrainedSize)) { + container = [_textContainer copy]; + container.size = constrainedSize; + [container makeImmutable]; + } else { + container = _textContainer; + } [self _ensureTruncationText]; - NSMutableAttributedString *mutableText = [attributedText mutableCopy]; + NSMutableAttributedString *mutableText = [_attributedText mutableCopy]; [self prepareAttributedString:mutableText]; ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:container text:mutableText]; @@ -365,9 +370,12 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { ASLockScopeSelf(); [self _ensureTruncationText]; + + // Unlike layout, here we must copy the container since drawing is asynchronous. ASTextContainer *copiedContainer = [_textContainer copy]; copiedContainer.size = self.bounds.size; - NSMutableAttributedString *mutableText = [self.attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + [copiedContainer makeImmutable]; + NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; [self prepareAttributedString:mutableText]; diff --git a/Source/ASViewController.h b/Source/ASViewController.h index b04137cc0..9f1e78dee 100644 --- a/Source/ASViewController.h +++ b/Source/ASViewController.h @@ -44,7 +44,7 @@ typedef ASTraitCollection * _Nonnull (^ASDisplayTraitsForTraitWindowSizeBlock)(C * * @see ASVisibilityDepth */ -- (instancetype)initWithNode:(DisplayNodeType)node; +- (instancetype)initWithNode:(DisplayNodeType)node NS_DESIGNATED_INITIALIZER; NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 05e266a7d..9ecfe2dc8 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -149,14 +149,13 @@ - (void)setLayoutDelegate:(id)layoutDelegate #pragma mark - Cell Layout -- (void)_allocateNodesFromElements:(NSArray *)elements completion:(ASDataControllerCompletionBlock)completionHandler +- (void)_allocateNodesFromElements:(NSArray *)elements { ASSERT_ON_EDITING_QUEUE; NSUInteger nodeCount = elements.count; __weak id weakDataSource = _dataSource; if (nodeCount == 0 || weakDataSource == nil) { - completionHandler(); return; } @@ -165,30 +164,23 @@ - (void)_allocateNodesFromElements:(NSArray *)elements co { as_activity_create_for_scope("Data controller batch"); - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + // TODO: Should we use USER_INITIATED here since the user is probably waiting? + dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); ASDispatchApply(nodeCount, queue, 0, ^(size_t i) { - __strong id strongDataSource = weakDataSource; - if (strongDataSource == nil) { + if (!weakDataSource) { return; } - // Allocate the node. - ASCollectionElement *context = elements[i]; - ASCellNode *node = context.node; - if (node == nil) { - ASDisplayNodeAssertNotNil(node, @"Node block created nil node; %@, %@", self, strongDataSource); - node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps. - } - + unowned ASCollectionElement *element = elements[i]; + unowned ASCellNode *node = element.node; // Layout the node if the size range is valid. - ASSizeRange sizeRange = context.constrainedSize; + ASSizeRange sizeRange = element.constrainedSize; if (ASSizeRangeHasSignificantArea(sizeRange)) { [self _layoutNode:node withConstrainedSize:sizeRange]; } }); } - completionHandler(); ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (weakDataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed)); } @@ -648,28 +640,9 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet __block __unused os_activity_scope_state_s preparationScope = {}; // unused if deployment target < iOS10 as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope); - dispatch_block_t completion = ^() { - [_mainSerialQueue performBlockOnMainThread:^{ - as_activity_scope_leave(&preparationScope); - // Step 4: Inform the delegate - [_delegate dataController:self updateWithChangeSet:changeSet updates:^{ - // Step 5: Deploy the new data as "completed" - // - // Note that since the backing collection view might be busy responding to user events (e.g scrolling), - // it will not consume the batch update blocks immediately. - // As a result, in a short intermidate time, the view will still be relying on the old data source state. - // Thus, we can't just swap the new map immediately before step 4, but until this update block is executed. - // (https://github.com/TextureGroup/Texture/issues/378) - self.visibleMap = newMap; - }]; - }]; - --_editingTransactionGroupCount; - }; - // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements if (canDelegate) { [layoutDelegateClass calculateLayoutWithContext:layoutContext]; - completion(); } else { let elementsToProcess = [[NSMutableArray alloc] init]; for (ASCollectionElement *element in newMap) { @@ -682,8 +655,24 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet [elementsToProcess addObject:element]; } } - [self _allocateNodesFromElements:elementsToProcess completion:completion]; + [self _allocateNodesFromElements:elementsToProcess]; } + + // Step 4: Inform the delegate on main thread + [_mainSerialQueue performBlockOnMainThread:^{ + as_activity_scope_leave(&preparationScope); + [_delegate dataController:self updateWithChangeSet:changeSet updates:^{ + // Step 5: Deploy the new data as "completed" + // + // Note that since the backing collection view might be busy responding to user events (e.g scrolling), + // it will not consume the batch update blocks immediately. + // As a result, in a short intermidate time, the view will still be relying on the old data source state. + // Thus, we can't just swap the new map immediately before step 4, but until this update block is executed. + // (https://github.com/TextureGroup/Texture/issues/378) + self.visibleMap = newMap; + }]; + }]; + --_editingTransactionGroupCount; }); if (_usesSynchronousDataLoading) { diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 3c7c2e887..927513754 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -34,6 +34,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol _ASDisplayLayerDelegate; @class _ASDisplayLayer; @class _ASPendingState; +@class ASNodeController; struct ASDisplayNodeFlags; BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); @@ -135,6 +136,9 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest ASDisplayNode * __weak _supernode; NSMutableArray *_subnodes; + ASNodeController *_strongNodeController; + __weak ASNodeController *_weakNodeController; + // Set this to nil whenever you modify _subnodes NSArray *_cachedSubnodes; @@ -273,6 +277,16 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest */ - (void)__setNeedsDisplay; +/** + * Setup the node -> controller reference. Strong or weak is based on + * the "shouldInvertStrongReference" property of the controller. + * + * Note: To prevent lock-ordering deadlocks, this method does not take the node's lock. + * In practice, changing the node controller of a node multiple times is not + * supported behavior. + */ +- (void)__setNodeController:(ASNodeController *)controller; + /** * Called whenever the node needs to layout its subnodes and, if it's already loaded, its subviews. Executes the layout pass for the node * diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.h b/Source/Private/TextExperiment/Component/ASTextLayout.h index 544d9f5f3..c78f4271f 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.h +++ b/Source/Private/TextExperiment/Component/ASTextLayout.h @@ -67,6 +67,9 @@ AS_EXTERN const CGSize ASTextContainerMaxSize; /// Creates a container with the specified path. @param path The path. + (instancetype)containerWithPath:(nullable UIBezierPath *)path NS_RETURNS_RETAINED; +/// Mark this immutable, so you get free copies going forward. +- (void)makeImmutable; + /// The constrained size. (if the size is larger than ASTextContainerMaxSize, it will be clipped) @property CGSize size; @@ -225,8 +228,6 @@ AS_EXTERN const CGSize ASTextContainerMaxSize; @property (nonatomic, readonly) NSAttributedString *text; ///< The text range in full text @property (nonatomic, readonly) NSRange range; -///< CTFrameSetter -@property (nonatomic, readonly) CTFramesetterRef frameSetter; ///< CTFrame @property (nonatomic, readonly) CTFrameRef frame; ///< Array of `ASTextLine`, no truncated diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.m b/Source/Private/TextExperiment/Component/ASTextLayout.m index d0aceee9a..4b3ba691f 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.m +++ b/Source/Private/TextExperiment/Component/ASTextLayout.m @@ -16,11 +16,16 @@ // #import + +#import +#import #import #import #import #import +#import + const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; typedef struct { @@ -130,26 +135,36 @@ - (instancetype)init { return self; } -- (id)copyWithZone:(NSZone *)zone { - ASTextContainer *one = [self.class new]; +- (id)copyForced:(BOOL)forceCopy +{ dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); + if (_readonly && !forceCopy) { + dispatch_semaphore_signal(_lock); + return self; + } + + ASTextContainer *one = [self.class new]; one->_size = _size; one->_insets = _insets; one->_path = _path; - one->_exclusionPaths = _exclusionPaths.copy; + one->_exclusionPaths = [_exclusionPaths copy]; one->_pathFillEvenOdd = _pathFillEvenOdd; one->_pathLineWidth = _pathLineWidth; one->_verticalForm = _verticalForm; one->_maximumNumberOfRows = _maximumNumberOfRows; one->_truncationType = _truncationType; - one->_truncationToken = _truncationToken.copy; + one->_truncationToken = [_truncationToken copy]; one->_linePositionModifier = [(NSObject *)_linePositionModifier copy]; dispatch_semaphore_signal(_lock); return one; } -- (id)mutableCopyWithZone:(nullable NSZone *)zone { - return [self copyWithZone:zone]; +- (id)copyWithZone:(NSZone *)zone { + return [self copyForced:NO]; +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + return [self copyForced:YES]; } - (void)encodeWithCoder:(NSCoder *)aCoder { @@ -185,18 +200,25 @@ - (id)initWithCoder:(NSCoder *)aDecoder { return self; } +- (void)makeImmutable +{ + dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); + _readonly = YES; + dispatch_semaphore_signal(_lock); +} + #define Getter(...) \ dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ __VA_ARGS__; \ dispatch_semaphore_signal(_lock); #define Setter(...) \ -if (_readonly) { \ -@throw [NSException exceptionWithName:NSInternalInconsistencyException \ -reason:@"Cannot change the property of the 'container' in 'ASTextLayout'." userInfo:nil]; \ -return; \ -} \ dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ +if (__builtin_expect(_readonly, NO)) { \ + ASDisplayNodeFailAssert(@"Attempt to modify immutable text container."); \ + dispatch_semaphore_signal(_lock); \ + return; \ +} \ __VA_ARGS__; \ dispatch_semaphore_signal(_lock); @@ -320,7 +342,6 @@ @interface ASTextLayout () @property (nonatomic) NSAttributedString *text; @property (nonatomic) NSRange range; -@property (nonatomic) CTFramesetterRef frameSetter; @property (nonatomic) CTFrameRef frame; @property (nonatomic) NSArray *lines; @property (nonatomic) ASTextLine *truncatedLine; @@ -404,11 +425,10 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (lineRowsIndex) free(lineRowsIndex); \ return nil; } - text = text.mutableCopy; - container = container.copy; + container = [container copy]; if (!text || !container) return nil; if (range.location + range.length > text.length) return nil; - container->_readonly = YES; + [container makeImmutable]; maximumNumberOfRows = container.maximumNumberOfRows; // It may use larger constraint size when create CTFrame with @@ -484,10 +504,71 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri frameAttrs[(id)kCTFrameProgressionAttributeName] = @(kCTFrameProgressionRightToLeft); } - // create CoreText objects - ctSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)text); + /* + * Framesetter cache. + * Framesetters can only be used by one thread at a time. + * Create a CFSet with no callbacks (raw pointers) to keep track of which + * framesetters are in use on other threads. If the one for our string is already in use, + * just create a new one. This should be pretty rare. + */ + static pthread_mutex_t busyFramesettersLock = PTHREAD_MUTEX_INITIALIZER; + static NSCache *framesetterCache; + static CFMutableSetRef busyFramesetters; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (ASActivateExperimentalFeature(ASExperimentalFramesetterCache)) { + framesetterCache = [[NSCache alloc] init]; + framesetterCache.name = @"org.TextureGroup.Texture.framesetterCache"; + busyFramesetters = CFSetCreateMutable(NULL, 0, NULL); + } + }); + + BOOL haveCached = NO, useCached = NO; + if (framesetterCache) { + // Check if there's one in the cache. + ctSetter = (__bridge_retained CTFramesetterRef)[framesetterCache objectForKey:text]; + + if (ctSetter) { + haveCached = YES; + + // Check-and-set busy on the cached one. + pthread_mutex_lock(&busyFramesettersLock); + BOOL busy = CFSetContainsValue(busyFramesetters, ctSetter); + if (!busy) { + CFSetAddValue(busyFramesetters, ctSetter); + useCached = YES; + } + pthread_mutex_unlock(&busyFramesettersLock); + + // Release if it was busy. + if (busy) { + CFRelease(ctSetter); + ctSetter = NULL; + } + } + } + + // Create a framesetter if needed. + if (!ctSetter) { + ctSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)text); + } + if (!ctSetter) FAIL_AND_RETURN ctFrame = CTFramesetterCreateFrame(ctSetter, ASTextCFRangeFromNSRange(range), cgPath, (CFDictionaryRef)frameAttrs); + + // Return to cache. + if (framesetterCache) { + if (useCached) { + // If reused: mark available. + pthread_mutex_lock(&busyFramesettersLock); + CFSetRemoveValue(busyFramesetters, ctSetter); + pthread_mutex_unlock(&busyFramesettersLock); + } else if (!haveCached) { + // If first framesetter, add to cache. + [framesetterCache setObject:(__bridge id)ctSetter forKey:text]; + } + } + if (!ctFrame) FAIL_AND_RETURN lines = [NSMutableArray new]; ctLines = CTFrameGetLines(ctFrame); @@ -857,8 +938,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (attachments.count == 0) { attachments = attachmentRanges = attachmentRects = nil; } - - layout.frameSetter = ctSetter; + layout.frame = ctFrame; layout.lines = lines; layout.truncatedLine = truncatedLine; @@ -903,14 +983,6 @@ + (NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString return layouts; } -- (void)setFrameSetter:(CTFramesetterRef)frameSetter { - if (_frameSetter != frameSetter) { - if (frameSetter) CFRetain(frameSetter); - if (_frameSetter) CFRelease(_frameSetter); - _frameSetter = frameSetter; - } -} - - (void)setFrame:(CTFrameRef)frame { if (_frame != frame) { if (frame) CFRetain(frame); @@ -920,7 +992,6 @@ - (void)setFrame:(CTFrameRef)frame { } - (void)dealloc { - if (_frameSetter) CFRelease(_frameSetter); if (_frame) CFRelease(_frame); if (_lineRowsIndex) free(_lineRowsIndex); if (_lineRowsEdge) free(_lineRowsEdge); diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index a87707bb6..3565aa835 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -607,92 +607,92 @@ - (void)setIsAccessibilityElement:(BOOL)newIsAccessibilityElement - (NSString *)accessibilityLabel { + if (_flags.setAccessibilityAttributedLabel) { + return accessibilityAttributedLabel.string; + } return accessibilityLabel; } - (void)setAccessibilityLabel:(NSString *)newAccessibilityLabel { - if (! ASObjectIsEqual(accessibilityLabel, newAccessibilityLabel)) { - _flags.setAccessibilityLabel = YES; - _flags.setAccessibilityAttributedLabel = YES; - accessibilityLabel = newAccessibilityLabel ? [newAccessibilityLabel copy] : nil; - accessibilityAttributedLabel = newAccessibilityLabel ? [[NSAttributedString alloc] initWithString:newAccessibilityLabel] : nil; - } + ASCompareAssignCopy(accessibilityLabel, newAccessibilityLabel); + _flags.setAccessibilityLabel = YES; + _flags.setAccessibilityAttributedLabel = NO; } - (NSAttributedString *)accessibilityAttributedLabel { + if (_flags.setAccessibilityLabel) { + return [[NSAttributedString alloc] initWithString:accessibilityLabel]; + } return accessibilityAttributedLabel; } - (void)setAccessibilityAttributedLabel:(NSAttributedString *)newAccessibilityAttributedLabel { - if (! ASObjectIsEqual(accessibilityAttributedLabel, newAccessibilityAttributedLabel)) { - _flags.setAccessibilityAttributedLabel = YES; - _flags.setAccessibilityLabel = YES; - accessibilityAttributedLabel = newAccessibilityAttributedLabel ? [newAccessibilityAttributedLabel copy] : nil; - accessibilityLabel = newAccessibilityAttributedLabel ? [newAccessibilityAttributedLabel.string copy] : nil; - } + ASCompareAssignCopy(accessibilityAttributedLabel, newAccessibilityAttributedLabel); + _flags.setAccessibilityAttributedLabel = YES; + _flags.setAccessibilityLabel = NO; } - (NSString *)accessibilityHint { + if (_flags.setAccessibilityAttributedHint) { + return accessibilityAttributedHint.string; + } return accessibilityHint; } - (void)setAccessibilityHint:(NSString *)newAccessibilityHint { - if (! ASObjectIsEqual(accessibilityHint, newAccessibilityHint)) { - _flags.setAccessibilityHint = YES; - _flags.setAccessibilityAttributedHint = YES; - accessibilityHint = newAccessibilityHint ? [newAccessibilityHint copy] : nil; - accessibilityAttributedHint = newAccessibilityHint ? [[NSAttributedString alloc] initWithString:newAccessibilityHint] : nil; - } + ASCompareAssignCopy(accessibilityHint, newAccessibilityHint); + _flags.setAccessibilityHint = YES; + _flags.setAccessibilityAttributedHint = NO; } - (NSAttributedString *)accessibilityAttributedHint { + if (_flags.setAccessibilityHint) { + return [[NSAttributedString alloc] initWithString:accessibilityHint]; + } return accessibilityAttributedHint; } - (void)setAccessibilityAttributedHint:(NSAttributedString *)newAccessibilityAttributedHint { - if (! ASObjectIsEqual(accessibilityAttributedHint, newAccessibilityAttributedHint)) { - _flags.setAccessibilityAttributedHint = YES; - _flags.setAccessibilityHint = YES; - accessibilityAttributedHint = newAccessibilityAttributedHint ? [newAccessibilityAttributedHint copy] : nil; - accessibilityHint = newAccessibilityAttributedHint ? [newAccessibilityAttributedHint.string copy] : nil; - } + ASCompareAssignCopy(accessibilityAttributedHint, newAccessibilityAttributedHint); + _flags.setAccessibilityAttributedHint = YES; + _flags.setAccessibilityHint = NO; } - (NSString *)accessibilityValue { + if (_flags.setAccessibilityAttributedValue) { + return accessibilityAttributedValue.string; + } return accessibilityValue; } - (void)setAccessibilityValue:(NSString *)newAccessibilityValue { - if (! ASObjectIsEqual(accessibilityValue, newAccessibilityValue)) { - _flags.setAccessibilityValue = YES; - _flags.setAccessibilityAttributedValue = YES; - accessibilityValue = newAccessibilityValue ? [newAccessibilityValue copy] : nil; - accessibilityAttributedValue = newAccessibilityValue ? [[NSAttributedString alloc] initWithString:newAccessibilityValue] : nil; - } + ASCompareAssignCopy(accessibilityValue, newAccessibilityValue); + _flags.setAccessibilityValue = YES; + _flags.setAccessibilityAttributedValue = NO; } - (NSAttributedString *)accessibilityAttributedValue { + if (_flags.setAccessibilityValue) { + return [[NSAttributedString alloc] initWithString:accessibilityValue]; + } return accessibilityAttributedValue; } - (void)setAccessibilityAttributedValue:(NSAttributedString *)newAccessibilityAttributedValue { - if (! ASObjectIsEqual(accessibilityAttributedValue, newAccessibilityAttributedValue)) { - _flags.setAccessibilityAttributedValue = YES; - _flags.setAccessibilityValue = YES; - accessibilityAttributedValue = newAccessibilityAttributedValue? [newAccessibilityAttributedValue copy] : nil; - accessibilityValue = newAccessibilityAttributedValue ? [newAccessibilityAttributedValue.string copy] : nil; - } + ASCompareAssignCopy(accessibilityAttributedValue, newAccessibilityAttributedValue); + _flags.setAccessibilityAttributedValue = YES; + _flags.setAccessibilityValue = NO; } - (UIAccessibilityTraits)accessibilityTraits @@ -1087,20 +1087,23 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr if (flags.setAccessibilityLabel) view.accessibilityLabel = accessibilityLabel; - if (AS_AT_LEAST_IOS11 && flags.setAccessibilityAttributedLabel) - [view setValue:accessibilityAttributedLabel forKey:@"accessibilityAttributedLabel"]; - if (flags.setAccessibilityHint) view.accessibilityHint = accessibilityHint; - if (AS_AT_LEAST_IOS11 && flags.setAccessibilityAttributedHint) - [view setValue:accessibilityAttributedHint forKey:@"accessibilityAttributedHint"]; - if (flags.setAccessibilityValue) view.accessibilityValue = accessibilityValue; - if (AS_AT_LEAST_IOS11 && flags.setAccessibilityAttributedValue) - [view setValue:accessibilityAttributedValue forKey:@"accessibilityAttributedValue"]; + if (AS_AVAILABLE_IOS(11)) { + if (flags.setAccessibilityAttributedLabel) { + view.accessibilityAttributedLabel = accessibilityAttributedLabel; + } + if (flags.setAccessibilityAttributedHint) { + view.accessibilityAttributedHint = accessibilityAttributedHint; + } + if (flags.setAccessibilityAttributedValue) { + view.accessibilityAttributedValue = accessibilityAttributedValue; + } + } if (flags.setAccessibilityTraits) view.accessibilityTraits = accessibilityTraits; diff --git a/Source/TextKit/ASTextKitFontSizeAdjuster.mm b/Source/TextKit/ASTextKitFontSizeAdjuster.mm index dce59597f..13d3e9296 100644 --- a/Source/TextKit/ASTextKitFontSizeAdjuster.mm +++ b/Source/TextKit/ASTextKitFontSizeAdjuster.mm @@ -87,8 +87,6 @@ + (void)adjustFontSizeForAttributeString:(NSMutableAttributedString *)attrString paragraphStyle.tailIndent = (paragraphStyle.tailIndent * scaleFactor); paragraphStyle.minimumLineHeight = (paragraphStyle.minimumLineHeight * scaleFactor); paragraphStyle.maximumLineHeight = (paragraphStyle.maximumLineHeight * scaleFactor); - paragraphStyle.lineHeightMultiple = (paragraphStyle.lineHeightMultiple * scaleFactor); - paragraphStyle.paragraphSpacing = (paragraphStyle.paragraphSpacing * scaleFactor); [attrString removeAttribute:NSParagraphStyleAttributeName range:range]; [attrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; @@ -214,7 +212,7 @@ - (CGFloat)scaleFactor if (longestWordFits == NO) { // we need to check the longest word to make sure it fits - longestWordFits = std::ceil((longestWordSize.width * adjustedScale) <= _constrainedSize.width); + longestWordFits = std::ceil((longestWordSize.width * adjustedScale) <= _constrainedSize.width); } // if the longest word fits, go ahead and check max line and height. If it didn't fit continue to the next scale factor diff --git a/Tests/ASDisplayLayerTests.m b/Tests/ASDisplayLayerTests.m index 5f20e928e..e462e0a4a 100644 --- a/Tests/ASDisplayLayerTests.m +++ b/Tests/ASDisplayLayerTests.m @@ -217,7 +217,7 @@ + (BOOL)respondsToSelector:(SEL)selector } // DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests! -+ (UIImage *)displayWithParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(asdisplaynode_iscancelled_block_t)sentinelBlock ++ (UIImage *)displayWithParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)sentinelBlock { UIImage *contents = bogusImage(); if (delegate->_displayLayerBlock != NULL) { @@ -228,7 +228,7 @@ + (UIImage *)displayWithParameters:(_ASDisplayLayerTestDelegate *)delegate isCan } // DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests! -+ (void)drawRect:(CGRect)bounds withParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(asdisplaynode_iscancelled_block_t)sentinelBlock isRasterizing:(BOOL)isRasterizing ++ (void)drawRect:(CGRect)bounds withParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)sentinelBlock isRasterizing:(BOOL)isRasterizing { __atomic_add_fetch(&delegate->_drawRectCount, 1, __ATOMIC_SEQ_CST); } diff --git a/Tests/ASTextKitFontSizeAdjusterTests.mm b/Tests/ASTextKitFontSizeAdjusterTests.mm new file mode 100644 index 000000000..937028d3f --- /dev/null +++ b/Tests/ASTextKitFontSizeAdjusterTests.mm @@ -0,0 +1,51 @@ +// +// ASTextKitFontSizeAdjusterTests.mm +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASFontSizeAdjusterTests : XCTestCase + +@end + +@implementation ASFontSizeAdjusterTests + +- (void)testFontSizeAdjusterAttributes +{ + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.lineHeightMultiple = 2.0; + paragraphStyle.lineSpacing = 2.0; + paragraphStyle.paragraphSpacing = 4.0; + paragraphStyle.firstLineHeadIndent = 6.0; + paragraphStyle.headIndent = 8.0; + paragraphStyle.tailIndent = 10.0; + paragraphStyle.minimumLineHeight = 12.0; + paragraphStyle.maximumLineHeight = 14.0; + + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet" + attributes:@{ NSParagraphStyleAttributeName: paragraphStyle }]; + + [ASTextKitFontSizeAdjuster adjustFontSizeForAttributeString:string withScaleFactor:0.5]; + + NSParagraphStyle *adjustedParagraphStyle = [string attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil]; + + XCTAssertEqual(adjustedParagraphStyle.lineHeightMultiple, 2.0); + XCTAssertEqual(adjustedParagraphStyle.lineSpacing, 1.0); + XCTAssertEqual(adjustedParagraphStyle.paragraphSpacing, 2.0); + XCTAssertEqual(adjustedParagraphStyle.firstLineHeadIndent, 3.0); + XCTAssertEqual(adjustedParagraphStyle.headIndent, 4.0); + XCTAssertEqual(adjustedParagraphStyle.tailIndent, 5.0); + XCTAssertEqual(adjustedParagraphStyle.minimumLineHeight, 6.0); + XCTAssertEqual(adjustedParagraphStyle.maximumLineHeight, 7.0); +} + +@end diff --git a/Tests/ASTextNodeSnapshotTests.m b/Tests/ASTextNodeSnapshotTests.m index c81f5241a..1d705f77b 100644 --- a/Tests/ASTextNodeSnapshotTests.m +++ b/Tests/ASTextNodeSnapshotTests.m @@ -137,4 +137,20 @@ - (void)DISABLED_testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextW ASSnapshotVerifyNode(textNode, nil); } +- (void)testFontPointSizeScaling +{ + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.lineHeightMultiple = 0.5; + paragraphStyle.lineSpacing = 2.0; + + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.style.maxSize = CGSizeMake(60, 80); + textNode.pointSizeScaleFactors = @[@0.5]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is an important thing" + attributes:@{ NSParagraphStyleAttributeName: paragraphStyle }]; + + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + @end diff --git a/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png b/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png new file mode 100644 index 000000000..111bd5004 Binary files /dev/null and b/Tests/ReferenceImages_64/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png b/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png new file mode 100644 index 000000000..111bd5004 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testFontPointSizeScaling@2x.png differ diff --git a/docs/_data/nav_docs.yml b/docs/_data/nav_docs.yml index 91e76d343..6b4b7b807 100755 --- a/docs/_data/nav_docs.yml +++ b/docs/_data/nav_docs.yml @@ -44,7 +44,6 @@ - title: Advanced Technologies items: - asvisibility - - asenvironment - asrunloopqueue - title: Node Containers items: diff --git a/docs/_docs/asrunloopqueue.md b/docs/_docs/asrunloopqueue.md index cd2e978c2..f40f532ed 100755 --- a/docs/_docs/asrunloopqueue.md +++ b/docs/_docs/asrunloopqueue.md @@ -2,7 +2,7 @@ title: ASRunLoopQueue layout: docs permalink: /docs/asrunloopqueue.html -prevPage: asenvironment.html +prevPage: asvisibility.html --- Even with main thread work, Texture is able to dramatically reduce its impact on the user experience by way of the rather amazing ASRunLoopQueue. diff --git a/docs/_docs/automatic-layout-basics.md b/docs/_docs/automatic-layout-basics.md index 8a830b1cd..128a1040d 100755 --- a/docs/_docs/automatic-layout-basics.md +++ b/docs/_docs/automatic-layout-basics.md @@ -6,7 +6,7 @@ prevPage: scroll-node.html nextPage: automatic-layout-containers.html --- -##Box Model Layout +## Box Model Layout ASLayout is an automatic, asynchronous, purely Objective-C box model layout feature. It is a simplified version of CSS flex box, loosely inspired by ComponentKit’s Layout. It is designed to make your layouts extensible and reusable. @@ -14,7 +14,7 @@ ASLayout is an automatic, asynchronous, purely Objective-C box model layout feat `` instances (all ASDisplayNodes and subclasses) do not have any size or position information. Instead, Texture calls the `layoutSpecThatFits:` method with a given size constraint and the component must return a structure describing both its size, and the position and sizes of its children. -##Terminology +## Terminology The terminology is a bit confusing, so here is a brief description of all of the Texture automatic layout players: @@ -30,7 +30,7 @@ Every ASLayoutSpec must act on at least one child. The ASLayoutSpec has the resp You don’t need to be aware of **`ASLayout`** except to know that it represents a computed immutable layout tree and is returned by objects conforming to the `` protocol. -##Layout for UIKit Components: +## Layout for UIKit Components: - for UIViews that are added directly, you will still need to manually lay it out in `didLoad:` - for UIViews that are added via `[ASDisplayNode initWithViewBlock:]` or its variants, you can then include it in `layoutSpecThatFits:` diff --git a/docs/_docs/automatic-layout-containers.md b/docs/_docs/automatic-layout-containers.md index 8bd56d205..b612b37c7 100755 --- a/docs/_docs/automatic-layout-containers.md +++ b/docs/_docs/automatic-layout-containers.md @@ -16,7 +16,7 @@ Both nodes and layoutSpecs conform to the `` protocol. Any `ASLay ### Single Child layoutSpecs - +
@@ -39,10 +39,10 @@ Both nodes and layoutSpecs conform to the `` protocol. Any `ASLay - + - + @@ -70,7 +70,7 @@ The following layoutSpecs may contain one or more children.
LayoutSpec Description
ASRatioLayoutSpec

Lays out a component at a fixed aspect ratio (which can be scaled).

This spec is great for objects that do not have an intrinisic size, such as ASNetworkImageNodes and ASVideoNodes.

Lays out a component at a fixed aspect ratio (which can be scaled).

This spec is great for objects that do not have an intrinisic size, such as ASNetworkImageNodes and ASVideoNodes.

ASRelativeLayoutSpecASRelativeLayoutSpec

Lays out a component and positions it within the layout bounds according to vertical and horizontal positional specifiers. Similar to the “9-part” image areas, a child can be positioned at any of the 4 corners, or the middle of any of the 4 edges, as well as the center.

-# ASLayoutable Properties +### ASLayoutable Properties The following properties can be applied to both nodes _and_ `layoutSpec`s; both conform to the `ASLayoutable` protocol. diff --git a/docs/_docs/layout2-layout-element-properties.md b/docs/_docs/layout2-layout-element-properties.md index 21ed56b11..18735a8cd 100755 --- a/docs/_docs/layout2-layout-element-properties.md +++ b/docs/_docs/layout2-layout-element-properties.md @@ -22,42 +22,42 @@ nextPage: layout2-api-sizing.html Description - `CGFloat .style.spacingBefore` + CGFloat .style.spacingBefore Additional space to place before this object in the stacking direction. - `CGFloat .style.spacingAfter` + CGFloat .style.spacingAfter Additional space to place after this object in the stacking direction. - `CGFloat .style.flexGrow` + CGFloat .style.flexGrow If the sum of childrens' stack dimensions is less than the minimum size, should this object grow? - `CGFloat .style.flexShrink` + CGFloat .style.flexShrink If the sum of childrens' stack dimensions is greater than the maximum size, should this object shrink? - `ASDimension .style.flexBasis` - Specifies the initial size for this object, in the stack dimension (horizontal or vertical), before the `flexGrow` / `flexShrink` properties are applied and the remaining space is distributed. + ASDimension .style.flexBasis + Specifies the initial size for this object, in the stack dimension (horizontal or vertical), before the flexGrow / flexShrink properties are applied and the remaining space is distributed. - `ASStackLayoutAlignSelf .style.alignSelf` + ASStackLayoutAlignSelf .style.alignSelf Orientation of the object along cross axis, overriding alignItems. Options include:
    -
  • `ASStackLayoutAlignSelfAuto`
  • -
  • `ASStackLayoutAlignSelfStart`
  • -
  • `ASStackLayoutAlignSelfEnd`
  • -
  • `ASStackLayoutAlignSelfCenter`
  • -
  • `ASStackLayoutAlignSelfStretch`
  • +
  • ASStackLayoutAlignSelfAuto
  • +
  • ASStackLayoutAlignSelfStart
  • +
  • ASStackLayoutAlignSelfEnd
  • +
  • ASStackLayoutAlignSelfCenter
  • +
  • ASStackLayoutAlignSelfStretch
- `CGFloat .style.ascender` + CGFloat .style.ascender Used for baseline alignment. The distance from the top of the object to its baseline. - `CGFloat .style.descender` + CGFloat .style.descender Used for baseline alignment. The distance from the baseline of the object to its bottom. @@ -75,8 +75,8 @@ nextPage: layout2-api-sizing.html Description - `CGPoint .style.layoutPosition` - The `CGPoint` position of this object within its `ASAbsoluteLayoutSpec` parent spec. + CGPoint .style.layoutPosition + The CGPoint position of this object within its ASAbsoluteLayoutSpec parent spec. @@ -92,55 +92,55 @@ nextPage: layout2-api-sizing.html Description - `ASDimension .style.width` - The `width` property specifies the width of the content area of an `ASLayoutElement`. The `minWidth` and `maxWidth` properties override `width`. Defaults to `ASDimensionAuto`. + ASDimension .style.width + The width property specifies the width of the content area of an ASLayoutElement. The minWidth and maxWidth properties override width. Defaults to ASDimensionAuto. - `ASDimension .style.height` - The `height` property specifies the height of the content area of an `ASLayoutElement`. The `minHeight` and `maxHeight` properties override `height`. Defaults to `ASDimensionAuto`. + ASDimension .style.height + The height property specifies the height of the content area of an ASLayoutElement. The minHeight and maxHeight properties override height. Defaults to ASDimensionAuto. - `ASDimension .style.minWidth` - The `minWidth` property is used to set the minimum width of a given element. It prevents the used value of the `width` property from becoming smaller than the value specified for `minWidth`. The value of `minWidth` overrides both `maxWidth` and `width`. Defaults to `ASDimensionAuto`. + ASDimension .style.minWidth + The minWidth property is used to set the minimum width of a given element. It prevents the used value of the width property from becoming smaller than the value specified for minWidth. The value of minWidth overrides both maxWidth and width. Defaults to ASDimensionAuto. - `ASDimension .style.maxWidth` - The `maxWidth` property is used to set the maximum width of a given element. It prevents the used value of the `width` property from becoming larger than the value specified for `maxWidth`. The value of `maxWidth` overrides `width`, but `minWidth` overrides `maxWidth`. Defaults to `ASDimensionAuto`. + ASDimension .style.maxWidth + The maxWidth property is used to set the maximum width of a given element. It prevents the used value of the width property from becoming larger than the value specified for maxWidth. The value of maxWidth overrides width, but minWidth overrides maxWidth. Defaults to ASDimensionAuto. - `ASDimension .style.minHeight` - The `minHeight` property is used to set the minimum height of a given element. It prevents the used value of the `height` property from becoming smaller than the value specified for `minHeight`. The value of `minHeight` overrides both `maxHeight` and `height`. Defaults to `ASDimensionAuto`. + ASDimension .style.minHeight + The minHeight property is used to set the minimum height of a given element. It prevents the used value of the height property from becoming smaller than the value specified for minHeight. The value of minHeight overrides both maxHeight and height. Defaults to ASDimensionAuto. - `ASDimension .style.maxHeight` - The `maxHeight` property is used to set the maximum height of a given element. It prevents the used value of the `height` property from becoming larger than the value specified for `maxHeight`. The value of `maxHeight` overrides `height`, but `minHeight` overrides `maxHeight`. Defaults to `ASDimensionAuto` + ASDimension .style.maxHeight + The maxHeight property is used to set the maximum height of a given element. It prevents the used value of the height property from becoming larger than the value specified for maxHeight. The value of maxHeight overrides height, but minHeight overrides maxHeight. Defaults to ASDimensionAuto - `CGSize .style.preferredSize` + CGSize .style.preferredSize

Provides a suggested size for a layout element. If the optional minSize or maxSize are provided, and the preferredSize exceeds these, the minSize or maxSize will be enforced. If this optional value is not provided, the layout element’s size will default to it’s intrinsic content size provided calculateSizeThatFits:

This method is optional, but one of either preferredSize or preferredLayoutSize is required for nodes that either have no intrinsic content size or should be laid out at a different size than its intrinsic content size. For example, this property could be set on an ASImageNode to display at a size different from the underlying image size.

Warning: calling the getter when the size's width or height are relative will cause an assert.

- `CGSize .style.minSize` + CGSize .style.minSize

An optional property that provides a minimum size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s minimum size is smaller than its child’s minimum size, the child’s minimum size will be enforced and its size will extend out of the layout spec’s.

For example, if you set a preferred relative width of 50% and a minimum width of 200 points on an element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, since 160 pts is lower than the minimum width of 200 pts, the minimum width would be used.

- `CGSize .style.maxSize` + CGSize .style.maxSize

An optional property that provides a maximum size bound for a layout element. If provided, this restriction will always be enforced. If a child layout element’s maximum size is smaller than its parent, the child’s maximum size will be enforced and its size will extend out of the layout spec’s.

For example, if you set a preferred relative width of 50% and a maximum width of 120 points on an element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, since 160 pts is higher than the maximum width of 120 pts, the maximum width would be used.

- `ASLayoutSize .style.preferredLayoutSize` - Provides a suggested RELATIVE size for a layout element. An ASLayoutSize uses percentages rather than points to specify layout. E.g. width should be 50% of the parent’s width. If the optional minLayoutSize or maxLayoutSize are provided, and the preferredLayoutSize exceeds these, the minLayoutSize or maxLayoutSize will be enforced. If this optional value is not provided, the layout element’s size will default to its intrinsic content size provided `calculateSizeThatFits:` + ASLayoutSize .style.preferredLayoutSize + Provides a suggested RELATIVE size for a layout element. An ASLayoutSize uses percentages rather than points to specify layout. E.g. width should be 50% of the parent’s width. If the optional minLayoutSize or maxLayoutSize are provided, and the preferredLayoutSize exceeds these, the minLayoutSize or maxLayoutSize will be enforced. If this optional value is not provided, the layout element’s size will default to its intrinsic content size provided calculateSizeThatFits: - `ASLayoutSize .style.minLayoutSize` + ASLayoutSize .style.minLayoutSize An optional property that provides a minimum RELATIVE size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s minimum relative size is smaller than its child’s minimum relative size, the child’s minimum relative size will be enforced and its size will extend out of the layout spec’s. - `ASLayoutSize .style.maxLayoutSize` + ASLayoutSize .style.maxLayoutSize An optional property that provides a maximum RELATIVE size bound for a layout element. If provided, this restriction will always be enforced. If a parent layout element’s maximum relative size is smaller than its child’s maximum relative size, the child’s maximum relative size will be enforced and its size will extend out of the layout spec’s. diff --git a/docs/_docs/team.md b/docs/_docs/team.md index 5f6726589..e3a964391 100755 --- a/docs/_docs/team.md +++ b/docs/_docs/team.md @@ -13,7 +13,7 @@ permalink: /docs/team.html -

Michael Schneider (@maicki) is especially passionate about API design and recently led the re-architecture of the layout API for the 2.0 release. As our resident layout expert, Michael volunteers much of his own time to help developers on Texture's public slack channel. Previous, Michael worked on Pocket for iOS, Mac and Chrome and the Instapaper Mac app.

+

Michael Schneider (@maicki) is especially passionate about API design and recently led the re-architecture of the layout API for the 2.0 release. As our resident layout expert, Michael volunteers much of his own time to help developers on Texture's public slack channel. Before he joined Pinterest, Michael worked on Pocket for iOS, Mac and Chrome and Read Later an Instapaper and Pocket Mac app.

diff --git a/docs/showcase.md b/docs/showcase.md index 1eaddbf0d..fed05f3d0 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -256,6 +256,12 @@ permalink: /showcase.html
Apollo for Reddit + + + +
+ Wishpoke +