diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index d9f55893b3..6ad191c842 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -33,6 +33,9 @@ 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */; }; 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */; }; + 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1A6C000E1FAB4E2100D05926 /* ASCornerLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */; }; + 1A6C00111FAB4EDD00D05926 /* ASCornerLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */; }; 242995D31B29743C00090100 /* ASBasicImageDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */; }; 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */; }; 254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */; }; @@ -96,6 +99,7 @@ 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.m */; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */; }; + 4E9127691F64157600499623 /* ASRunLoopQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */; }; 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E111B371BD7007741D0 /* ASScrollDirection.m */; }; 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E171B37339C007741D0 /* ASAbstractLayoutController.h */; }; 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E181B37339C007741D0 /* ASAbstractLayoutController.mm */; }; @@ -106,7 +110,6 @@ 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */; }; 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */ = {isa = PBXBuildFile; fileRef = AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */; }; 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 683489281D70DE3400327501 /* ASDisplayNode+Deprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */; }; 68355B3E1CB57A60001D4E68 /* ASPINRemoteImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */; }; 68355B401CB57A69001D4E68 /* ASImageContainerProtocolCategories.m in Sources */ = {isa = PBXBuildFile; fileRef = 68355B381CB57A5A001D4E68 /* ASImageContainerProtocolCategories.m */; }; @@ -129,8 +132,6 @@ 6907C25A1DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */; }; 690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */; }; 690C35641E055C7B00069B91 /* ASDimensionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 690C35631E055C7B00069B91 /* ASDimensionInternal.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 690C35671E0567C600069B91 /* ASDimensionDeprecated.mm in Sources */ = {isa = PBXBuildFile; fileRef = 690C35651E0567C600069B91 /* ASDimensionDeprecated.mm */; }; - 690C356B1E05680300069B91 /* ASDimensionDeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = 690C356A1E05680300069B91 /* ASDimensionDeprecated.h */; settings = {ATTRIBUTES = (Public, ); }; }; 690ED58E1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 690ED58D1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 690ED5961E36D118000627C0 /* ASControlNode+tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 690ED5921E36D118000627C0 /* ASControlNode+tvOS.h */; settings = {ATTRIBUTES = (Private, ); }; }; 690ED5981E36D118000627C0 /* ASControlNode+tvOS.m in Sources */ = {isa = PBXBuildFile; fileRef = 690ED5931E36D118000627C0 /* ASControlNode+tvOS.m */; }; @@ -309,10 +310,11 @@ B350625C1B010F070018CF92 /* ASLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3B1A15563400B4EBED /* ASLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */; }; + BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */; }; C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F880581BEAEC7500D17647 /* ASTableNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A0A1E60BEB400626263 /* ASDisplayNode+Convenience.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */; }; - CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */; }; CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */ = {isa = PBXBuildFile; fileRef = CC034A111E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC034A141E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = CC034A121E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.m */; }; CC051F1F1D7A286A006434CB /* ASCALayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC051F1E1D7A286A006434CB /* ASCALayerTests.m */; }; @@ -402,6 +404,9 @@ CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */; }; CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */; }; CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */; }; + CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */; }; + CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */; }; + CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */; }; CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -425,6 +430,9 @@ DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */; }; + E52AC9BA1FEA90EB00AA4040 /* ASRectMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = E52AC9B81FEA90EB00AA4040 /* ASRectMap.mm */; }; + E52AC9BB1FEA90EB00AA4040 /* ASRectMap.h in Headers */ = {isa = PBXBuildFile; fileRef = E52AC9B91FEA90EB00AA4040 /* ASRectMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E52AC9C01FEA916C00AA4040 /* ASRectMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E52AC9BE1FEA915D00AA4040 /* ASRectMapTests.m */; }; E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; @@ -440,6 +448,7 @@ E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */; }; E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.m in Sources */ = {isa = PBXBuildFile; fileRef = E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */; }; E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E586F96C1F9F9E2900ECE00E /* ASScrollNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.m */; }; E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */; }; E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -447,8 +456,6 @@ E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E491E941DA5004CFC59 /* ASCollectionLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */; }; - E5ABAC7B1E8564EE007AC15C /* ASRectTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E5ABAC791E8564EE007AC15C /* ASRectTable.h */; }; - E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */; }; E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */; }; E5B225281F1790D6001E1431 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B225271F1790B5001E1431 /* ASHashing.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -572,6 +579,9 @@ 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionNode.h; sourceTree = ""; }; 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionNode.mm; sourceTree = ""; }; 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = ""; }; + 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCornerLayoutSpec.h; sourceTree = ""; }; + 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCornerLayoutSpec.mm; sourceTree = ""; }; + 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCornerLayoutSpecSnapshotTests.mm; sourceTree = ""; }; 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewLayout+ASConvenience.h"; sourceTree = ""; }; 205F0E0E1B371875007741D0 /* UICollectionViewLayout+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionViewLayout+ASConvenience.m"; sourceTree = ""; }; 205F0E111B371BD7007741D0 /* ASScrollDirection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollDirection.m; sourceTree = ""; }; @@ -627,7 +637,7 @@ 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableLayoutController.h; sourceTree = ""; }; 4640521C1A3F83C40061C0BA /* ASTableLayoutController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableLayoutController.m; sourceTree = ""; }; 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; - 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Deprecated.h"; sourceTree = ""; }; + 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRunLoopQueueTests.m; sourceTree = ""; }; 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = ""; }; 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPINRemoteImageDownloader.m; sourceTree = ""; }; 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageContainerProtocolCategories.h; sourceTree = ""; }; @@ -652,8 +662,6 @@ 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASObjectDescriptionHelpers.m; sourceTree = ""; }; 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDimensionInternal.mm; sourceTree = ""; }; 690C35631E055C7B00069B91 /* ASDimensionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDimensionInternal.h; sourceTree = ""; }; - 690C35651E0567C600069B91 /* ASDimensionDeprecated.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDimensionDeprecated.mm; sourceTree = ""; }; - 690C356A1E05680300069B91 /* ASDimensionDeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDimensionDeprecated.h; sourceTree = ""; }; 690ED58D1E36BCA6000627C0 /* ASLayoutElementStylePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementStylePrivate.h; sourceTree = ""; }; 690ED5921E36D118000627C0 /* ASControlNode+tvOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASControlNode+tvOS.h"; sourceTree = ""; }; 690ED5931E36D118000627C0 /* ASControlNode+tvOS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASControlNode+tvOS.m"; sourceTree = ""; }; @@ -787,10 +795,11 @@ 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; }; + BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASNavigationControllerTests.m; sourceTree = ""; }; + BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASTabBarControllerTests.m; sourceTree = ""; }; BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Convenience.h"; sourceTree = ""; }; CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASDisplayNode+Convenience.m"; sourceTree = ""; }; - CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectTableTests.m; sourceTree = ""; }; CC034A111E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit+IGListKitMethods.h"; sourceTree = ""; }; CC034A121E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "AsyncDisplayKit+IGListKitMethods.m"; sourceTree = ""; }; CC051F1E1D7A286A006434CB /* ASCALayerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCALayerTests.m; sourceTree = ""; }; @@ -892,6 +901,12 @@ CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = ""; }; CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSupplementaryNodeSource.h; sourceTree = ""; }; CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIntegerMapTests.m; sourceTree = ""; }; + CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutEngineTests.mm; sourceTree = ""; }; + CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTestNode.h; sourceTree = ""; }; + CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTestNode.mm; sourceTree = ""; }; + CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = debugbreak.h; sourceTree = ""; }; + CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTLayoutFixture.h; sourceTree = ""; }; + CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTLayoutFixture.mm; sourceTree = ""; }; D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = ""; }; @@ -915,6 +930,9 @@ E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLayoutFlatteningTests.m; sourceTree = ""; }; E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; + E52AC9B81FEA90EB00AA4040 /* ASRectMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRectMap.mm; sourceTree = ""; }; + E52AC9B91FEA90EB00AA4040 /* ASRectMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRectMap.h; sourceTree = ""; }; + E52AC9BE1FEA915D00AA4040 /* ASRectMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectMapTests.m; sourceTree = ""; }; E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASPagerNode+Beta.h"; sourceTree = ""; }; E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPageTable.m; sourceTree = ""; }; @@ -930,6 +948,7 @@ E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutCache.mm; sourceTree = ""; }; E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutDefines.m; sourceTree = ""; }; E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDefines.h; sourceTree = ""; }; + E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASScrollNodeTests.m; sourceTree = ""; }; E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionFlowLayoutDelegate.h; sourceTree = ""; }; E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionFlowLayoutDelegate.m; sourceTree = ""; }; E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutContext.h; sourceTree = ""; }; @@ -937,8 +956,6 @@ E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDelegate.h; sourceTree = ""; }; E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayout.h; sourceTree = ""; }; E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayout.mm; sourceTree = ""; }; - E5ABAC791E8564EE007AC15C /* ASRectTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRectTable.h; sourceTree = ""; }; - E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectTable.m; sourceTree = ""; }; E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASElementMap.h; sourceTree = ""; }; E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASElementMap.m; sourceTree = ""; }; E5B225261F1790B5001E1431 /* ASHashing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; @@ -1081,7 +1098,6 @@ 058D09D9195D050800B7D73C /* ASDisplayNode.mm */, 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */, 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */, - 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */, 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */, CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */, CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */, @@ -1162,10 +1178,16 @@ children = ( CC583ABF1EF9BAB400134156 /* Common */, CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, - CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, + BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */, + BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */, + CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */, + CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */, + CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */, + CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */, + CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */, CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */, CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */, CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */, @@ -1173,6 +1195,7 @@ CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.m */, CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.m */, CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */, + E52AC9BE1FEA915D00AA4040 /* ASRectMapTests.m */, 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.m */, DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */, DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */, @@ -1186,6 +1209,7 @@ 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */, ACF6ED551B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm */, ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */, + 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */, 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */, ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */, ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */, @@ -1211,6 +1235,7 @@ E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */, 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, + E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */, CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */, 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */, @@ -1226,6 +1251,7 @@ 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.m */, 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */, 699B83501E3C1BA500433FA4 /* ASLayoutSpecTests.m */, + 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */, ); path = Tests; sourceTree = ""; @@ -1357,8 +1383,8 @@ CCA282B31E9EA7310037E8B7 /* ASTipsController.m */, CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */, CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */, - E5ABAC791E8564EE007AC15C /* ASRectTable.h */, - E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */, + E52AC9B91FEA90EB00AA4040 /* ASRectMap.h */, + E52AC9B81FEA90EB00AA4040 /* ASRectMap.mm */, CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */, CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, 6947B0BB1E36B4E30007C478 /* Layout */, @@ -1522,10 +1548,10 @@ ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */, ACF6ED031B17843500DA7C62 /* ASCenterLayoutSpec.h */, ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */, + 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */, + 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */, ACF6ED071B17843500DA7C62 /* ASDimension.h */, ACF6ED081B17843500DA7C62 /* ASDimension.mm */, - 690C356A1E05680300069B91 /* ASDimensionDeprecated.h */, - 690C35651E0567C600069B91 /* ASDimensionDeprecated.mm */, 690C35631E055C7B00069B91 /* ASDimensionInternal.h */, 690C35601E055C5D00069B91 /* ASDimensionInternal.mm */, ACF6ED091B17843500DA7C62 /* ASInsetLayoutSpec.h */, @@ -1561,6 +1587,7 @@ CC583ABF1EF9BAB400134156 /* Common */ = { isa = PBXGroup; children = ( + CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */, CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.m */, CC583AC11EF9BAB400134156 /* ASTestCase.h */, CC583AC21EF9BAB400134156 /* ASTestCase.m */, @@ -1717,6 +1744,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */, E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */, E5B225281F1790D6001E1431 /* ASHashing.h in Headers */, CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, @@ -1731,8 +1759,6 @@ 696F01EC1DD2AF450049FBD5 /* ASEventLog.h in Headers */, 690C35641E055C7B00069B91 /* ASDimensionInternal.h in Headers */, 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */, - 690C356B1E05680300069B91 /* ASDimensionDeprecated.h in Headers */, - 683489281D70DE3400327501 /* ASDisplayNode+Deprecated.h in Headers */, 698371DB1E4379CD00437585 /* ASNodeController+Beta.h in Headers */, 6907C2581DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h in Headers */, 69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */, @@ -1831,6 +1857,7 @@ E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */, E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */, + E52AC9BB1FEA90EB00AA4040 /* ASRectMap.h in Headers */, E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, @@ -1892,7 +1919,6 @@ B35062061B010EFD0018CF92 /* ASNetworkImageNode.h in Headers */, CCA282C81E9EB64B0037E8B7 /* ASDisplayNodeTipState.h in Headers */, 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */, - E5ABAC7B1E8564EE007AC15C /* ASRectTable.h in Headers */, B35062261B010EFD0018CF92 /* ASRangeController.h in Headers */, 34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */, DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */, @@ -2074,13 +2100,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-AsyncDisplayKitTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B9D88CDF51B429C8409E4B6 /* [CP] Copy Pods Resources */ = { @@ -2139,9 +2168,10 @@ 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */, 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */, - CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */, F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */, + BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.m in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, + BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.m in Sources */, 695BE2551DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm in Sources */, CCA221D31D6FA7EF00AF6A0F /* ASViewControllerTests.m in Sources */, 058D0A38195D057000B7D73C /* ASDisplayLayerTests.m in Sources */, @@ -2154,6 +2184,7 @@ 696FCB311D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm in Sources */, CC583AD81EF9BDC300134156 /* OCMockObject+ASAdditions.m in Sources */, 69FEE53D1D95A9AF0086F066 /* ASLayoutElementStyleTests.m in Sources */, + 4E9127691F64157600499623 /* ASRunLoopQueueTests.m in Sources */, CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */, CC54A81E1D7008B300296A24 /* ASDispatchTests.m in Sources */, CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.m in Sources */, @@ -2166,25 +2197,31 @@ CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */, 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m in Sources */, + E586F96C1F9F9E2900ECE00E /* ASScrollNodeTests.m in Sources */, CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.m in Sources */, CC583AD91EF9BDC600134156 /* ASDisplayNode+OCMock.m in Sources */, 697B315A1CFE4B410049936F /* ASEditableTextNodeTests.m in Sources */, ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */, CC8B05D61D73836400F54286 /* ASPerformanceTestContext.m in Sources */, CC0AEEA41D66316E005D1C78 /* ASUICollectionViewTests.m in Sources */, + CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */, 69B225671D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm in Sources */, ACF6ED621B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm in Sources */, 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */, CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m in Sources */, + 1A6C00111FAB4EDD00D05926 /* ASCornerLayoutSpecSnapshotTests.mm in Sources */, 254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */, 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */, ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */, + E52AC9C01FEA916C00AA4040 /* ASRectMapTests.m in Sources */, + CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */, 81E95C141D62639600336598 /* ASTextNodeSnapshotTests.m in Sources */, 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */, AEEC47E41C21D3D200EC1693 /* ASVideoNodeTests.m in Sources */, 254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */, 058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m in Sources */, CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */, + CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */, 058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */, DBC453221C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m in Sources */, 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */, @@ -2205,9 +2242,9 @@ 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.m in Sources */, CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */, CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.m in Sources */, + E52AC9BA1FEA90EB00AA4040 /* ASRectMap.mm in Sources */, 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.m in Sources */, B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */, - 690C35671E0567C600069B91 /* ASDimensionDeprecated.mm in Sources */, 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, CCA282B91E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.m in Sources */, 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */, @@ -2318,7 +2355,6 @@ 254C6B8B1BF94F8A003EC431 /* ASTextKitShadower.mm in Sources */, 254C6B851BF94F8A003EC431 /* ASTextKitAttributes.mm in Sources */, 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */, - E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */, CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.m in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, @@ -2337,6 +2373,7 @@ 68FC85E51CE29B7E00EDD713 /* ASTabBarController.m in Sources */, CCCCCCDC1EC3EF060087FE10 /* ASTextLine.m in Sources */, 34EFC7741B701D0A00AD841F /* ASAbsoluteLayoutSpec.mm in Sources */, + 1A6C000E1FAB4E2100D05926 /* ASCornerLayoutSpec.mm in Sources */, CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */, 690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */, 909C4C761F09C98B00D6B76F /* ASTextNode2.mm in Sources */, @@ -2431,14 +2468,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -2476,14 +2519,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -2634,14 +2683,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; diff --git a/CHANGELOG.md b/CHANGELOG.md index 7deeff472a..35e4f360bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,38 @@ ## master - * Add your own contributions to the next release on the line below this with your name. +- [ASRectMap] Replace implementation of ASRectTable with a simpler one based on unordered_map.[Scott Goodson](https://github.com/appleguy) [#719](https://github.com/TextureGroup/Texture/pull/719) +- [ASCollectionView] Add missing flags for ASCollectionDelegate [Ilya Zheleznikov](https://github.com/ilyailya) [#718](https://github.com/TextureGroup/Texture/pull/718) +- [ASNetworkImageNode] Deprecates .URLs in favor of .URL [Garrett Moon](https://github.com/garrettmoon) [#699](https://github.com/TextureGroup/Texture/pull/699) +- [iOS11] Update project settings and fix errors [Eke](https://github.com/Eke) [#676](https://github.com/TextureGroup/Texture/pull/676) +- [ASCollectionView] Improve performance and behavior of rotation / bounds changes. [Scott Goodson](https://github.com/appleguy) [#431](https://github.com/TextureGroup/Texture/pull/431) +- [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy) +- [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) +- [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon) +- [ASNetworkImageNode] New delegate callback to tell the consumer whether the image was loaded from cache or download. [Adlai Holler](https://github.com/Adlai-Holler) +- [Layout] Fixes a deadlock in layout. [#638](https://github.com/TextureGroup/Texture/pull/638) [Garrett Moon](https://github.com/garrettmoon) +- Updated to be backwards compatible with Xcode 8. [Adlai Holler](https://github.com/Adlai-Holler) +- [API CHANGES] `ASPerformMainThreadDeallocation` and `ASPerformBackgroundDeallocation` functions take `id *` instead of `id` and they're now more reliable. Also, in Swift, `ASDeallocQueue.sharedDeallocationQueue() -> ASDeallocQueue.sharedDeallocationQueue`. [Adlai Holler](https://github.com/Adlai-Holler) [#651](https://github.com/TextureGroup/Texture/pull/651) +- [Collection/Table] Added direct support for mapping section indexes between data spaces. [Adlai Holler](https://github.com/Adlai-Holler) [#651](https://github.com/TextureGroup/Texture/pull/660) +- [ASCornerLayoutSpec] New layout spec class for declarative corner element layout. [#657](https://github.com/TextureGroup/Texture/pull/657) [huangkun](https://github.com/huang-kun) +- [Layout] Fix an issue that causes a pending layout to be applied multiple times. [Huy Nguyen](https://github.com/nguyenhuy) [#695](https://github.com/TextureGroup/Texture/pull/695) +- [ASScrollNode] Ensure the node respects the given size range while calculating its layout. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) +- [ASScrollNode] Invalidate the node's calculated layout if its scrollable directions changed. Also add unit tests for the class. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy) +- Add new unit testing to the layout engine. [Adlai Holler](https://github.com/Adlai-Holler) [#424](https://github.com/TextureGroup/Texture/pull/424) +- [Automatic Subnode Management] Nodes with ASM enabled now insert/delete their subnodes as soon as they enter preload state, so the subnodes can preload too. [Huy Nguyen](https://github.com/nguyenhuy) [#706](https://github.com/TextureGroup/Texture/pull/706) + +## 2.6 +- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) + +## 2.5.1 +- [ASVideoNode] Fix unreleased time observer. [Flo Vouin](https://github.com/flovouin) +- [PINCache] Set a default .byteLimit to reduce disk usage and startup time. [#595](https://github.com/TextureGroup/Texture/pull/595) [Scott Goodson](https://github.com/appleguy) +- [ASNetworkImageNode] Fix deadlock in GIF handling. [#582](https://github.com/TextureGroup/Texture/pull/582) [Garrett Moon](https://github.com/garrettmoon) +- [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) +- [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) +- [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) + +## 2.5 + - [ASCollectionNode] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. [#522](https://github.com/TextureGroup/Texture/pull/522) [Scott Goodson](https://github.com/appleguy) - [Accessibility] Add .isAccessibilityContainer property, allowing automatic aggregation of children's a11y labels. [#468][Scott Goodson](https://github.com/appleguy) - [ASImageNode] Enabled .clipsToBounds by default, fixing the use of .cornerRadius and clipping of GIFs. [Scott Goodson](https://github.com/appleguy) [#466](https://github.com/TextureGroup/Texture/pull/466) @@ -8,15 +40,25 @@ - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) - Fix an issue that causes calculatedLayoutDidChange being called needlessly. [Huy Nguyen](https://github.com/nguyenhuy) [#490](https://github.com/TextureGroup/Texture/pull/490) - Negate iOS 11 automatic estimated table row heights. [Christian Selig](https://github.com/christianselig) [#485](https://github.com/TextureGroup/Texture/pull/485) -- [Breaking] Add content offset bridging property to ASTableNode and ASCollectionNode. Deprecate related methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) +- Add content inset and offset bridging properties to ASTableNode and ASCollectionNode. Deprecate related properties and methods in ASTableView and ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#460](https://github.com/TextureGroup/Texture/pull/460) [#560](https://github.com/TextureGroup/Texture/pull/560) - Remove re-entrant access to self.view when applying initial pending state. [Adlai Holler](https://github.com/Adlai-Holler) [#510](https://github.com/TextureGroup/Texture/pull/510) -- Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) [#513](https://github.com/TextureGroup/Texture/pull/513) +- Small improvements in ASCollectionLayout [Huy Nguyen](https://github.com/nguyenhuy) [#509](https://github.com/TextureGroup/Texture/pull/509) [#513](https://github.com/TextureGroup/Texture/pull/513) [#562]((https://github.com/TextureGroup/Texture/pull/562) - Fix retain cycle between ASImageNode and PINAnimatedImage [Phil Larson](https://github.com/plarson) [#520](https://github.com/TextureGroup/Texture/pull/520) - Change the API for disabling logging from a compiler flag to a runtime C function ASDisableLogging(). [Adlai Holler](https://github.com/Adlai-Holler) [#528](https://github.com/TextureGroup/Texture/pull/528) - Table and collection views to consider content inset when calculating (default) element size range [Huy Nguyen](https://github.com/nguyenhuy) [#525](https://github.com/TextureGroup/Texture/pull/525) - [ASEditableTextNode] added -editableTextNodeShouldBeginEditing to ASEditableTextNodeDelegate to mirror the corresponding method from UITextViewDelegate. [Yan S.](https://github.com/yans) [#535](https://github.com/TextureGroup/Texture/pull/535) +- [Breaking] Remove APIs that have been deprecated since 2.0 and/or for at least 6 months [Huy Nguyen](https://github.com/nguyenhuy) [#529](https://github.com/TextureGroup/Texture/pull/529) +- [ASDisplayNode] Ensure `-displayWillStartAsynchronously:` and `-displayDidFinish` are invoked on rasterized subnodes. [Eric Scheers](https://github.com/smeis) [#532](https://github.com/TextureGroup/Texture/pull/532) +- Fixed a memory corruption issue in the ASImageNode display system. [Adlai Holler](https://github.com/Adlai-Holler) [#555](https://github.com/TextureGroup/Texture/pull/555) +- [Breaking] Rename ASCollectionGalleryLayoutSizeProviding to ASCollectionGalleryLayoutPropertiesProviding. Besides a fixed item size, it now can provide interitem and line spacings, as well as section inset [Huy Nguyen](https://github.com/nguyenhuy) [#496](https://github.com/TextureGroup/Texture/pull/496) [#533](https://github.com/TextureGroup/Texture/pull/533) +- Deprecate `-[ASDisplayNode displayWillStart]` in favor of `-displayWillStartAsynchronously:` [Huy Nguyen](https://github.com/nguyenhuy) [#536](https:/ +/github.com/TextureGroup/Texture/pull/536) +- Add support for URLs on ASNetworkImageNode. [Garrett Moon](https://github.com/garrettmoon) +- [ASImageNode] Always dealloc images in a background queue [Huy Nguyen](https://github.com/nguyenhuy) [#561](https://github.com/TextureGroup/Texture/pull/561) +- Mark ASRunLoopQueue as drained if it contains only NULLs [Cesar Estebanez](https://github.com/cesteban) [#558](https://github.com/TextureGroup/Texture/pull/558) +- Fix crashes caused by failing to unlock or destroy a static mutex while the app is being terminated [Huy Nguyen](https://github.com/nguyenhuy) -##2.4 +## 2.4 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) - Overhaul logging and add activity tracing support. [Adlai Holler](https://github.com/Adlai-Holler) - Fix a crash where scrolling a table view after entering editing mode could lead to bad internal states in the table. [Huy Nguyen](https://github.com/nguyenhuy) [#416](https://github.com/TextureGroup/Texture/pull/416/) @@ -30,7 +72,7 @@ - Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) - Rename ASCellNode.viewModel to ASCellNode.nodeModel to reduce collisions with subclass properties implemented by clients. [Adlai Holler](https://github.com/Adlai-Holler) [#504](https://github.com/TextureGroup/Texture/pull/504) -##2.3.4 +## 2.3.4 - [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration [Scott Goodson](https://github.com/appleguy) - [ASTraitCollection] Convert ASPrimitiveTraitCollection from lock to atomic. [Scott Goodson](https://github.com/appleguy) - Add a synchronous mode to ASCollectionNode, for colletion view data source debugging. [Hannah Troisi](https://github.com/hannahmbanana) diff --git a/Cartfile b/Cartfile index 93ba674369..aebf9308e8 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ -github "pinterest/PINRemoteImage" "3.0.0-beta.11" -github "pinterest/PINCache" "3.0.1-beta.5" +github "pinterest/PINRemoteImage" "3.0.0-beta.13" +github "pinterest/PINCache" diff --git a/Podfile b/Podfile index 0d8cb0fa26..0ebd8c98ca 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ target :'AsyncDisplayKitTests' do pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' # Only for buck build - pod 'PINRemoteImage', '3.0.0-beta.10' + pod 'PINRemoteImage', '3.0.0-beta.13' end #TODO CocoaPods plugin instead? diff --git a/Source/ASBlockTypes.h b/Source/ASBlockTypes.h index cf05a19377..f0e2875e12 100644 --- a/Source/ASBlockTypes.h +++ b/Source/ASBlockTypes.h @@ -22,7 +22,7 @@ /** * ASCellNode creation block. Used to lazily create the ASCellNode instance for a specified indexPath. */ -typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(); +typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(void); // Type for the cancellation checker block passed into the async display blocks. YES means the operation has been cancelled, NO means continue. typedef BOOL(^asdisplaynode_iscancelled_block_t)(void); diff --git a/Source/ASCollectionNode+Beta.h b/Source/ASCollectionNode+Beta.h index 8529ea0999..ea391e8dce 100644 --- a/Source/ASCollectionNode+Beta.h +++ b/Source/ASCollectionNode+Beta.h @@ -68,6 +68,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion ASDISPLAYNODE_DEPRECATED_MSG("Use -performBatchUpdates:completion: instead."); +- (void)invalidateFlowLayoutDelegateMetrics; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 542a8bf444..63e53b95e9 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -130,6 +130,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, weak) id layoutInspector; +/** + * The distance that the content view is inset from the collection node edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset; + /** * The offset of the content view's origin from the collection node's origin. Defaults to CGPointZero. */ @@ -241,7 +246,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. @@ -252,7 +257,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. @@ -281,7 +286,7 @@ NS_ASSUME_NONNULL_BEGIN * * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. */ -- (void)onDidFinishProcessingUpdates:(nullable void (^)())didFinishProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))didFinishProcessingUpdates; /** * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. @@ -379,7 +384,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UICollectionView's version. */ -- (void)reloadDataWithCompletion:(nullable void (^)())completion; +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion; /** @@ -526,16 +531,6 @@ NS_ASSUME_NONNULL_BEGIN @interface ASCollectionNode (Deprecated) -/** - * Reload everything from scratch, destroying the working range and all cached nodes. - * - * @warning This method is substantially more expensive than UICollectionView's version. - * - * @deprecated This method is deprecated in 2.0. Use @c reloadDataWithCompletion: and - * then @c waitUntilAllUpdatesAreProcessed instead. - */ -- (void)reloadDataImmediately ASDISPLAYNODE_DEPRECATED_MSG("Use -reloadData / -reloadDataWithCompletion: followed by -waitUntilAllUpdatesAreProcessed instead."); - - (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -waitUntilAllUpdatesAreProcessed."); @end @@ -883,6 +878,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 5db5f8f63d..8ad45c4829 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -50,6 +50,7 @@ @property (nonatomic, assign) BOOL usesSynchronousDataLoading; @property (nonatomic, assign) CGFloat leadingScreensForBatching; @property (weak, nonatomic) id layoutInspector; +@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) CGPoint contentOffset; @property (nonatomic, assign) BOOL animatesContentOffset; @end @@ -64,6 +65,7 @@ _allowsSelection = YES; _allowsMultipleSelection = NO; _inverted = NO; + _contentInset = UIEdgeInsetsZero; _contentOffset = CGPointZero; _animatesContentOffset = NO; } @@ -182,6 +184,7 @@ if (_pendingState) { _ASCollectionPendingState *pendingState = _pendingState; + self.pendingState = nil; view.asyncDelegate = pendingState.delegate; view.asyncDataSource = pendingState.dataSource; view.inverted = pendingState.inverted; @@ -189,7 +192,7 @@ view.allowsMultipleSelection = pendingState.allowsMultipleSelection; view.usesSynchronousDataLoading = pendingState.usesSynchronousDataLoading; view.layoutInspector = pendingState.layoutInspector; - self.pendingState = nil; + view.contentInset = pendingState.contentInset; if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; @@ -441,6 +444,25 @@ } } +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + if ([self pendingState]) { + _pendingState.contentInset = contentInset; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.contentInset = contentInset; + } +} + +- (UIEdgeInsets)contentInset +{ + if ([self pendingState]) { + return _pendingState.contentInset; + } else { + return self.view.contentInset; + } +} + - (void)setContentOffset:(CGPoint)contentOffset { [self setContentOffset:contentOffset animated:NO]; @@ -452,6 +474,7 @@ _pendingState.contentOffset = contentOffset; _pendingState.animatesContentOffset = animated; } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); [self.view setContentOffset:contentOffset animated:animated]; } } @@ -754,13 +777,6 @@ [self reloadDataWithCompletion:nil]; } -- (void)reloadDataImmediately -{ - ASDisplayNodeAssertMainThread(); - [self reloadData]; - [self waitUntilAllUpdatesAreProcessed]; -} - - (void)relayoutItems { ASDisplayNodeAssertMainThread(); @@ -790,6 +806,13 @@ } } +- (void)invalidateFlowLayoutDelegateMetrics { + ASDisplayNodeAssertMainThread(); + if (self.nodeLoaded) { + [self.view invalidateFlowLayoutDelegateMetrics]; + } +} + - (void)insertSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index 45dc040c5e..869990aada 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -143,6 +143,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) BOOL zeroContentInsets ASDISPLAYNODE_DEPRECATED_MSG("Set automaticallyAdjustsScrollViewInsets=NO on your view controller instead."); +/** + * The distance that the content view is inset from the collection view edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode property instead"); + /** * The point at which the origin of the content view is offset from the origin of the collection view. */ @@ -253,7 +258,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); /** * Perform a batch of updates asynchronously. This method must be called from the main thread. @@ -264,7 +269,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -273,7 +278,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UICollectionView's version. */ -- (void)reloadDataWithCompletion:(nullable void (^)())completion AS_UNAVAILABLE("Use ASCollectionNode method instead."); +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion AS_UNAVAILABLE("Use ASCollectionNode method instead."); /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -282,14 +287,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData AS_UNAVAILABLE("Use ASCollectionNode method instead."); -/** - * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. - * - * @warning This method is substantially more expensive than UICollectionView's version and will block the main thread - * while all the cells load. - */ -- (void)reloadDataImmediately AS_UNAVAILABLE("Use ASCollectionNode method instead."); - /** * Triggers a relayout of all nodes. * @@ -301,7 +298,7 @@ NS_ASSUME_NONNULL_BEGIN * See ASCollectionNode.h for full documentation of these methods. */ @property (nonatomic, readonly) BOOL isProcessingUpdates; -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion; - (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASCollectionNode waitUntilAllUpdatesAreProcessed] instead."); /** diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 52a7e53f9f..1fdcd54dac 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -29,6 +29,7 @@ #import #import #import +#import #import #import #import @@ -63,6 +64,12 @@ return __val; \ } +#define ASFlowLayoutDefault(layout, property, default) \ +({ \ + UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ + flowLayout ? flowLayout.property : default; \ +}) + /// What, if any, invalidation should we perform during the next -layoutSubviews. typedef NS_ENUM(NSUInteger, ASCollectionViewInvalidationStyle) { /// Perform no invalidation. @@ -192,6 +199,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; unsigned int interop:1; unsigned int interopWillDisplayCell:1; unsigned int interopDidEndDisplayingCell:1; + unsigned int interopWillDisplaySupplementaryView:1; + unsigned int interopdidEndDisplayingSupplementaryView:1; } _asyncDelegateFlags; struct { @@ -264,7 +273,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; // Experiments done by Instagram show that this option being YES (default) // when unused causes a significant hit to scroll performance. // https://github.com/Instagram/IGListKit/issues/318 - if (AS_AT_LEAST_IOS10) { + if (AS_AVAILABLE_IOS(10)) { super.prefetchingEnabled = NO; } @@ -321,8 +330,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; [self setAsyncDataSource:nil]; // Data controller & range controller may own a ton of nodes, let's deallocate those off-main. - ASPerformBackgroundDeallocation(_dataController); - ASPerformBackgroundDeallocation(_rangeController); + ASPerformBackgroundDeallocation(&_dataController); + ASPerformBackgroundDeallocation(&_rangeController); } #pragma mark - @@ -356,7 +365,10 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (void)relayoutItems { - [_dataController relayoutAllNodes]; + [_dataController relayoutAllNodesWithInvalidationBlock:^{ + [self.collectionViewLayout invalidateLayout]; + [self invalidateFlowLayoutDelegateMetrics]; + }]; } - (BOOL)isProcessingUpdates @@ -527,11 +539,15 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; _asyncDelegateFlags.collectionNodeShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldShowMenuForItemAtIndexPath:)]; _asyncDelegateFlags.collectionNodeCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:canPerformAction:forItemAtIndexPath:sender:)]; _asyncDelegateFlags.collectionNodePerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:performAction:forItemAtIndexPath:sender:)]; + _asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplaySupplementaryElementWithNode:)]; + _asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingSupplementaryElementWithNode:)]; _asyncDelegateFlags.interop = [_asyncDelegate conformsToProtocol:@protocol(ASCollectionDelegateInterop)]; if (_asyncDelegateFlags.interop) { id interopDelegate = (id)_asyncDelegate; _asyncDelegateFlags.interopWillDisplayCell = [interopDelegate respondsToSelector:@selector(collectionView:willDisplayCell:forItemAtIndexPath:)]; _asyncDelegateFlags.interopDidEndDisplayingCell = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)]; + _asyncDelegateFlags.interopWillDisplaySupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:)]; + _asyncDelegateFlags.interopdidEndDisplayingSupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:)]; } } @@ -663,19 +679,13 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; if (indexPath == nil) { return nil; } - - // If this is a section index path, we don't currently have a method - // to do a mapping. - if (indexPath.item == NSNotFound) { - return indexPath; - } else { - NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; - if (viewIndexPath == nil && wait) { - [self waitUntilAllUpdatesAreCommitted]; - return [self convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:NO]; - } - return viewIndexPath; + + NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; + if (viewIndexPath == nil && wait) { + [self waitUntilAllUpdatesAreCommitted]; + return [self convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:NO]; } + return viewIndexPath; } /** @@ -709,13 +719,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; return nil; } - // If this is a section index path, we don't currently have a method - // to do a mapping. - if (indexPath.item == NSNotFound) { - return indexPath; - } else { - return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; - } + return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; } - (NSArray *)convertIndexPathsToCollectionNode:(NSArray *)indexPaths @@ -771,6 +775,25 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; self.dataController.usesSynchronousDataLoading = usesSynchronousDataLoading; } +- (void)invalidateFlowLayoutDelegateMetrics { + for (ASCollectionElement *element in self.dataController.pendingMap) { + // This may be either a Supplementary or Item type element. + // For UIKit passthrough cells of either type, re-fetch their sizes from the standard UIKit delegate methods. + ASCellNode *node = element.node; + if (node.shouldUseUIKitCell) { + NSIndexPath *indexPath = [self indexPathForNode:node]; + NSString *kind = [element supplementaryElementKind]; + CGSize previousSize = node.style.preferredSize; + CGSize size = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath]; + + if (!CGSizeEqualToSize(previousSize, size)) { + node.style.preferredSize = size; + [node invalidateCalculatedLayout]; + } + } + } +} + #pragma mark Internal - (void)_configureCollectionViewLayout:(nonnull UICollectionViewLayout *)layout @@ -781,6 +804,46 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; } } +/** + This method is called only for UIKit Passthrough cells - either regular Items or Supplementary elements. + It checks if the delegate implements the UICollectionViewFlowLayout methods that provide sizes, and if not, + uses the default values set on the flow layout. If a flow layout is not in use, UICollectionView Passthrough + cells must be sized by logic in the Layout object, and Texture does not participate in these paths. +*/ +- (CGSize)_sizeForUIKitCellWithKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + CGSize size = CGSizeZero; + UICollectionViewLayout *l = self.collectionViewLayout; + + if (kind == nil) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interop, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:); + if ([_asyncDelegate respondsToSelector:sizeForItem]) { + size = [(id)_asyncDelegate collectionView:self layout:l sizeForItemAtIndexPath:indexPath]; + } else { + size = ASFlowLayoutDefault(l, itemSize, CGSizeZero); + } + } else if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:); + if ([_asyncDelegate respondsToSelector:sizeForHeader]) { + size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForHeaderInSection:indexPath.section]; + } else { + size = ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); + } + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { + ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); + SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:); + if ([_asyncDelegate respondsToSelector:sizeForFooter]) { + size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForFooterInSection:indexPath.section]; + } else { + size = ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); + } + } + + return size; +} + /** Performing nested batch updates with super (e.g. resizing a cell node & updating collection view during same frame) can cause super to throw data integrity exceptions because it checks the data source counts before @@ -961,15 +1024,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; return [_dataController.visibleMap numberOfItemsInSection:section]; } -#define ASIndexPathForSection(section) [NSIndexPath indexPathForItem:0 inSection:section] -#define ASFlowLayoutDefault(layout, property, default) \ -({ \ - UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ - flowLayout ? flowLayout.property : default; \ -}) - - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout - sizeForItemAtIndexPath:(NSIndexPath *)indexPath + sizeForItemAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; @@ -977,66 +1033,65 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; } - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -referenceSizeForHeaderInSection:(NSInteger)section + referenceSizeForHeaderInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); ASElementMap *map = _dataController.visibleMap; ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionHeader - atIndexPath:ASIndexPathForSection(section)]; + atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); } - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -referenceSizeForFooterInSection:(NSInteger)section + referenceSizeForFooterInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); ASElementMap *map = _dataController.visibleMap; ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionFooter - atIndexPath:ASIndexPathForSection(section)]; + atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); } // For the methods that call delegateIndexPathForSection:withSelector:, translate the section from // visibleMap to pendingMap. If the section no longer exists, or the delegate doesn't implement -// the selector, we will return a nil indexPath (and then use the ASFlowLayoutDefault). -- (NSIndexPath *)delegateIndexPathForSection:(NSInteger)section withSelector:(SEL)selector +// the selector, we will return NSNotFound (and then use the ASFlowLayoutDefault). +- (NSInteger)delegateIndexForSection:(NSInteger)section withSelector:(SEL)selector { if ([_asyncDelegate respondsToSelector:selector]) { - return [_dataController.pendingMap convertIndexPath:ASIndexPathForSection(section) - fromMap:_dataController.visibleMap]; + return [_dataController.pendingMap convertSection:section fromMap:_dataController.visibleMap]; } else { - return nil; + return NSNotFound; } } - (UIEdgeInsets)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l - insetForSectionAtIndex:(NSInteger)section + insetForSectionAtIndex:(NSInteger)section { - NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; - if (indexPath) { - return [(id)_asyncDelegate collectionView:cv layout:l insetForSectionAtIndex:indexPath.section]; + section = [self delegateIndexForSection:section withSelector:_cmd]; + if (section != NSNotFound) { + return [(id)_asyncDelegate collectionView:cv layout:l insetForSectionAtIndex:section]; } return ASFlowLayoutDefault(l, sectionInset, UIEdgeInsetsZero); } - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -minimumInteritemSpacingForSectionAtIndex:(NSInteger)section + minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { - NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; - if (indexPath) { + section = [self delegateIndexForSection:section withSelector:_cmd]; + if (section != NSNotFound) { return [(id)_asyncDelegate collectionView:cv layout:l - minimumInteritemSpacingForSectionAtIndex:indexPath.section]; + minimumInteritemSpacingForSectionAtIndex:section]; } return ASFlowLayoutDefault(l, minimumInteritemSpacing, 10.0); // Default is documented as 10.0 } - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l -minimumLineSpacingForSectionAtIndex:(NSInteger)section + minimumLineSpacingForSectionAtIndex:(NSInteger)section { - NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; - if (indexPath) { + section = [self delegateIndexForSection:section withSelector:_cmd]; + if (section != NSNotFound) { return [(id)_asyncDelegate collectionView:cv layout:l - minimumLineSpacingForSectionAtIndex:indexPath.section]; + minimumLineSpacingForSectionAtIndex:section]; } return ASFlowLayoutDefault(l, minimumLineSpacing, 10.0); // Default is documented as 10.0 } @@ -1046,7 +1101,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section if ([_registeredSupplementaryKinds containsObject:kind] == NO) { [self registerSupplementaryNodeOfKind:kind]; } - + UICollectionReusableView *view = nil; ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:kind atIndexPath:indexPath]; ASCellNode *node = element.node; @@ -1097,7 +1152,10 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopWillDisplayCell) { - [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; + } } _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); @@ -1118,7 +1176,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section ASCellNode *cellNode = element.node; cellNode.scrollView = collectionView; - // Update the selected background view in collectionView:willDisplayCell:forItemAtIndexPath: otherwise it could be to + // Update the selected background view in collectionView:willDisplayCell:forItemAtIndexPath: otherwise it could be too // early e.g. if the selectedBackgroundView was set in didLoad() cell.selectedBackgroundView = cellNode.selectedBackgroundView; @@ -1154,7 +1212,10 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopDidEndDisplayingCell) { - [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; + } } _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); @@ -1195,6 +1256,13 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)rawView forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + if (_asyncDelegateFlags.interopWillDisplaySupplementaryView) { + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:indexPath]; + } + } + _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); if (view == nil) { return; @@ -1228,6 +1296,13 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)rawView forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + if (_asyncDelegateFlags.interopdidEndDisplayingSupplementaryView) { + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:indexPath]; + } + } + _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); if (view == nil) { return; @@ -1427,7 +1502,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section } for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { - // Only nodes that respond to the selector are added to _cellsForVisibilityUpdates + // _cellsForVisibilityUpdates only includes cells for ASCellNode subclasses with overrides of the visibility method. [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView]; } if (_asyncDelegateFlags.scrollViewDidScroll) { @@ -1496,7 +1571,12 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { - _leadingScreensForBatching = leadingScreensForBatching; + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + ASPerformBlockOnMainThread(^{ + [self _checkForBatchFetching]; + }); + } } - (CGFloat)leadingScreensForBatching @@ -1680,6 +1760,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath { + ASDisplayNodeAssertMainThread(); ASCellNodeBlock block = nil; ASCellNode *cell = nil; @@ -1707,14 +1788,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section if (block == nil) { if (_asyncDataSourceFlags.interop) { - UICollectionViewLayout *layout = self.collectionViewLayout; - CGSize preferredSize = CGSizeZero; - SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:); - if ([_asyncDelegate respondsToSelector:sizeForItem]) { - preferredSize = [(id)_asyncDelegate collectionView:self layout:layout sizeForItemAtIndexPath:indexPath]; - } else { - preferredSize = ASFlowLayoutDefault(layout, itemSize, CGSizeZero); - } + CGSize preferredSize = [self _sizeForUIKitCellWithKind:nil atIndexPath:indexPath]; block = ^{ ASCellNode *node = [[ASCellNode alloc] init]; node.shouldUseUIKitCell = YES; @@ -1790,6 +1864,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section - (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + ASDisplayNodeAssertMainThread(); ASCellNodeBlock nodeBlock = nil; ASCellNode *node = nil; if (_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement) { @@ -1809,27 +1884,12 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section if (node) { nodeBlock = ^{ return node; }; } else { - BOOL useUIKitCell = _asyncDataSourceFlags.interop; + // In this case, the app code returned nil for the node and the nodeBlock. + // If the UIKit method is implemented, then we should use it. Otherwise the CGSizeZero default will cause UIKit to not show it. CGSize preferredSize = CGSizeZero; + BOOL useUIKitCell = _asyncDataSourceFlags.interopViewForSupplementaryElement; if (useUIKitCell) { - UICollectionViewLayout *layout = self.collectionViewLayout; - if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { - SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:); - if ([_asyncDelegate respondsToSelector:sizeForHeader]) { - preferredSize = [(id)_asyncDelegate collectionView:self layout:layout - referenceSizeForHeaderInSection:indexPath.section]; - } else { - preferredSize = ASFlowLayoutDefault(layout, headerReferenceSize, CGSizeZero); - } - } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { - SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:); - if ([_asyncDelegate respondsToSelector:sizeForFooter]) { - preferredSize = [(id)_asyncDelegate collectionView:self layout:layout - referenceSizeForFooterInSection:indexPath.section]; - } else { - preferredSize = ASFlowLayoutDefault(layout, footerReferenceSize, CGSizeZero); - } - } + preferredSize = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath]; } nodeBlock = ^{ ASCellNode *node = [[ASCellNode alloc] init]; @@ -1914,7 +1974,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section /// The UIKit version of this method is only available on iOS >= 9 - (NSArray *)asdk_indexPathsForVisibleSupplementaryElementsOfKind:(NSString *)kind { - if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_9_0) { + if (AS_AVAILABLE_IOS(9)) { return [self indexPathsForVisibleSupplementaryElementsOfKind:kind]; } @@ -2177,18 +2237,17 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section */ - (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds { - if (_hasDataControllerLayoutDelegate) { - // Let the layout delegate handle bounds changes if it's available. - return; - } - if (self.collectionViewLayout == nil) { - return; - } + CGSize newSize = newBounds.size; CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes; - if (CGSizeEqualToSize(lastUsedSize, newBounds.size)) { + if (CGSizeEqualToSize(lastUsedSize, newSize)) { return; } - _lastBoundsSizeUsedForMeasuringNodes = newBounds.size; + if (_hasDataControllerLayoutDelegate || self.collectionViewLayout == nil) { + // Let the layout delegate handle bounds changes if it's available. If no layout, it will init in the new state. + return; + } + + _lastBoundsSizeUsedForMeasuringNodes = newSize; // Laying out all nodes is expensive. // We only need to do this if the bounds changed in the non-scrollable direction. @@ -2196,16 +2255,14 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section // appearance update, we do not need to relayout all nodes. // For a more permanent fix to the unsafety mentioned above, see https://github.com/facebook/AsyncDisplayKit/pull/2182 ASScrollDirection scrollDirection = self.scrollableDirections; - BOOL fixedVertically = (ASScrollDirectionContainsVerticalDirection(scrollDirection) == NO); + BOOL fixedVertically = (ASScrollDirectionContainsVerticalDirection (scrollDirection) == NO); BOOL fixedHorizontally = (ASScrollDirectionContainsHorizontalDirection(scrollDirection) == NO); - BOOL changedInNonScrollingDirection = (fixedHorizontally && newBounds.size.width != lastUsedSize.width) || (fixedVertically && newBounds.size.height != lastUsedSize.height); + BOOL changedInNonScrollingDirection = (fixedHorizontally && newSize.width != lastUsedSize.width) || + (fixedVertically && newSize.height != lastUsedSize.height); if (changedInNonScrollingDirection) { - [_dataController relayoutAllNodes]; - [_dataController waitUntilAllUpdatesAreProcessed]; - // We need to ensure the size requery is done before we update our layout. - [self.collectionViewLayout invalidateLayout]; + [self relayoutItems]; } } diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index 8e413a5a35..fff4ee9752 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -28,8 +28,8 @@ NS_ASSUME_NONNULL_BEGIN ASDISPLAYNODE_EXTERN_C_BEGIN -void ASPerformBlockOnMainThread(void (^block)()); -void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT +void ASPerformBlockOnMainThread(void (^block)(void)); +void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT ASDISPLAYNODE_EXTERN_C_END #if ASEVENTLOG_ENABLE @@ -61,19 +61,6 @@ typedef struct { @interface ASDisplayNode (Beta) -/** - * ASTableView and ASCollectionView now throw exceptions on invalid updates - * like their UIKit counterparts. If YES, these classes will log messages - * on invalid updates rather than throwing exceptions. - * - * Note that even if AsyncDisplayKit's exception is suppressed, the app may still crash - * as it proceeds with an invalid update. - * - * This property defaults to NO. It will be removed in a future release. - */ -+ (BOOL)suppressesInvalidCollectionUpdateExceptions AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled."); -+ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled."); - /** * @abstract Recursively ensures node and all subnodes are displayed. * @see Full documentation in ASDisplayNode+FrameworkPrivate.h @@ -188,12 +175,15 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable - (void)addYogaChild:(ASDisplayNode *)child; - (void)removeYogaChild:(ASDisplayNode *)child; +- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index; - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; @property (nonatomic, assign) BOOL yogaLayoutInProgress; @property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout; -// These methods should not normally be called directly. + +// These methods are intended to be used internally to Texture, and should not be called directly. +- (BOOL)shouldHaveYogaMeasureFunc; - (void)invalidateCalculatedYogaLayout; - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize; diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 5674997d00..dd0a2e2adf 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -57,15 +57,6 @@ #pragma mark Measurement Pass - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize -{ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // For now we just call the deprecated measureWithSizeRange: method to not break old API - return [self measureWithSizeRange:constrainedSize]; -#pragma clang diagnostic pop -} - -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; } @@ -94,6 +85,7 @@ layout = [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; + as_log_verbose(ASLayoutLog(), "Established pending layout for %@ in %s", self, sel_getName(_cmd)); _pendingDisplayNodeLayout = std::make_shared(layout, constrainedSize, parentSize, version); ASDisplayNodeAssertNotNil(layout, @"-[ASDisplayNode layoutThatFits:parentSize:] newly calculated layout should not be nil! %@", self); } @@ -127,8 +119,6 @@ ASLayoutElementStyleExtensibilityForwarding return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; } -ASPrimitiveTraitCollectionDeprecatedImplementation - #pragma mark - ASLayoutElementAsciiArtProtocol - (NSString *)asciiArtString @@ -222,8 +212,9 @@ ASPrimitiveTraitCollectionDeprecatedImplementation * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. */ -- (void)_setNeedsLayoutFromAbove +- (void)_u_setNeedsLayoutFromAbove { + ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock); as_activity_create_for_scope("Set needs layout from above"); ASDisplayNodeAssertThreadAffinity(self); @@ -238,7 +229,7 @@ ASPrimitiveTraitCollectionDeprecatedImplementation if (supernode) { // Threading model requires that we unlock before calling a method on our parent. - [supernode _setNeedsLayoutFromAbove]; + [supernode _u_setNeedsLayoutFromAbove]; } else { // Let the root node method know that the size was invalidated [self _rootNodeDidInvalidateSize]; @@ -298,8 +289,10 @@ ASPrimitiveTraitCollectionDeprecatedImplementation } } -- (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds +- (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds { + ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock); + ASDN::MutexLocker l(__instanceLock__); // Check if we are a subnode in a layout transition. // In this case no measurement is needed as it's part of the layout transition if ([self _isLayoutTransitionInvalid]) { @@ -308,14 +301,16 @@ ASPrimitiveTraitCollectionDeprecatedImplementation CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); - // Prefer _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout (if exists, it's the newest) - // If there is no _pending, check if _calculated is valid to reuse (avoiding recalculation below). - if (_pendingDisplayNodeLayout == nullptr || _pendingDisplayNodeLayout->version < _layoutVersion) { - if (_calculatedDisplayNodeLayout->version >= _layoutVersion - && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove == YES - || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { - return; - } + // Prefer a newer and not yet applied _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout + // If there is no such _pending, check if _calculated is valid to reuse (avoiding recalculation below). + BOOL pendingLayoutIsPreferred = (_pendingDisplayNodeLayout != nullptr + && _pendingDisplayNodeLayout->version >= _layoutVersion + && _pendingDisplayNodeLayout->version > _calculatedDisplayNodeLayout->version); // _pending is not yet applied + BOOL calculatedLayoutIsReusable = (_calculatedDisplayNodeLayout->version >= _layoutVersion + && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))); + if (!pendingLayoutIsPreferred && calculatedLayoutIsReusable) { + return; } as_activity_create_for_scope("Update node layout for current bounds"); @@ -379,8 +374,10 @@ ASPrimitiveTraitCollectionDeprecatedImplementation // In this case, we need to detect that we've already asked to be resized to match this // particular ASLayout object, and shouldn't loop asking again unless we have a different ASLayout. nextLayout->requestedLayoutFromAbove = YES; - [self _setNeedsLayoutFromAbove]; - // Update the layout's version here because _setNeedsLayoutFromAbove calls __setNeedsLayout which in turn increases _layoutVersion + __instanceLock__.unlock(); + [self _u_setNeedsLayoutFromAbove]; + __instanceLock__.lock(); + // Update the layout's version here because _u_setNeedsLayoutFromAbove calls __setNeedsLayout which in turn increases _layoutVersion // Failing to do this will cause the layout to be invalid immediately nextLayout->version = _layoutVersion; } @@ -400,7 +397,7 @@ ASPrimitiveTraitCollectionDeprecatedImplementation - (ASSizeRange)_locked_constrainedSizeForLayoutPass { - // TODO: The logic in -_setNeedsLayoutFromAbove seems correct and doesn't use this method. + // TODO: The logic in -_u_setNeedsLayoutFromAbove seems correct and doesn't use this method. // logic seems correct. For what case does -this method need to do the CGSizeEqual checks? // IF WE CAN REMOVE BOUNDS CHECKS HERE, THEN WE CAN ALSO REMOVE "REQUESTED FROM ABOVE" CHECK @@ -938,7 +935,7 @@ ASPrimitiveTraitCollectionDeprecatedImplementation // Grab lock after calling out to subclass ASDN::MutexLocker l(__instanceLock__); - // We generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go. + // We generate placeholders at -layoutThatFits: time so that a node is guaranteed to have a placeholder ready to go. // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. // First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync. if (_placeholderEnabled && !_placeholderImage && [self _locked_displaysAsynchronously]) { diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h index d549692737..ead690f93c 100644 --- a/Source/ASDisplayNode+Subclasses.h +++ b/Source/ASDisplayNode+Subclasses.h @@ -116,12 +116,12 @@ NS_ASSUME_NONNULL_BEGIN * @discussion For node subclasses that implement manual layout (e.g., they have a custom -layout method), * calculatedLayout may be accessed on subnodes to retrieved cached information about their size. * This allows -layout to be very fast, saving time on the main thread. - * Note: .calculatedLayout will only be set for nodes that have had -measure: called on them. - * For manual layout, make sure you call -measure: in your implementation of -calculateSizeThatFits:. + * Note: .calculatedLayout will only be set for nodes that have had -layoutThatFits: called on them. + * For manual layout, make sure you call -layoutThatFits: in your implementation of -calculateSizeThatFits:. * * For node subclasses that use automatic layout (e.g., they implement -layoutSpecThatFits:), * it is typically not necessary to use .calculatedLayout at any point. For these nodes, - * the ASLayoutSpec implementation will automatically call -measureWithSizeRange: on all of the subnodes, + * the ASLayoutSpec implementation will automatically call -layoutThatFits: on all of the subnodes, * and the ASDisplayNode base class implementation of -layout will automatically make use of .calculatedLayout on the subnodes. * * @return Layout that wraps calculated size returned by -calculateSizeThatFits: (in manual layout mode), @@ -183,7 +183,7 @@ NS_ASSUME_NONNULL_BEGIN * or -calculateSizeThatFits:, whichever method is overriden. Subclasses rarely need to override this method, * override -layoutSpecThatFits: or -calculateSizeThatFits: instead. * - * @note This method should not be called directly outside of ASDisplayNode; use -measure: or -calculatedLayout instead. + * @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: or -calculatedLayout instead. */ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize; @@ -315,6 +315,18 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable id)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer; +/** + * @abstract Indicates that the receiver is about to display. + * + * @discussion Deprecated in 2.5. + * + * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is + * about to begin. + * + * @note Called on the main thread only + */ +- (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use displayWillStartAsynchronously: instead."); + /** * @abstract Indicates that the receiver is about to display. * @@ -323,7 +335,6 @@ NS_ASSUME_NONNULL_BEGIN * * @note Called on the main thread only */ -- (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER; - (void)displayWillStartAsynchronously:(BOOL)asynchronously ASDISPLAYNODE_REQUIRES_SUPER; /** diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 281bf1e781..70cdd953cc 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -59,20 +59,7 @@ - (void)addYogaChild:(ASDisplayNode *)child { - if (child == nil) { - return; - } - if (_yogaChildren == nil) { - _yogaChildren = [NSMutableArray array]; - } - - // Clean up state in case this child had another parent. - [self removeYogaChild:child]; - - [_yogaChildren addObject:child]; - - // YGNodeRef insertion is done in setParent: - child.yogaParent = self; + [self insertYogaChild:child atIndex:_yogaChildren.count]; } - (void)removeYogaChild:(ASDisplayNode *)child @@ -87,6 +74,24 @@ child.yogaParent = nil; } +- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index +{ + if (child == nil) { + return; + } + if (_yogaChildren == nil) { + _yogaChildren = [NSMutableArray array]; + } + + // Clean up state in case this child had another parent. + [self removeYogaChild:child]; + + [_yogaChildren insertObject:child atIndex:index]; + + // YGNodeRef insertion is done in setParent: + child.yogaParent = self; +} + - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute { if (AS_AT_LEAST_IOS9) { @@ -168,28 +173,72 @@ CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; - self.yogaCalculatedLayout = layout; +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // Assert that the sublayout is already flattened. + for (ASLayout *sublayout in layout.sublayouts) { + if (sublayout.sublayouts.count > 0 || ASDynamicCast(sublayout.layoutElement, ASDisplayNode) == nil) { + ASDisplayNodeAssert(NO, @"Yoga sublayout is not flattened! %@, %@", self, sublayout); + } + } +#endif + + // Because this layout won't go through the rest of the logic in calculateLayoutThatFits:, flatten it now. + layout = [layout filteredNodeLayoutTree]; + + if ([self.yogaCalculatedLayout isEqual:layout] == NO) { + self.yogaCalculatedLayout = layout; + } else { + layout = self.yogaCalculatedLayout; + ASYogaLog("-setupYogaCalculatedLayout: applying identical ASLayout: %@", layout); + } + + // Setup _pendingDisplayNodeLayout to reference the Yoga-calculated ASLayout, *unless* we are a leaf node. + // Leaf yoga nodes may have their own .sublayouts, if they use a layout spec (such as ASButtonNode). + // Their _pending variable is set after passing the Yoga checks at the start of -calculateLayoutThatFits: + + // For other Yoga nodes, there is no code that will set _pending unless we do it here. Why does it need to be set? + // When CALayer triggers the -[ASDisplayNode __layout] call, we will check if our current _pending layout + // has a size which matches our current bounds size. If it does, that layout will be used without recomputing it. + + // NOTE: Yoga does not make the constrainedSize available to intermediate nodes in the tree (e.g. not root or leaves). + // Although the size range provided here is not accurate, this will only affect caching of calls to layoutThatFits: + // These calls will behave as if they are not cached, starting a new Yoga layout pass, but this will tap into Yoga's + // own internal cache. + + if ([self shouldHaveYogaMeasureFunc] == NO) { + YGNodeRef parentNode = YGNodeGetParent(yogaNode); + CGSize parentSize = CGSizeZero; + if (parentNode) { + parentSize.width = YGNodeLayoutGetWidth(parentNode); + parentSize.height = YGNodeLayoutGetHeight(parentNode); + } + _pendingDisplayNodeLayout = std::make_shared(layout, ASSizeRangeUnconstrained, parentSize, 0); + } } -- (void)updateYogaMeasureFuncIfNeeded +- (BOOL)shouldHaveYogaMeasureFunc { // Size calculation via calculateSizeThatFits: or layoutSpecThatFits: // This will be used for ASTextNode, as well as any other node that has no Yoga children BOOL isLeafNode = (self.yogaChildren.count == 0); BOOL definesCustomLayout = [self implementsLayoutMethod]; + return (isLeafNode && definesCustomLayout); +} +- (void)updateYogaMeasureFuncIfNeeded +{ // We set the measure func only during layout. Otherwise, a cycle is created: // The YGNodeRef Context will retain the ASDisplayNode, which retains the style, which owns the YGNodeRef. - BOOL shouldHaveMeasureFunc = (isLeafNode && definesCustomLayout && checkFlag(YogaLayoutInProgress)); + BOOL shouldHaveMeasureFunc = ([self shouldHaveYogaMeasureFunc] && checkFlag(YogaLayoutInProgress)); ASLayoutElementYogaUpdateMeasureFunc(self.style.yogaNode, shouldHaveMeasureFunc ? self : nil); } - (void)invalidateCalculatedYogaLayout { - // Yoga internally asserts that this method may only be called on nodes with a measurement function. YGNodeRef yogaNode = self.style.yogaNode; if (yogaNode && YGNodeGetMeasureFunc(yogaNode)) { + // Yoga internally asserts that MarkDirty() may only be called on nodes with a measurement function. YGNodeMarkDirty(yogaNode); } self.yogaCalculatedLayout = nil; @@ -200,7 +249,7 @@ ASDisplayNode *yogaParent = self.yogaParent; if (yogaParent) { - ASYogaLog(@"ESCALATING to Yoga root: %@", self); + ASYogaLog("ESCALATING to Yoga root: %@", self); // TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually. [yogaParent calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained]; return; @@ -217,7 +266,7 @@ rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass]; } - ASYogaLog(@"CALCULATING at Yoga root with constraint = {%@, %@}: %@", + ASYogaLog("CALCULATING at Yoga root with constraint = {%@, %@}: %@", NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self); YGNodeRef rootYogaNode = self.style.yogaNode; @@ -258,7 +307,7 @@ NSLog(@"node = %@", node); NSLog(@"style = %@", node.style); NSLog(@"layout = %@", node.yogaCalculatedLayout); - YGNodePrint(node.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); + YGNodePrint(node.style.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); }); } #endif /* YOGA_LAYOUT_LOGGING */ diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index be5579a46e..b0ea2791b8 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -37,17 +37,17 @@ NS_ASSUME_NONNULL_BEGIN /** * UIView creation block. Used to create the backing view of a new display node. */ -typedef UIView * _Nonnull(^ASDisplayNodeViewBlock)(); +typedef UIView * _Nonnull(^ASDisplayNodeViewBlock)(void); /** * UIView creation block. Used to create the backing view of a new display node. */ -typedef UIViewController * _Nonnull(^ASDisplayNodeViewControllerBlock)(); +typedef UIViewController * _Nonnull(^ASDisplayNodeViewControllerBlock)(void); /** * CALayer creation block. Used to create the backing layer of a new display node. */ -typedef CALayer * _Nonnull(^ASDisplayNodeLayerBlock)(); +typedef CALayer * _Nonnull(^ASDisplayNodeLayerBlock)(void); /** * ASDisplayNode loaded callback block. This block is called BEFORE the -didLoad method and is always called on the main thread. @@ -101,6 +101,12 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState) ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible, }; +typedef NS_ENUM(NSInteger, ASCornerRoundingType) { + ASCornerRoundingTypeDefaultSlowCALayer, + ASCornerRoundingTypePrecomposited, + ASCornerRoundingTypeClipping +}; + /** * Default drawing priority for display node */ @@ -382,7 +388,6 @@ extern NSInteger const ASDefaultDrawingPriority; /** @name Drawing and Updating the View */ - /** * @abstract Whether this node's view performs asynchronous rendering. * @@ -638,6 +643,31 @@ extern NSInteger const ASDefaultDrawingPriority; @property (nonatomic, assign) CGPoint position; // default=CGPointZero @property (nonatomic, assign) CGFloat alpha; // default=1.0f +/* @abstract Sets the corner rounding method to use on the ASDisplayNode. + * There are three types of corner rounding provided by Texture: CALayer, Precomposited, and Clipping. + * + * - ASCornerRoundingTypeDefaultSlowCALayer: uses CALayer's inefficient .cornerRadius property. Use + * this type of corner in situations in which there is both movement through and movement underneath + * the corner (very rare). This uses only .cornerRadius. + * + * - ASCornerRoundingTypePrecomposited: corners are drawn using bezier paths to clip the content in a + * CGContext / UIGraphicsContext. This requires .backgroundColor and .cornerRadius to be set. Use opaque + * background colors when possible for optimal efficiency, but transparent colors are supported and much + * more efficient than CALayer. The only limitation of this approach is that it cannot clip children, and + * thus works best for ASImageNodes or containers showing a background around their children. + * + * - ASCornerRoundingTypeClipping: overlays 4 seperate opaque corners on top of the content that needs + * corner rounding. Requires .backgroundColor and .cornerRadius to be set. Use clip corners in situations + * in which is movement through the corner, with an opaque background (no movement underneath the corner). + * Clipped corners are ideal for animating / resizing views, and still outperform CALayer. + * + * For more information and examples, see http://texturegroup.org/docs/corner-rounding.html + * + * @default ASCornerRoundingTypeDefaultSlowCALayer + */ +@property (nonatomic, assign) ASCornerRoundingType cornerRoundingType; // default=Slow CALayer .cornerRadius (offscreen rendering) +@property (nonatomic, assign) CGFloat cornerRadius; // default=0.0 + @property (nonatomic, assign) BOOL clipsToBounds; // default==NO @property (nonatomic, getter=isHidden) BOOL hidden; // default==NO @property (nonatomic, getter=isOpaque) BOOL opaque; // default==YES @@ -650,7 +680,6 @@ extern NSInteger const ASDefaultDrawingPriority; @property (nonatomic, assign) CGPoint anchorPoint; // default={0.5, 0.5} @property (nonatomic, assign) CGFloat zPosition; // default=0.0 -@property (nonatomic, assign) CGFloat cornerRadius; // default=0.0 @property (nonatomic, assign) CATransform3D transform; // default=CATransform3DIdentity @property (nonatomic, assign) CATransform3D subnodeTransform; // default=CATransform3DIdentity @@ -680,7 +709,7 @@ extern NSInteger const ASDefaultDrawingPriority; */ @property (nonatomic, assign) UIViewContentMode contentMode; // default=UIViewContentModeScaleToFill @property (nonatomic, copy) NSString *contentsGravity; // Use .contentMode in preference when possible. -@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute; // default=Unspecified +@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); // default=Unspecified @property (nonatomic, nullable) CGColorRef shadowColor; // default=opaque rgb black @property (nonatomic, assign) CGFloat shadowOpacity; // default=0.0 @@ -723,8 +752,11 @@ extern NSInteger const ASDefaultDrawingPriority; // Accessibility support @property (nonatomic, assign) BOOL isAccessibilityElement; @property (nonatomic, copy, nullable) NSString *accessibilityLabel; +@property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedLabel API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, copy, nullable) NSString *accessibilityHint; +@property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedHint API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, copy, nullable) NSString *accessibilityValue; +@property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedValue API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, assign) UIAccessibilityTraits accessibilityTraits; @property (nonatomic, assign) CGRect accessibilityFrame; @property (nonatomic, copy, nullable) UIBezierPath *accessibilityPath; @@ -792,7 +824,7 @@ extern NSInteger const ASDefaultDrawingPriority; * @abstract Return the calculated size. * * @discussion Ideal for use by subclasses in -layout, having already prompted their subnodes to calculate their size by - * calling -measure: on them in -calculateLayoutThatFits. + * calling -layoutThatFits: on them in -calculateLayoutThatFits. * * @return Size already calculated by -calculateLayoutThatFits:. * @@ -855,7 +887,7 @@ extern NSInteger const ASDefaultDrawingPriority; - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated shouldMeasureAsync:(BOOL)shouldMeasureAsync - measurementCompletion:(nullable void(^)())completion; + measurementCompletion:(nullable void(^)(void))completion; /** @@ -872,7 +904,7 @@ extern NSInteger const ASDefaultDrawingPriority; */ - (void)transitionLayoutWithAnimation:(BOOL)animated shouldMeasureAsync:(BOOL)shouldMeasureAsync - measurementCompletion:(nullable void(^)())completion; + measurementCompletion:(nullable void(^)(void))completion; /** * @abstract Cancels all performing layout transitions. Can be called on any thread. diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 55be0bfb8c..e4b7927b8a 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -20,7 +20,6 @@ #import #import #import -#import #import #import #import @@ -63,7 +62,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; // We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 @protocol CALayerDelegate; -@interface ASDisplayNode () +@interface ASDisplayNode () /** * See ASDisplayNodeInternal.h for ivars @@ -77,19 +76,8 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; @synthesize threadSafeBounds = _threadSafeBounds; -static BOOL suppressesInvalidCollectionUpdateExceptions = NO; static std::atomic_bool storesUnflattenedLayouts = ATOMIC_VAR_INIT(NO); -+ (BOOL)suppressesInvalidCollectionUpdateExceptions -{ - return suppressesInvalidCollectionUpdateExceptions; -} - -+ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses -{ - suppressesInvalidCollectionUpdateExceptions = suppresses; -} - BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) { return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector); @@ -182,12 +170,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateSizeThatFits:))) { overrides |= ASDisplayNodeMethodOverrideCalcSizeThatFits; } - if (ASDisplayNodeSubclassOverridesSelector(c, @selector(fetchData))) { - overrides |= ASDisplayNodeMethodOverrideFetchData; - } - if (ASDisplayNodeSubclassOverridesSelector(c, @selector(clearFetchedData))) { - overrides |= ASDisplayNodeMethodOverrideClearFetchedData; - } return overrides; } @@ -202,8 +184,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedLayout)), @"Subclass %@ must not override calculatedLayout method.", classString); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measure:)), @"Subclass %@ must not override measure: method", classString); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measureWithSizeRange:)), @"Subclass %@ must not override measureWithSizeRange: method. Instead override calculateLayoutThatFits:", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:)), @"Subclass %@ must not override layoutThatFits: method. Instead override calculateLayoutThatFits:.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:parentSize:)), @"Subclass %@ must not override layoutThatFits:parentSize method. Instead override calculateLayoutThatFits:.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method.", classString); @@ -448,10 +428,19 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) for (Ivar ivar : ivars) { id value = object_getIvar(self, ivar); + if (value == nil) { + continue; + } + if (ASClassRequiresMainThreadDeallocation(object_getClass(value))) { as_log_debug(ASMainThreadDeallocationLog(), "%@: Trampolining ivar '%s' value %@ for main deallocation.", self, ivar_getName(ivar), value); - //NSLog(@"%@: Trampolining ivar '%s' value %@ for main deallocation.", self, ivar_getName(ivar), value); - ASPerformMainThreadDeallocation(value); + + // Before scheduling the ivar for main thread deallocation we have clear out the ivar, otherwise we can run + // into a race condition where the main queue is drained earlier than this node is deallocated and the ivar + // is still deallocated on a background thread + object_setIvar(self, ivar, nil); + + ASPerformMainThreadDeallocation(&value); } else { as_log_debug(ASMainThreadDeallocationLog(), "%@: Not trampolining ivar '%s' value %@.", self, ivar_getName(ivar), value); //NSLog(@"%@: Not trampolining ivar '%s' value %@.", self, ivar_getName(ivar), value); @@ -908,7 +897,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (CGRectEqualToRect(bounds, CGRectZero)) { // Performing layout on a zero-bounds view often results in frame calculations // with negative sizes after applying margins, which will cause - // measureWithSizeRange: on subnodes to assert. + // layoutThatFits: on subnodes to assert. as_log_debug(OS_LOG_DISABLED, "Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self); return; } @@ -923,7 +912,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // This method will confirm that the layout is up to date (and update if needed). // Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). - [self _locked_measureNodeWithBoundsIfNecessary:bounds]; + __instanceLock__.unlock(); + [self _u_measureNodeWithBoundsIfNecessary:bounds]; + __instanceLock__.lock(); [self _locked_layoutPlaceholderIfNecessary]; } @@ -934,6 +925,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (loaded) { ASPerformBlockOnMainThread(^{ [self layout]; + [self _layoutClipCornersIfNeeded]; [self layoutDidFinish]; }); } @@ -989,23 +981,30 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren. // - This node is a Yoga tree node: it has both a yogaParent and yogaChildren. // - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren. - // If we're a leaf node, we are probably being called by a measure function and proceed as normal. - // If we're a yoga root or tree node, initiate a new Yoga calculation pass from root. YGNodeRef yogaNode = _style.yogaNode; BOOL hasYogaParent = (_yogaParent != nil); BOOL hasYogaChildren = (_yogaChildren.count > 0); BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); - if (usesYoga && (_yogaParent == nil || _yogaChildren.count > 0)) { + if (usesYoga) { // This node has some connection to a Yoga tree. - ASDN::MutexUnlocker ul(__instanceLock__); - - if (self.yogaLayoutInProgress == NO) { - [self calculateLayoutFromYogaRoot:constrainedSize]; + if ([self shouldHaveYogaMeasureFunc] == NO) { + // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then + // initiate a new Yoga calculation pass from root. + ASDN::MutexUnlocker ul(__instanceLock__); + as_activity_create_for_scope("Yoga layout calculation"); + if (self.yogaLayoutInProgress == NO) { + ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize)); + [self calculateLayoutFromYogaRoot:constrainedSize]; + } else { + ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); + } + ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); + return _yogaCalculatedLayout; + } else { + // If we're a yoga leaf node with custom measurement function, proceed with normal layout so layoutSpecs can run (e.g. ASButtonNode). + ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); } - ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); - return _yogaCalculatedLayout; } - ASYogaLog(@"PROCEEDING past Yoga check to calculate ASLayout for: %@", self); #endif /* YOGA */ // Manual size calculation via calculateSizeThatFits: @@ -1480,6 +1479,147 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) }); } +- (void)_layoutClipCornersIfNeeded +{ + ASDisplayNodeAssertMainThread(); + if (_clipCornerLayers[0] == nil) { + return; + } + + CGSize boundsSize = self.bounds.size; + for (int idx = 0; idx < 4; idx++) { + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + if (_clipCornerLayers[idx]) { + // Note the Core Animation coordinates are reversed for y; 0 is at the bottom. + _clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? boundsSize.height : 0.0); + [_layer addSublayer:_clipCornerLayers[idx]]; + } + } +} + +- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor +{ + ASPerformBlockOnMainThread(^{ + for (int idx = 0; idx < 4; idx++) { + // Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left. + // anchorPoint is Bottom Left at 0,0 and Top Right at 1,1. + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + + CGSize size = CGSizeMake(radius + 1, radius + 1); + UIGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + if (isRight == YES) { + CGContextTranslateCTM(ctx, -radius + 1, 0); + } + if (isTop == YES) { + CGContextTranslateCTM(ctx, 0, -radius + 1); + } + UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius]; + [roundedRect setUsesEvenOddFillRule:YES]; + [roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]]; + [backgroundColor setFill]; + [roundedRect fill]; + + // No lock needed, as _clipCornerLayers is only modified on the main thread. + CALayer *clipCornerLayer = _clipCornerLayers[idx]; + clipCornerLayer.contents = (id)(UIGraphicsGetImageFromCurrentImageContext().CGImage); + clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height); + clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0); + + UIGraphicsEndImageContext(); + } + [self _layoutClipCornersIfNeeded]; + }); +} + +- (void)_setClipCornerLayersVisible:(BOOL)visible +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + if (visible) { + for (int idx = 0; idx < 4; idx++) { + if (_clipCornerLayers[idx] == nil) { + _clipCornerLayers[idx] = [[CALayer alloc] init]; + _clipCornerLayers[idx].zPosition = 99999; + _clipCornerLayers[idx].delegate = self; + } + } + [self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor]; + } else { + for (int idx = 0; idx < 4; idx++) { + [_clipCornerLayers[idx] removeFromSuperlayer]; + _clipCornerLayers[idx] = nil; + } + } + }); +} + +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius +{ + __instanceLock__.lock(); + CGFloat oldCornerRadius = _cornerRadius; + ASCornerRoundingType oldRoundingType = _cornerRoundingType; + + _cornerRadius = newCornerRadius; + _cornerRoundingType = newRoundingType; + __instanceLock__.unlock(); + + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) { + if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + if (newRoundingType == ASCornerRoundingTypePrecomposited) { + self.layerCornerRadius = 0.0; + if (oldCornerRadius > 0.0) { + [self displayImmediately]; + } else { + [self setNeedsDisplay]; // Async display is OK if we aren't replacing an existing .cornerRadius. + } + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + self.layerCornerRadius = 0.0; + [self _setClipCornerLayersVisible:YES]; + } else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + } + } + else if (oldRoundingType == ASCornerRoundingTypePrecomposited) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + // Corners are already precomposited, but the radius has changed. + // Default to async re-display. The user may force a synchronous display if desired. + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + [self _setClipCornerLayersVisible:YES]; + [self setNeedsDisplay]; + } + } + else if (oldRoundingType == ASCornerRoundingTypeClipping) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self _setClipCornerLayersVisible:NO]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + [self _setClipCornerLayersVisible:NO]; + [self displayImmediately]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + // Clip corners already exist, but the radius has changed. + [self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor]; + } + } + } + }); +} + - (void)recursivelySetDisplaySuspended:(BOOL)flag { _recursivelySetDisplaySuspended(self, nil, flag); @@ -1555,7 +1695,11 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, - (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer asynchronously:(BOOL)asynchronously { // Subclass hook. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self displayWillStart]; +#pragma clang diagnostic pop + [self displayWillStartAsynchronously:asynchronously]; } @@ -1568,7 +1712,6 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, - (void)displayWillStart {} - (void)displayWillStartAsynchronously:(BOOL)asynchronously { - [self displayWillStart]; // Subclass override ASDisplayNodeAssertMainThread(); ASDisplayNodeLogEvent(self, @"displayWillStart"); @@ -2620,7 +2763,8 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { } } - ASDisplayNodeLogEvent(self, @"setHierarchyState: oldState = %@, newState = %@", NSStringFromASHierarchyState(oldState), NSStringFromASHierarchyState(newState)); + ASDisplayNodeLogEvent(self, @"setHierarchyState: %@", NSStringFromASHierarchyStateChange(oldState, newState)); + as_log_verbose(ASNodeLog(), "%s%@ %@", sel_getName(_cmd), NSStringFromASHierarchyStateChange(oldState, newState), self); } - (void)willEnterHierarchy @@ -2936,14 +3080,21 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); - [_interfaceStateDelegate didEnterPreloadState]; - - if (_methodOverrides & ASDisplayNodeMethodOverrideFetchData) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [self fetchData]; -#pragma clang diagnostic pop + + if (self.automaticallyManagesSubnodes) { + // Tell the node to apply its applicable pending layout, if any, so that its subnodes are inserted/deleted + // and start preloading right away. + // + // If this node has an up-to-date layout (and subnodes), calling layoutIfNeeded will be fast. + // + // If this node doesn't have a calculated or pending layout that fits its current bounds, a measurement pass will occur + // (see __layout and _u_measureNodeWithBoundsIfNecessary:). + // This scenario should be uncommon, and running a measurement pass here is a fine trade-off because preloading + // any time after this point would be late. + [self layoutIfNeeded]; } + + [_interfaceStateDelegate didEnterPreloadState]; } - (void)didExitPreloadState @@ -2951,13 +3102,6 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); [_interfaceStateDelegate didExitPreloadState]; - - if (_methodOverrides & ASDisplayNodeMethodOverrideClearFetchedData) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [self clearFetchedData]; -#pragma clang diagnostic pop - } } - (void)clearContents @@ -3507,101 +3651,3 @@ static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; } @end - -#pragma mark - ASDisplayNode (Deprecated) - -@implementation ASDisplayNode (Deprecated) - -- (NSString *)name -{ - return self.debugName; -} - -- (void)setName:(NSString *)name -{ - self.debugName = name; -} - -- (void)setPreferredFrameSize:(CGSize)preferredFrameSize -{ - // Deprecated preferredFrameSize just calls through to set width and height - self.style.preferredSize = preferredFrameSize; - [self setNeedsLayout]; -} - -- (CGSize)preferredFrameSize -{ - ASLayoutSize size = self.style.preferredLayoutSize; - BOOL isPoints = (size.width.unit == ASDimensionUnitPoints && size.height.unit == ASDimensionUnitPoints); - return isPoints ? CGSizeMake(size.width.value, size.height.value) : CGSizeZero; -} - -- (BOOL)usesImplicitHierarchyManagement -{ - return self.automaticallyManagesSubnodes; -} - -- (void)setUsesImplicitHierarchyManagement:(BOOL)enabled -{ - self.automaticallyManagesSubnodes = enabled; -} - -- (CGSize)measure:(CGSize)constrainedSize -{ - return [self layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; -} - -ASLayoutElementStyleForwarding - -- (void)visibilityDidChange:(BOOL)isVisible -{ - if (isVisible) { - [self didEnterVisibleState]; - } else { - [self didExitVisibleState]; - } -} - -- (void)visibleStateDidChange:(BOOL)isVisible -{ - if (isVisible) { - [self didEnterVisibleState]; - } else { - [self didExitVisibleState]; - } -} - -- (void)displayStateDidChange:(BOOL)inDisplayState -{ - if (inDisplayState) { - [self didEnterVisibleState]; - } else { - [self didExitVisibleState]; - } -} - -- (void)loadStateDidChange:(BOOL)inLoadState -{ - if (inLoadState) { - [self didEnterPreloadState]; - } else { - [self didExitPreloadState]; - } -} - -- (void)fetchData -{ - // subclass override -} - -- (void)clearFetchedData -{ - // subclass override -} - -- (void)cancelLayoutTransitionsInProgress -{ - [self cancelLayoutTransition]; -} - -@end diff --git a/Source/ASDisplayNodeExtras.h b/Source/ASDisplayNodeExtras.h index a5cc344bcb..ba0a38172c 100644 --- a/Source/ASDisplayNodeExtras.h +++ b/Source/ASDisplayNodeExtras.h @@ -35,13 +35,7 @@ #endif /// For deallocation of objects on the main thread across multiple run loops. -#ifdef __cplusplus -extern "C" { -#endif -extern void ASPerformMainThreadDeallocation(_Nullable id object); -#ifdef __cplusplus -} -#endif +extern void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr); // Because inline methods can't be extern'd and need to be part of the translation unit of code // that compiles with them to actually inline, we both declare and define these in the header. @@ -214,8 +208,8 @@ extern __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnode(ASDispla */ extern __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT; -extern UIColor *ASDisplayNodeDefaultPlaceholderColor() AS_WARN_UNUSED_RESULT; -extern UIColor *ASDisplayNodeDefaultTintColor() AS_WARN_UNUSED_RESULT; +extern UIColor *ASDisplayNodeDefaultPlaceholderColor(void) AS_WARN_UNUSED_RESULT; +extern UIColor *ASDisplayNodeDefaultTintColor(void) AS_WARN_UNUSED_RESULT; /** Disable willAppear / didAppear / didDisappear notifications for a sub-hierarchy, then re-enable when done. Nested calls are supported. diff --git a/Source/ASDisplayNodeExtras.mm b/Source/ASDisplayNodeExtras.mm index 279687662a..2839e1a6cf 100644 --- a/Source/ASDisplayNodeExtras.mm +++ b/Source/ASDisplayNodeExtras.mm @@ -23,8 +23,7 @@ #import #import -extern void ASPerformMainThreadDeallocation(_Nullable id object) -{ +extern void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr) { /** * UIKit components must be deallocated on the main thread. We use this shared * run loop queue to gradually deallocate them across many turns of the main run loop. @@ -35,8 +34,14 @@ extern void ASPerformMainThreadDeallocation(_Nullable id object) queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil]; queue.batchSize = 10; }); - if (object != nil) { - [queue enqueue:object]; + + if (objectPtr != NULL && *objectPtr != nil) { + // Lock queue while enqueuing and releasing, so that there's no risk + // that the queue will release before we get a chance to release. + [queue lock]; + [queue enqueue:*objectPtr]; // Retain, +1 + *objectPtr = nil; // Release, +0 + [queue unlock]; // (After queue drains), release, -1 } } diff --git a/Source/ASEditableTextNode.mm b/Source/ASEditableTextNode.mm index e7b332bd07..2e63d6b3b4 100644 --- a/Source/ASEditableTextNode.mm +++ b/Source/ASEditableTextNode.mm @@ -75,7 +75,7 @@ See issue: https://github.com/facebook/AsyncDisplayKit/issues/1063 */ -@interface ASPanningOverriddenUITextView : UITextView +@interface ASPanningOverriddenUITextView : ASTextKitComponentsTextView { BOOL _shouldBlockPanGesture; } @@ -183,13 +183,6 @@ return self; } -- (void)dealloc -{ - _textKitComponents.textView.delegate = nil; - _textKitComponents.layoutManager.delegate = nil; - _placeholderTextKitComponents.layoutManager.delegate = nil; -} - #pragma mark - ASDisplayNode Overrides - (void)didLoad { @@ -228,7 +221,7 @@ ASDN::MutexLocker l(_textKitLock); // Create and configure the placeholder text view. - _placeholderTextKitComponents.textView = [[UITextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; + _placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; _placeholderTextKitComponents.textView.userInteractionEnabled = NO; _placeholderTextKitComponents.textView.accessibilityElementsHidden = YES; configureTextView(_placeholderTextKitComponents.textView); diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index e681c9d0d9..8443779945 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -20,6 +20,7 @@ #import #import #import +#import #import #import #import @@ -45,7 +46,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setAnimatedImage:(id )animatedImage { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_setAnimatedImage:animatedImage]; } @@ -56,6 +57,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; } id previousAnimatedImage = _animatedImage; + _animatedImage = animatedImage; if (animatedImage != nil) { @@ -67,17 +69,25 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; }; } + animatedImage.playbackReadyCallback = ^{ + // In this case the lock is already gone we have to call the unlocked version therefore + [weakSelf setShouldAnimate:YES]; + }; if (animatedImage.playbackReady) { [self _locked_setShouldAnimate:YES]; - } else { - animatedImage.playbackReadyCallback = ^{ - // In this case the lock is already gone we have to call the unlocked version therefore - [weakSelf setShouldAnimate:YES]; - }; } + } else { + // Clean up after ourselves. + self.contents = nil; + [self setCoverImage:nil]; } [self animatedImageSet:_animatedImage previousAnimatedImage:previousAnimatedImage]; + + // Animated image can take while to dealloc, do it off the main queue + if (previousAnimatedImage != nil) { + ASPerformBackgroundDeallocation(&previousAnimatedImage); + } } - (void)animatedImageSet:(id )newAnimatedImage previousAnimatedImage:(id )previousAnimatedImage @@ -87,13 +97,13 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (id )animatedImage { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); return _animatedImage; } - (void)setAnimatedImagePaused:(BOOL)animatedImagePaused { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); _animatedImagePaused = animatedImagePaused; @@ -102,14 +112,16 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (BOOL)animatedImagePaused { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); return _animatedImagePaused; } - (void)setCoverImageCompleted:(UIImage *)coverImage { - ASDN::MutexLocker l(_animatedImageLock); - [self _locked_setCoverImageCompleted:coverImage]; + if (ASInterfaceStateIncludesDisplay(self.interfaceState)) { + ASDN::MutexLocker l(__instanceLock__); + [self _locked_setCoverImageCompleted:coverImage]; + } } - (void)_locked_setCoverImageCompleted:(UIImage *)coverImage @@ -125,7 +137,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setCoverImage:(UIImage *)coverImage { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_setCoverImage:coverImage]; } @@ -136,8 +148,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; #ifndef MINIMAL_ASDK if ([self isKindOfClass:[ASNetworkImageNode class]]) { [(ASNetworkImageNode *)self _locked_setDefaultImage:coverImage]; - } else { -#endif + } else if (_displayLink == nil || _displayLink.paused == YES) { [self _locked_setImage:coverImage]; #ifndef MINIMAL_ASDK } @@ -167,7 +178,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setShouldAnimate:(BOOL)shouldAnimate { - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_setShouldAnimate:shouldAnimate]; } @@ -200,7 +211,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_startAnimating]; } @@ -223,11 +234,14 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; NSLog(@"starting animation: %p", self); #endif + // Get frame interval before holding display link lock to avoid deadlock + NSUInteger frameInterval = self.animatedImage.frameInterval; ASDN::MutexLocker l(_displayLinkLock); if (_displayLink == nil) { _playHead = 0; _displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)]; - _displayLink.frameInterval = self.animatedImage.frameInterval; + _displayLink.frameInterval = frameInterval; + _lastSuccessfulFrameIndex = NSUIntegerMax; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; } else { @@ -239,7 +253,7 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(_animatedImageLock); + ASDN::MutexLocker l(__instanceLock__); [self _locked_stopAnimating]; } @@ -268,7 +282,9 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; if (self.animatedImage.coverImageReady) { [self setCoverImage:self.animatedImage.coverImage]; } - [self startAnimating]; + if (self.animatedImage.playbackReady) { + [self startAnimating]; + } } - (void)didExitVisibleState @@ -279,6 +295,26 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; [self stopAnimating]; } +- (void)didExitDisplayState +{ + ASDisplayNodeAssertMainThread(); +#if ASAnimatedImageDebug + NSLog(@"exiting display state: %p", self); +#endif + + // Check to see if we're an animated image before calling super in case someone + // decides they want to clear out the animatedImage itself on exiting the display + // state + BOOL isAnimatedImage = self.animatedImage != nil; + [super didExitDisplayState]; + + // Also clear out the contents we've set to be good citizens, we'll put it back in when we become visible. + if (isAnimatedImage) { + self.contents = nil; + [self setCoverImage:nil]; + } +} + #pragma mark - Display Link Callbacks - (void)displayLinkFired:(CADisplayLink *)displayLink @@ -288,6 +324,8 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; CFTimeInterval timeBetweenLastFire; if (self.lastDisplayLinkFire == 0) { timeBetweenLastFire = 0; + } else if (AS_AVAILABLE_IOS(10)){ + timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp; } else { timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire; } @@ -296,7 +334,8 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; _playHead += timeBetweenLastFire; while (_playHead > self.animatedImage.totalDuration) { - _playHead -= self.animatedImage.totalDuration; + // Set playhead to zero to keep from showing different frames on different playthroughs + _playHead = 0; _playedLoops++; } @@ -306,15 +345,18 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; } NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead]; + if (frameIndex == _lastSuccessfulFrameIndex) { + return; + } CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex]; if (frameImage == nil) { - _playHead -= timeBetweenLastFire; //Pause the display link until we get a file ready notification displayLink.paused = YES; self.lastDisplayLinkFire = 0; } else { self.contents = (__bridge id)frameImage; + _lastSuccessfulFrameIndex = frameIndex; [self displayDidFinish]; } } diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index 1861199a7d..b38f5e156b 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -42,6 +42,8 @@ #include +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); @interface ASImageNodeDrawParameters : NSObject { @@ -249,11 +251,11 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); if (ASObjectIsEqual(_image, image)) { return; } - + + UIImage *oldImage = _image; _image = image; if (image != nil) { - // We explicitly call setNeedsDisplay in this case, although we know setNeedsDisplay will be called with lock held. // Therefore we have to be careful in methods that are involved with setNeedsDisplay to not run into a deadlock [self setNeedsDisplay]; @@ -267,10 +269,19 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); } return; } - } else { self.contents = nil; } + + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + CGSize oldImageSize = oldImage.size; + BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width + || oldImageSize.height > kMinReleaseImageOnBackgroundSize.height; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(&oldImage); + } } - (UIImage *)image @@ -437,12 +448,13 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); } static ASWeakMap *cache = nil; -static ASDN::Mutex cacheLock; +// Allocate cacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& cacheLock = *new ASDN::StaticMutex; + (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled { { - ASDN::MutexLocker l(cacheLock); + ASDN::StaticMutexLocker l(cacheLock); if (!cache) { cache = [[ASWeakMap alloc] init]; } @@ -459,7 +471,7 @@ static ASDN::Mutex cacheLock; } { - ASDN::MutexLocker l(cacheLock); + ASDN::StaticMutexLocker l(cacheLock); return [cache setObject:contents forKey:key]; } } diff --git a/Source/ASMultiplexImageNode.mm b/Source/ASMultiplexImageNode.mm index 7df6351234..2043d1d134 100644 --- a/Source/ASMultiplexImageNode.mm +++ b/Source/ASMultiplexImageNode.mm @@ -261,11 +261,11 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent return (self.image == nil && self.animatedImage == nil && self.imageIdentifiers.count > 0); } -/* displayWillStart in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary +/* displayWillStartAsynchronously in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary in ASNetworkImageNode as well. */ -- (void)displayWillStart +- (void)displayWillStartAsynchronously:(BOOL)asynchronously { - [super displayWillStart]; + [super displayWillStartAsynchronously:asynchronously]; [self didEnterPreloadState]; @@ -532,10 +532,10 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent CGSize imageSize = image.size; BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || imageSize.height > kMinReleaseImageOnBackgroundSize.height; - if (shouldReleaseImageOnBackgroundThread) { - ASPerformBackgroundDeallocation(image); - } [self _setImage:nil]; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(&image); + } } #pragma mark - diff --git a/Source/ASNetworkImageNode.h b/Source/ASNetworkImageNode.h index a19ba8d4cc..33220d9e5e 100644 --- a/Source/ASNetworkImageNode.h +++ b/Source/ASNetworkImageNode.h @@ -84,6 +84,18 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, strong, readwrite) NSURL *URL; +/** + * An array of URLs of increasing cost to download. + * + * @discussion By setting an array of URLs, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. + * + * @deprecated This API has been removed for now due to the increased complexity to the class that it brought. + * Please use .URL instead. + */ +@property (nullable, nonatomic, strong, readwrite) NSArray *URLs ASDISPLAYNODE_DEPRECATED_MSG("Please use URL instead."); + /** * Download and display a new image. * @@ -123,6 +135,21 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - + +typedef NS_ENUM(NSInteger, ASNetworkImageSource) { + ASNetworkImageSourceUnspecified = 0, + ASNetworkImageSourceSynchronousCache, + ASNetworkImageSourceAsynchronousCache, + ASNetworkImageSourceFileURL, + ASNetworkImageSourceDownload, +}; + +/// A struct that carries details about ASNetworkImageNode's image loads. +typedef struct { + /// The source from which the image was loaded. + ASNetworkImageSource imageSource; +} ASNetworkImageNodeDidLoadInfo; + /** * The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to * notifications such as finished decoding and downloading an image. @@ -130,6 +157,18 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASNetworkImageNodeDelegate @optional +/** + * Notification that the image node finished downloading an image, with additional info. + * If implemented, this method will be called instead of `imageNode:didLoadImage:`. + * + * @param imageNode The sender. + * @param image The newly-loaded image. + * @param info Misc information about the image load. + * + * @discussion Called on a background queue. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageNodeDidLoadInfo)info; + /** * Notification that the image node finished downloading an image. * diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index e933a602ae..9603561d44 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -33,8 +33,6 @@ #import #endif -static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - @interface ASNetworkImageNode () { // Only access any of these with __instanceLock__. @@ -59,6 +57,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; unsigned int delegateDidFailWithError:1; unsigned int delegateDidFinishDecoding:1; unsigned int delegateDidLoadImage:1; + unsigned int delegateDidLoadImageWithInfo:1; } _delegateFlags; @@ -157,6 +156,18 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; [super _locked_setImage:image]; } +// Deprecated +- (void)setURLs:(NSArray *)URLs +{ + [self setURL:[URLs firstObject]]; +} + +// Deprecated +- (NSArray *)URLs +{ + return @[self.URL]; +} + - (void)setURL:(NSURL *)URL { [self setURL:URL resetToDefault:YES]; @@ -281,6 +292,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _delegateFlags.delegateDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)]; _delegateFlags.delegateDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)]; _delegateFlags.delegateDidLoadImage = [delegate respondsToSelector:@selector(imageNode:didLoadImage:)]; + _delegateFlags.delegateDidLoadImageWithInfo = [delegate respondsToSelector:@selector(imageNode:didLoadImage:info:)]; } - (id)delegate @@ -314,7 +326,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; return (self.image == nil && self.animatedImage == nil && _URL != nil); } -/* displayWillStart in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary +/* displayWillStartAsynchronously: in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary in ASMultiplexImageNode as well. */ - (void)displayWillStartAsynchronously:(BOOL)asynchronously { @@ -328,8 +340,18 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (result) { [self _locked_setCurrentImageQuality:1.0]; [self _locked__setImage:result]; - _imageLoaded = YES; + + // Call out to the delegate. + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker l(__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = ASNetworkImageSourceSynchronousCache; + [_delegate imageNode:self didLoadImage:result info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { + ASDN::MutexUnlocker l(__instanceLock__); + [_delegate imageNode:self didLoadImage:result]; + } } } } @@ -492,21 +514,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { [self _locked_cancelImageDownloadWithResumePossibility:storeResume]; - // Destruction of bigger images on the main thread can be expensive - // and can take some time, so we dispatch onto a bg queue to - // actually dealloc. - UIImage *image = [self _locked_Image]; - UIImage *defaultImage = _defaultImage; - CGSize imageSize = image.size; - BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || - imageSize.height > kMinReleaseImageOnBackgroundSize.height; - if (shouldReleaseImageOnBackgroundThread) { - ASPerformBackgroundDeallocation(image); - } - [self _locked_setAnimatedImage:nil]; [self _locked_setCurrentImageQuality:0.0]; - [self _locked__setImage:defaultImage]; + [self _locked__setImage:_defaultImage]; _imageLoaded = NO; @@ -658,53 +668,82 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; [self _locked_setCurrentImageQuality:1.0]; - if (_delegateFlags.delegateDidLoadImage) { + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker u(__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = ASNetworkImageSourceFileURL; + [delegate imageNode:self didLoadImage:self.image info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { ASDN::MutexUnlocker u(__instanceLock__); [delegate imageNode:self didLoadImage:self.image]; } }); } else { __weak __typeof__(self) weakSelf = self; - auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier) { - - __typeof__(self) strongSelf = weakSelf; - if (strongSelf == nil) { - return; - } - - as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); - - // Grab the lock for the rest of the block - ASDN::MutexLocker l(strongSelf->__instanceLock__); - - //Getting a result back for a different download identifier, download must not have been successfully canceled - if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSource imageSource) { + ASPerformBlockOnBackgroundThread(^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { return; - } - - if (imageContainer != nil) { - [strongSelf _locked_setCurrentImageQuality:1.0]; - if ([imageContainer asdk_animatedImageData] && strongSelf->_downloaderFlags.downloaderImplementsAnimatedImage) { - id animatedImage = [strongSelf->_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]]; - [strongSelf _locked_setAnimatedImage:animatedImage]; - } else { - [strongSelf _locked__setImage:[imageContainer asdk_image]]; } - strongSelf->_imageLoaded = YES; - } - - strongSelf->_downloadIdentifier = nil; - strongSelf->_cacheUUID = nil; - - if (imageContainer != nil) { - if (strongSelf->_delegateFlags.delegateDidLoadImage) { - ASDN::MutexUnlocker u(strongSelf->__instanceLock__); - [delegate imageNode:strongSelf didLoadImage:strongSelf.image]; + + as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); + + // Grab the lock for the rest of the block + ASDN::MutexLocker l(strongSelf->__instanceLock__); + + //Getting a result back for a different download identifier, download must not have been successfully canceled + if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + return; } - } else if (error && strongSelf->_delegateFlags.delegateDidFailWithError) { - ASDN::MutexUnlocker u(strongSelf->__instanceLock__); - [delegate imageNode:strongSelf didFailWithError:error]; - } + + //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) + if (ASInterfaceStateIncludesPreload(self->_interfaceState) == NO) { + self->_downloadIdentifier = nil; + self->_cacheUUID = nil; + return; + } + + if (imageContainer != nil) { + [strongSelf _locked_setCurrentImageQuality:1.0]; + NSData *animatedImageData = [imageContainer asdk_animatedImageData]; + if (animatedImageData && strongSelf->_downloaderFlags.downloaderImplementsAnimatedImage) { + id animatedImage = [strongSelf->_downloader animatedImageWithData:animatedImageData]; + [strongSelf _locked_setAnimatedImage:animatedImage]; + } else { + [strongSelf _locked__setImage:[imageContainer asdk_image]]; + } + strongSelf->_imageLoaded = YES; + } + + strongSelf->_downloadIdentifier = nil; + strongSelf->_cacheUUID = nil; + + ASPerformBlockOnMainThread(^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + // Grab the lock for the rest of the block + ASDN::MutexLocker l(strongSelf->__instanceLock__); + + if (imageContainer != nil) { + if (strongSelf->_delegateFlags.delegateDidLoadImageWithInfo) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + ASNetworkImageNodeDidLoadInfo info = {}; + info.imageSource = imageSource; + [delegate imageNode:strongSelf didLoadImage:strongSelf.image info:info]; + } else if (strongSelf->_delegateFlags.delegateDidLoadImage) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + [delegate imageNode:strongSelf didLoadImage:strongSelf.image]; + } + } else if (error && strongSelf->_delegateFlags.delegateDidFailWithError) { + ASDN::MutexUnlocker u(strongSelf->__instanceLock__); + [delegate imageNode:strongSelf didFailWithError:error]; + } + }); + }); }; // As the _cache and _downloader is only set once in the intializer we don't have to use a @@ -716,27 +755,33 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; __instanceLock__.unlock(); as_log_verbose(ASImageLoadingLog(), "Decaching image for %@ url: %@", self, URL); + + ASImageCacherCompletion completion = ^(id imageContainer) { + // If the cache UUID changed, that means this request was cancelled. + __instanceLock__.lock(); + NSUUID *currentCacheUUID = _cacheUUID; + __instanceLock__.unlock(); + + if (!ASObjectIsEqual(currentCacheUUID, cacheUUID)) { + return; + } + + if ([imageContainer asdk_image] == nil && _downloader != nil) { + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); + }]; + } else { + as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); + finished(imageContainer, nil, nil, ASNetworkImageSourceAsynchronousCache); + } + }; [_cache cachedImageWithURL:URL callbackQueue:dispatch_get_main_queue() - completion:^(id imageContainer) { - // If the cache UUID changed, that means this request was cancelled. - __instanceLock__.lock(); - NSUUID *currentCacheUUID = _cacheUUID; - __instanceLock__.unlock(); - - if (!ASObjectIsEqual(currentCacheUUID, cacheUUID)) { - return; - } - - if ([imageContainer asdk_image] == nil && _downloader != nil) { - [self _downloadImageWithCompletion:finished]; - } else { - as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); - finished(imageContainer, nil, nil); - } - }]; + completion:completion]; } else { - [self _downloadImageWithCompletion:finished]; + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload); + }]; } } } diff --git a/Source/ASPagerNode.h b/Source/ASPagerNode.h index cd227a8b51..60db06c22e 100644 --- a/Source/ASPagerNode.h +++ b/Source/ASPagerNode.h @@ -63,18 +63,6 @@ NS_ASSUME_NONNULL_BEGIN @end @protocol ASPagerDelegate - -@optional - -/** - * Provides the constrained size range for measuring the node at the index. - * - * @param pagerNode The sender. - * @param index The index of the node. - * @return A constrained size range for layout the node at this index. - */ -- (ASSizeRange)pagerNode:(ASPagerNode *)pagerNode constrainedSizeForNodeAtIndex:(NSInteger)index ASDISPLAYNODE_DEPRECATED_MSG("Pages in a pager node should be the exact size of the collection node (default behavior)."); - @end /** diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index aa9852d506..8255a8d2e2 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -40,9 +40,6 @@ } _pagerDataSourceFlags; __weak id _pagerDelegate; - struct { - unsigned constrainedSizeForNode:1; - } _pagerDelegateFlags; ASPagerNodeProxy *_proxyDelegate; } @@ -119,7 +116,7 @@ - (CGSize)pageSize { - UIEdgeInsets contentInset = self.view.contentInset; + UIEdgeInsets contentInset = self.contentInset; CGSize pageSize = self.bounds.size; pageSize.height -= (contentInset.top + contentInset.bottom); return pageSize; @@ -149,7 +146,7 @@ #pragma mark - ASCollectionGalleryLayoutPropertiesProviding -- (CGSize)sizeForElements:(ASElementMap *)elements +- (CGSize)galleryLayoutDelegate:(nonnull ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(nonnull ASElementMap *)elements { ASDisplayNodeAssertMainThread(); return [self pageSize]; @@ -182,13 +179,6 @@ - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (_pagerDelegateFlags.constrainedSizeForNode) { - return [_pagerDelegate pagerNode:self constrainedSizeForNodeAtIndex:indexPath.item]; - } -#pragma clang diagnostic pop - return ASSizeRangeMake([self pageSize]); } @@ -221,15 +211,7 @@ { if (delegate != _pagerDelegate) { _pagerDelegate = delegate; - - if (delegate == nil) { - memset(&_pagerDelegateFlags, 0, sizeof(_pagerDelegateFlags)); - } else { - _pagerDelegateFlags.constrainedSizeForNode = [_pagerDelegate respondsToSelector:@selector(pagerNode:constrainedSizeForNodeAtIndex:)]; - } - _proxyDelegate = delegate ? [[ASPagerNodeProxy alloc] initWithTarget:delegate interceptor:self] : nil; - super.delegate = (id )_proxyDelegate; } } diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index 7ff563f409..50a0eeb249 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED -@interface ASRunLoopQueue : NSObject +@interface ASRunLoopQueue : NSObject /** * Create a new queue with the given run loop and handler. @@ -41,6 +41,8 @@ AS_SUBCLASSING_RESTRICTED - (void)enqueue:(ObjectType)object; +@property (nonatomic, readonly) BOOL isEmpty; + @property (nonatomic, assign) NSUInteger batchSize; // Default == 1. @property (nonatomic, assign) BOOL ensureExclusiveMembership; // Default == YES. Set-like behavior. @@ -49,11 +51,11 @@ AS_SUBCLASSING_RESTRICTED AS_SUBCLASSING_RESTRICTED @interface ASDeallocQueue : NSObject -+ (instancetype)sharedDeallocationQueue; +@property (class, atomic, readonly) ASDeallocQueue *sharedDeallocationQueue; - (void)test_drain; -- (void)releaseObjectInBackground:(id)object; +- (void)releaseObjectInBackground:(id __strong _Nullable * _Nonnull)objectPtr; @end diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 004d4abb97..ced7982529 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -45,7 +45,7 @@ static void runLoopSourceCallback(void *info) { ASDN::RecursiveMutex _queueLock; } -+ (instancetype)sharedDeallocationQueue ++ (ASDeallocQueue *)sharedDeallocationQueue { static ASDeallocQueue *deallocQueue = nil; static dispatch_once_t onceToken; @@ -55,16 +55,18 @@ static void runLoopSourceCallback(void *info) { return deallocQueue; } -- (void)releaseObjectInBackground:(id)object +- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr { // Disable background deallocation on iOS 8 and below to avoid crashes related to UIAXDelegateClearer (#2767). if (!AS_AT_LEAST_IOS9) { return; } - _queueLock.lock(); - _queue.push_back(object); - _queueLock.unlock(); + if (objectPtr != NULL && *objectPtr != nil) { + ASDN::MutexLocker l(_queueLock); + _queue.push_back(*objectPtr); + *objectPtr = nil; + } } - (void)threadMain @@ -348,12 +350,12 @@ typedef enum { { ASDN::MutexLocker l(_internalQueueLock); - // Early-exit if the queue is empty. NSInteger internalQueueCount = _internalQueue.count; + // Early-exit if the queue is empty. if (internalQueueCount == 0) { return; } - + ASSignpostStart(ASSignpostRunLoopQueueBatch); // Snatch the next batch of items. @@ -382,6 +384,14 @@ typedef enum { } } + if (foundItemCount == 0) { + // If _internalQueue holds weak references, and all of them just become NULL, then the array + // is never marked as needsCompletion, and compact will return early, not removing the NULL's. + // Inserting a NULL here ensures the compaction will take place. + // See http://www.openradar.me/15396578 and https://stackoverflow.com/a/40274426/1136669 + [_internalQueue addPointer:NULL]; + } + [_internalQueue compact]; if (_internalQueue.count == 0) { isQueueDrained = YES; @@ -434,10 +444,28 @@ typedef enum { if (!foundObject) { [_internalQueue addPointer:(__bridge void *)object]; - + CFRunLoopSourceSignal(_runLoopSource); CFRunLoopWakeUp(_runLoop); } } +- (BOOL)isEmpty +{ + ASDN::MutexLocker l(_internalQueueLock); + return _internalQueue.count == 0; +} + +#pragma mark - NSLocking + +- (void)lock +{ + _internalQueueLock.lock(); +} + +- (void)unlock +{ + _internalQueueLock.unlock(); +} + @end diff --git a/Source/ASScrollNode.mm b/Source/ASScrollNode.mm index 7d1beaa3d2..3ff0088095 100644 --- a/Source/ASScrollNode.mm +++ b/Source/ASScrollNode.mm @@ -99,10 +99,12 @@ // To understand this code, imagine we're containing a horizontal stack set within a vertical table node. // Our parentSize is fixed ~375pt width, but 0 - INF height. Our stack measures 1000pt width, 50pt height. // In this case, we want our scrollNode.bounds to be 375pt wide, and 50pt high. ContentSize 1000pt, 50pt. - // We can achieve this behavior by: 1. Always set contentSize to layout.size. 2. Set bounds to parentSize, + // We can achieve this behavior by: + // 1. Always set contentSize to layout.size. + // 2. Set bounds to a size that is calculated by clamping parentSize against constrained size, // unless one dimension is not defined, in which case adopt the contentSize for that dimension. _contentCalculatedSizeFromLayout = layout.size; - CGSize selfSize = parentSize; + CGSize selfSize = ASSizeRangeClamp(constrainedSize, parentSize); if (ASPointsValidForLayout(selfSize.width) == NO) { selfSize.width = _contentCalculatedSizeFromLayout.width; } @@ -161,7 +163,10 @@ - (void)setScrollableDirections:(ASScrollDirection)scrollableDirections { ASDN::MutexLocker l(__instanceLock__); - _scrollableDirections = scrollableDirections; + if (_scrollableDirections != scrollableDirections) { + _scrollableDirections = scrollableDirections; + [self setNeedsLayout]; + } } @end diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index 110e3acd2f..df1a4af76b 100644 --- a/Source/ASTableNode.h +++ b/Source/ASTableNode.h @@ -55,6 +55,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL inverted; +/** + * The distance that the content view is inset from the table node edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset; + /** * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. */ @@ -167,7 +172,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UITableView's version. */ -- (void)reloadDataWithCompletion:(nullable void (^)())completion; +- (void)reloadDataWithCompletion:(nullable void (^)(void))completion; /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -193,7 +198,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Perform a batch of updates asynchronously with animations in the batch. This method must be called from the main thread. @@ -204,7 +209,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Returns YES if the ASCollectionNode is still processing changes from performBatchUpdates:. @@ -233,7 +238,7 @@ NS_ASSUME_NONNULL_BEGIN * * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. */ -- (void)onDidFinishProcessingUpdates:(nullable void (^)())didFinishProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))didFinishProcessingUpdates; /** * Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread. diff --git a/Source/ASTableNode.mm b/Source/ASTableNode.mm index 5d11775b70..25bb59ab7a 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -44,6 +44,7 @@ @property (nonatomic, assign) BOOL allowsMultipleSelectionDuringEditing; @property (nonatomic, assign) BOOL inverted; @property (nonatomic, assign) CGFloat leadingScreensForBatching; +@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) CGPoint contentOffset; @property (nonatomic, assign) BOOL animatesContentOffset; @property (nonatomic, assign) BOOL automaticallyAdjustsContentOffset; @@ -61,6 +62,7 @@ _allowsMultipleSelectionDuringEditing = NO; _inverted = NO; _leadingScreensForBatching = 2; + _contentInset = UIEdgeInsetsZero; _contentOffset = CGPointZero; _animatesContentOffset = NO; _automaticallyAdjustsContentOffset = NO; @@ -114,17 +116,20 @@ if (_pendingState) { _ASTablePendingState *pendingState = _pendingState; - self.pendingState = nil; - view.asyncDelegate = pendingState.delegate; - view.asyncDataSource = pendingState.dataSource; - view.inverted = pendingState.inverted; - view.allowsSelection = pendingState.allowsSelection; - view.allowsSelectionDuringEditing = pendingState.allowsSelectionDuringEditing; - view.allowsMultipleSelection = pendingState.allowsMultipleSelection; + view.asyncDelegate = pendingState.delegate; + view.asyncDataSource = pendingState.dataSource; + view.inverted = pendingState.inverted; + view.allowsSelection = pendingState.allowsSelection; + view.allowsSelectionDuringEditing = pendingState.allowsSelectionDuringEditing; + view.allowsMultipleSelection = pendingState.allowsMultipleSelection; view.allowsMultipleSelectionDuringEditing = pendingState.allowsMultipleSelectionDuringEditing; + view.contentInset = pendingState.contentInset; + self.pendingState = nil; + if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; } + [view setContentOffset:pendingState.contentOffset animated:pendingState.animatesContentOffset]; } } @@ -238,6 +243,27 @@ } } +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + pendingState.contentInset = contentInset; + } else { + ASDisplayNodeAssert(self.nodeLoaded, @"ASTableNode should be loaded if pendingState doesn't exist"); + self.view.contentInset = contentInset; + } +} + +- (UIEdgeInsets)contentInset +{ + _ASTablePendingState *pendingState = self.pendingState; + if (pendingState) { + return pendingState.contentInset; + } else { + return self.view.contentInset; + } +} + - (void)setContentOffset:(CGPoint)contentOffset { [self setContentOffset:contentOffset animated:NO]; diff --git a/Source/ASTableView.h b/Source/ASTableView.h index b2285a846a..353cb34933 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -69,6 +69,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGFloat leadingScreensForBatching ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead."); +/** + * The distance that the content view is inset from the table view edges. Defaults to UIEdgeInsetsZero. + */ +@property (nonatomic, assign) UIEdgeInsets contentInset ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode property instead"); + /** * The offset of the content view's origin from the table node's origin. Defaults to CGPointZero. */ @@ -177,7 +182,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UITableView's version. */ --(void)reloadDataWithCompletion:(void (^ _Nullable)())completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); +-(void)reloadDataWithCompletion:(void (^ _Nullable)(void))completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -186,14 +191,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); -/** - * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. - * - * @warning This method is substantially more expensive than UITableView's version and will block the main thread while - * all the cells load. - */ -- (void)reloadDataImmediately ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode's reloadDataWithCompletion: followed by ASTableNode's -waitUntilAllUpdatesAreCommitted instead."); - /** * Triggers a relayout of all nodes. * @@ -224,7 +221,7 @@ NS_ASSUME_NONNULL_BEGIN * See ASTableNode.h for full documentation of these methods. */ @property (nonatomic, readonly) BOOL isProcessingUpdates; -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion; - (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASTableNode waitUntilAllUpdatesAreProcessed] instead."); - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); @@ -243,12 +240,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); -/// Deprecated in 2.0. You should not call this method. -- (void)clearContents ASDISPLAYNODE_DEPRECATED_MSG("You should not call this method directly. Intead, rely on the Interstate State callback methods."); - -/// Deprecated in 2.0. You should not call this method. -- (void)clearFetchedData ASDISPLAYNODE_DEPRECATED_MSG("You should not call this method directly. Intead, rely on the Interstate State callback methods."); - - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead."); @end diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 6801a97a52..09feccab44 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -131,6 +131,15 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [node __setHighlightedFromUIKit:self.highlighted]; } +- (BOOL)consumesCellNodeVisibilityEvents +{ + ASCellNode *node = self.node; + if (node == nil) { + return NO; + } + return ASSubclassOverridesSelector([ASCellNode class], [node class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:)); +} + - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; @@ -373,8 +382,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [self setAsyncDataSource:nil]; // Data controller & range controller may own a ton of nodes, let's deallocate those off-main - ASPerformBackgroundDeallocation(_dataController); - ASPerformBackgroundDeallocation(_rangeController); + ASPerformBackgroundDeallocation(&_dataController); + ASPerformBackgroundDeallocation(&_rangeController); } #pragma mark - @@ -545,13 +554,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [self reloadDataWithCompletion:nil]; } -- (void)reloadDataImmediately -{ - ASDisplayNodeAssertMainThread(); - [self reloadData]; - [_dataController waitUntilAllUpdatesAreProcessed]; -} - - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated { if ([self validateIndexPath:indexPath]) { @@ -561,7 +563,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)relayoutItems { - [_dataController relayoutAllNodes]; + [_dataController relayoutAllNodesWithInvalidationBlock:nil]; } - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType @@ -596,18 +598,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSIndexPath *)convertIndexPathFromTableNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait { - // If this is a section index path, we don't currently have a method - // to do a mapping. - if (indexPath == nil || indexPath.row == NSNotFound) { - return indexPath; - } else { - NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; - if (viewIndexPath == nil && wait) { - [self waitUntilAllUpdatesAreCommitted]; - return [self convertIndexPathFromTableNode:indexPath waitingIfNeeded:NO]; - } - return viewIndexPath; + NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; + if (viewIndexPath == nil && wait) { + [self waitUntilAllUpdatesAreCommitted]; + return [self convertIndexPathFromTableNode:indexPath waitingIfNeeded:NO]; } + return viewIndexPath; } - (NSIndexPath *)convertIndexPathToTableNode:(NSIndexPath *)indexPath @@ -616,13 +612,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return nil; } - // If this is a section index path, we don't currently have a method - // to do a mapping. - if (indexPath.row == NSNotFound) { - return indexPath; - } else { - return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; - } + return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; } - (NSArray *)convertIndexPathsToTableNode:(NSArray *)indexPaths @@ -768,7 +758,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; _nodesConstrainedWidth = constrainedWidth; [self beginUpdates]; - [_dataController relayoutAllNodes]; + [_dataController relayoutAllNodesWithInvalidationBlock:nil]; [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; } else { if (_cellsForLayoutUpdates.count > 0) { @@ -991,7 +981,14 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { ASCollectionElement *element = cell.element; - [_visibleElements addObject:element]; + if (element) { + ASDisplayNodeAssertTrue([_dataController.visibleMap elementForItemAtIndexPath:indexPath] == element); + [_visibleElements addObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplayCell: %@, %@, %@", cell, self, indexPath); + return; + } + ASCellNode *cellNode = element.node; cellNode.scrollView = tableView; @@ -1011,15 +1008,22 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_rangeController setNeedsUpdate]; - if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { + if ([cell consumesCellNodeVisibilityEvents]) { [_cellsForVisibilityUpdates addObject:cell]; } } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { + // Retrieve the element from cell instead of visible map because at this point visible map could have been updated and no longer holds the element. ASCollectionElement *element = cell.element; - [_visibleElements removeObject:element]; + if (element) { + [_visibleElements removeObject:element]; + } else { + ASDisplayNodeAssert(NO, @"Unexpected nil element for didEndDisplayingCell: %@, %@, %@", cell, self, indexPath); + return; + } + ASCellNode *cellNode = element.node; [_rangeController setNeedsUpdate]; @@ -1326,7 +1330,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { - _leadingScreensForBatching = leadingScreensForBatching; + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + ASPerformBlockOnMainThread(^{ + [self _checkForBatchFetching]; + }); + } } - (BOOL)automaticallyAdjustsContentOffset diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 18f0198efc..e5338f5217 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -539,7 +539,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } - (id)_linkAttributeValueAtPoint:(CGPoint)point - attributeName:(out NSString **)attributeNameOut + attributeName:(out NSString * __autoreleasing *)attributeNameOut range:(out NSRange *)rangeOut inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting @@ -1384,7 +1384,8 @@ static NSAttributedString *DefaultTruncationAttributedString() } #endif -static ASDN::Mutex _experimentLock; +// Allocate _experimentLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& _experimentLock = *new ASDN::StaticMutex; static ASTextNodeExperimentOptions _experimentOptions; static BOOL _hasAllocatedNode; @@ -1392,7 +1393,7 @@ static BOOL _hasAllocatedNode; { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ASDN::MutexLocker lock(_experimentLock); + ASDN::StaticMutexLocker lock(_experimentLock); // They must call this before allocating any text nodes. ASDisplayNodeAssertFalse(_hasAllocatedNode); @@ -1427,7 +1428,7 @@ static BOOL _hasAllocatedNode; { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ASDN::MutexLocker lock(_experimentLock); + ASDN::StaticMutexLocker lock(_experimentLock); _hasAllocatedNode = YES; }); diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 48ff3c4bb9..ecd9496c51 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -376,7 +376,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; text:(NSAttributedString *)text { - static ASDN::Mutex layoutCacheLock; + // Allocate layoutCacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) + static ASDN::StaticMutex& layoutCacheLock = *new ASDN::StaticMutex; static NSCache *textLayoutCache; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -384,7 +385,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; }); ASTextCacheValue *cacheValue = ({ - ASDN::MutexLocker lock(layoutCacheLock); + ASDN::StaticMutexLocker lock(layoutCacheLock); cacheValue = [textLayoutCache objectForKey:text]; if (cacheValue == nil) { cacheValue = [[ASTextCacheValue alloc] init]; diff --git a/Source/ASVideoNode.mm b/Source/ASVideoNode.mm index 291d733419..57e7861d20 100644 --- a/Source/ASVideoNode.mm +++ b/Source/ASVideoNode.mm @@ -181,12 +181,6 @@ static NSString * const kRate = @"rate"; if (self.image == nil && self.URL == nil) { [self generatePlaceholderImage]; } - - __weak __typeof(self) weakSelf = self; - _timeObserverInterval = CMTimeMake(1, _periodicTimeObserverTimescale); - _timeObserver = [_player addPeriodicTimeObserverForInterval:_timeObserverInterval queue:NULL usingBlock:^(CMTime time){ - [weakSelf periodicTimeObserver:time]; - }]; } - (void)addPlayerItemObservers:(AVPlayerItem *)playerItem @@ -231,10 +225,21 @@ static NSString * const kRate = @"rate"; } [player addObserver:self forKeyPath:kRate options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; + + __weak __typeof(self) weakSelf = self; + _timeObserverInterval = CMTimeMake(1, _periodicTimeObserverTimescale); + _timeObserver = [player addPeriodicTimeObserverForInterval:_timeObserverInterval queue:NULL usingBlock:^(CMTime time){ + [weakSelf periodicTimeObserver:time]; + }]; } - (void) removePlayerObservers:(AVPlayer *)player { + if (_timeObserver != nil) { + [player removeTimeObserver:_timeObserver]; + _timeObserver = nil; + } + @try { [player removeObserver:self forKeyPath:kRate context:ASVideoNodeContext]; } @@ -836,8 +841,6 @@ static NSString * const kRate = @"rate"; - (void)dealloc { - [_player removeTimeObserver:_timeObserver]; - _timeObserver = nil; [self removePlayerItemObservers:_currentPlayerItem]; [self removePlayerObservers:_player]; diff --git a/Source/ASVideoPlayerNode.h b/Source/ASVideoPlayerNode.h index 4b475f920f..7c30f86205 100644 --- a/Source/ASVideoPlayerNode.h +++ b/Source/ASVideoPlayerNode.h @@ -46,8 +46,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL controlsDisabled; -@property (nonatomic, assign, readonly) BOOL loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state. This flag does nothing."); - #pragma mark - ASVideoNode property proxy /** * When shouldAutoplay is set to true, a video node will play when it has both loaded and entered the "visible" interfaceState. @@ -81,12 +79,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithAsset:(AVAsset *)asset; - (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix; -#pragma mark Lifecycle Deprecated -- (instancetype)initWithUrl:(NSURL *)url ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); -- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); -- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); -- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore."); - #pragma mark - Public API - (void)seekToTime:(CGFloat)percentComplete; - (void)play; diff --git a/Source/ASVideoPlayerNode.mm b/Source/ASVideoPlayerNode.mm index 753960db05..bd383496c2 100644 --- a/Source/ASVideoPlayerNode.mm +++ b/Source/ASVideoPlayerNode.mm @@ -155,28 +155,6 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; [self addSubnode:_videoNode]; } -#pragma mark Deprecated - -- (instancetype)initWithUrl:(NSURL *)url -{ - return [self initWithURL:url]; -} - -- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible -{ - return [self initWithURL:url]; -} - -- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible -{ - return [self initWithAsset:asset]; -} - -- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible -{ - return [self initWithAsset:asset videoComposition:videoComposition audioMix:audioMix]; -} - #pragma mark - Setter / Getter - (void)setAssetURL:(NSURL *)assetURL diff --git a/Source/ASViewController.h b/Source/ASViewController.h index f96bd0194d..3d2c0e1fe7 100644 --- a/Source/ASViewController.h +++ b/Source/ASViewController.h @@ -94,19 +94,6 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface ASViewController (Deprecated) - -/** - * The constrained size used to measure the backing node. - * - * @discussion Defaults to providing a size range that uses the view controller view's bounds as - * both the min and max definitions. Override this method to provide a custom size range to the - * backing node. - */ -- (ASSizeRange)nodeConstrainedSize AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Set the size directly to the view's frame"); - -@end - NS_ASSUME_NONNULL_END #endif diff --git a/Source/ASViewController.mm b/Source/ASViewController.mm index 2531867d7c..fbb38b282f 100644 --- a/Source/ASViewController.mm +++ b/Source/ASViewController.mm @@ -97,7 +97,7 @@ - (void)dealloc { - ASPerformBackgroundDeallocation(_node); + ASPerformBackgroundDeallocation(&_node); } - (void)loadView @@ -147,12 +147,9 @@ [self propagateNewTraitCollection:traitCollection]; }]; } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" // Call layoutThatFits: to let the node prepare for a layout that will happen shortly in the layout pass of the view. // If the node's constrained size didn't change between the last layout pass it's a no-op [_node layoutThatFits:[self nodeConstrainedSize]]; -#pragma clang diagnostic pop } } @@ -297,13 +294,10 @@ ASVisibilityDepthImplementation; ASTraitCollectionPropagateDown(child, traitCollection); } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" // Once we've propagated all the traits, layout this node. // Remeasure the node with the latest constrained size – old constrained size may be incorrect. as_activity_scope_verbose(as_activity_create("Layout ASViewController node with new traits", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); [_node layoutThatFits:[self nodeConstrainedSize]]; -#pragma clang diagnostic pop } } diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 73a5288bcc..c20f5077a1 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -75,11 +75,11 @@ #import #import #import -#import #import #import #import #import +#import #import #import #import @@ -125,7 +125,6 @@ #import #import -#import #import #import diff --git a/Source/Base/ASAssert.h b/Source/Base/ASAssert.h index ddf87d882a..336ac3d0e9 100644 --- a/Source/Base/ASAssert.h +++ b/Source/Base/ASAssert.h @@ -64,8 +64,8 @@ #define ASDisplayNodeConditionalAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__) #define ASDisplayNodeConditionalCAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeCAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__) -#define ASDisplayNodeCAssertPositiveReal(description, num) ASDisplayNodeCAssert(num >= 0 && num <= CGFLOAT_MAX, @"%@ must be a real positive integer.", description) -#define ASDisplayNodeCAssertInfOrPositiveReal(description, num) ASDisplayNodeCAssert(isinf(num) || (num >= 0 && num <= CGFLOAT_MAX), @"%@ must be infinite or a real positive integer.", description) +#define ASDisplayNodeCAssertPositiveReal(description, num) ASDisplayNodeCAssert(num >= 0 && num <= CGFLOAT_MAX, @"%@ must be a real positive integer: %f.", description, (CGFloat)num) +#define ASDisplayNodeCAssertInfOrPositiveReal(description, num) ASDisplayNodeCAssert(isinf(num) || (num >= 0 && num <= CGFLOAT_MAX), @"%@ must be infinite or a real positive integer: %f.", description, (CGFloat)num) #define ASDisplayNodeErrorDomain @"ASDisplayNodeErrorDomain" #define ASDisplayNodeNonFatalErrorCode 1 @@ -78,11 +78,11 @@ #pragma mark - Main Thread Assertions Disabling ASDISPLAYNODE_EXTERN_C_BEGIN -BOOL ASMainThreadAssertionsAreDisabled(); +BOOL ASMainThreadAssertionsAreDisabled(void); -void ASPushMainThreadAssertionsDisabled(); +void ASPushMainThreadAssertionsDisabled(void); -void ASPopMainThreadAssertionsDisabled(); +void ASPopMainThreadAssertionsDisabled(void); ASDISPLAYNODE_EXTERN_C_END #pragma mark - Non-Fatal Assertions diff --git a/Source/Base/ASAvailability.h b/Source/Base/ASAvailability.h index 64c5a127ea..6a80e274a4 100644 --- a/Source/Base/ASAvailability.h +++ b/Source/Base/ASAvailability.h @@ -31,10 +31,21 @@ #define kCFCoreFoundationVersionNumber_iOS_11_0 1438.10 #endif +#ifndef __IPHONE_11_0 + #define __IPHONE_11_0 110000 +#endif + #define AS_AT_LEAST_IOS9 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_9_0) #define AS_AT_LEAST_IOS10 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_10_0) #define AS_AT_LEAST_IOS11 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_11_0) +// Use __builtin_available if we're on Xcode >= 9, AS_AT_LEAST otherwise. +#if __has_builtin(__builtin_available) + #define AS_AVAILABLE_IOS(ver) __builtin_available(iOS ver, *) +#else + #define AS_AVAILABLE_IOS(ver) AS_AT_LEAST_IOS##ver +#endif + // If Yoga is available, make it available anywhere we use ASAvailability. // This reduces Yoga-specific code in other files. // NOTE: Yoga integration is experimental and not fully tested. Use with caution and test layouts carefully. diff --git a/Source/Base/ASLog.h b/Source/Base/ASLog.h index e4f54fd812..c6aba76bac 100644 --- a/Source/Base/ASLog.h +++ b/Source/Base/ASLog.h @@ -40,31 +40,41 @@ ASDISPLAYNODE_EXTERN_C_BEGIN * are at the `debug` log level, which the system * disables in production. */ -void ASDisableLogging(); +void ASDisableLogging(void); + +/** + * Restore logging that has been runtime-disabled via ASDisableLogging(). + * + * Logging can be disabled at runtime using the ASDisableLogging() function. + * This command restores logging to the level provided in the build + * configuration. This can be used in conjunction with ASDisableLogging() + * to allow logging to be toggled off and back on at runtime. + */ +void ASEnableLogging(void); /// Log for general node events e.g. interfaceState, didLoad. #define ASNodeLogEnabled 1 -os_log_t ASNodeLog(); +os_log_t ASNodeLog(void); /// Log for layout-specific events e.g. calculateLayout. #define ASLayoutLogEnabled 1 -os_log_t ASLayoutLog(); +os_log_t ASLayoutLog(void); /// Log for display-specific events e.g. display queue batches. #define ASDisplayLogEnabled 1 -os_log_t ASDisplayLog(); +os_log_t ASDisplayLog(void); /// Log for collection events e.g. reloadData, performBatchUpdates. #define ASCollectionLogEnabled 1 -os_log_t ASCollectionLog(); +os_log_t ASCollectionLog(void); /// Log for ASNetworkImageNode and ASMultiplexImageNode events. #define ASImageLoadingLogEnabled 1 -os_log_t ASImageLoadingLog(); +os_log_t ASImageLoadingLog(void); /// Specialized log for our main thread deallocation trampoline. #define ASMainThreadDeallocationLogEnabled 0 -os_log_t ASMainThreadDeallocationLog(); +os_log_t ASMainThreadDeallocationLog(void); ASDISPLAYNODE_EXTERN_C_END @@ -119,11 +129,44 @@ ASDISPLAYNODE_EXTERN_C_END * The logging macros are not guarded by deployment-target checks like the activity macros are, but they are * only available on iOS >= 9 at runtime, so just make them conditional. */ -#define as_log_create(subsystem, category) (AS_AT_LEAST_IOS9 ? os_log_create(subsystem, category) : (os_log_t)0) -#define as_log_debug(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_debug(log, format, ##__VA_ARGS__) : (void)0) -#define as_log_info(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_info(log, format, ##__VA_ARGS__) : (void)0) -#define as_log_error(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_error(log, format, ##__VA_ARGS__) : (void)0) -#define as_log_fault(log, format, ...) (AS_AT_LEAST_IOS9 ? os_log_fault(log, format, ##__VA_ARGS__) : (void)0) + +#define as_log_create(subsystem, category) ({ \ +os_log_t __val; \ +if (AS_AVAILABLE_IOS(9)) { \ + __val = os_log_create(subsystem, category); \ +} else { \ + __val = (os_log_t)0; \ +} \ +__val; \ +}) + +#define as_log_debug(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_debug(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_info(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_info(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_error(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_error(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_fault(log, format, ...) \ +if (AS_AVAILABLE_IOS(9)) { \ + os_log_fault(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ #if ASEnableVerboseLogging #define as_log_verbose(log, format, ...) as_log_debug(log, format, ##__VA_ARGS__) diff --git a/Source/Base/ASLog.m b/Source/Base/ASLog.m index 8e65c4b55b..e1c42ea79d 100644 --- a/Source/Base/ASLog.m +++ b/Source/Base/ASLog.m @@ -16,10 +16,11 @@ static atomic_bool __ASLogEnabled = ATOMIC_VAR_INIT(YES); void ASDisableLogging() { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - atomic_store(&__ASLogEnabled, NO); - }); + atomic_store(&__ASLogEnabled, NO); +} + +void ASEnableLogging() { + atomic_store(&__ASLogEnabled, YES); } ASDISPLAYNODE_INLINE BOOL ASLoggingIsEnabled() { diff --git a/Source/Details/ASBasicImageDownloader.mm b/Source/Details/ASBasicImageDownloader.mm index 45961ced6e..2926181e8e 100644 --- a/Source/Details/ASBasicImageDownloader.mm +++ b/Source/Details/ASBasicImageDownloader.mm @@ -46,11 +46,12 @@ NSString * const kASBasicImageDownloaderContextCompletionBlock = @"kASBasicImage @implementation ASBasicImageDownloaderContext static NSMutableDictionary *currentRequests = nil; -static ASDN::RecursiveMutex currentRequestsLock; +// Allocate currentRequestsLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& currentRequestsLock = *new ASDN::StaticMutex; + (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL { - ASDN::MutexLocker l(currentRequestsLock); + ASDN::StaticMutexLocker l(currentRequestsLock); if (!currentRequests) { currentRequests = [[NSMutableDictionary alloc] init]; } @@ -64,7 +65,7 @@ static ASDN::RecursiveMutex currentRequestsLock; + (void)cancelContextWithURL:(NSURL *)URL { - ASDN::MutexLocker l(currentRequestsLock); + ASDN::StaticMutexLocker l(currentRequestsLock); if (currentRequests) { [currentRequests removeObjectForKey:URL]; } diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.h b/Source/Details/ASCollectionGalleryLayoutDelegate.h index ff49473865..0004db9f3e 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.h +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -16,6 +16,7 @@ #import @class ASElementMap; +@class ASCollectionGalleryLayoutDelegate; NS_ASSUME_NONNULL_BEGIN @@ -26,11 +27,13 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion This method will only be called on main thread. * + * @param delegate The calling object. + * * @param elements All elements to be sized. * * @return The elements' size */ -- (CGSize)sizeForElements:(ASElementMap *)elements; +- (CGSize)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(ASElementMap *)elements; @optional @@ -44,11 +47,13 @@ NS_ASSUME_NONNULL_BEGIN * It is not applied between the first line and the header, or between the last line and the footer. * This is the same behavior as UICollectionViewFlowLayout's minimumLineSpacing. * + * @param delegate The calling object. + * * @param elements All elements in the layout. * * @return The interitem spacing */ -- (CGFloat)minimumLineSpacingForElements:(ASElementMap *)elements; +- (CGFloat)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate minimumLineSpacingForElements:(ASElementMap *)elements; /** * Returns the minumum spacing to use between items in the same row or column, depending on the scroll directions. @@ -60,22 +65,26 @@ NS_ASSUME_NONNULL_BEGIN * It is considered while fitting items into lines, but the actual final spacing between some items might be larger. * This is the same behavior as UICollectionViewFlowLayout's minimumInteritemSpacing. * + * @param delegate The calling object. + * * @param elements All elements in the layout. * * @return The interitem spacing */ -- (CGFloat)minimumInteritemSpacingForElements:(ASElementMap *)elements; +- (CGFloat)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate minimumInteritemSpacingForElements:(ASElementMap *)elements; /** * Returns the margins of each section. * * @discussion This method will only be called on main thread. * + * @param delegate The calling object. + * * @param elements All elements in the layout. * * @return The margins used to layout content in a section */ -- (UIEdgeInsets)sectionInsetForElements:(ASElementMap *)elements; +- (UIEdgeInsets)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sectionInsetForElements:(ASElementMap *)elements; @end diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm index 690a5ac62f..bca291f0fa 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -68,9 +68,9 @@ _propertiesProviderFlags = {}; } else { _propertiesProvider = propertiesProvider; - _propertiesProviderFlags.minimumLineSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumLineSpacingForElements:)]; - _propertiesProviderFlags.minimumInteritemSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumInteritemSpacingForElements:)]; - _propertiesProviderFlags.sectionInsetForElements = [_propertiesProvider respondsToSelector:@selector(sectionInsetForElements:)]; + _propertiesProviderFlags.minimumLineSpacingForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:minimumLineSpacingForElements:)]; + _propertiesProviderFlags.minimumInteritemSpacingForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:minimumInteritemSpacingForElements:)]; + _propertiesProviderFlags.sectionInsetForElements = [_propertiesProvider respondsToSelector:@selector(galleryLayoutDelegate:sectionInsetForElements:)]; } } @@ -82,10 +82,10 @@ return nil; } - CGSize itemSize = [propertiesProvider sizeForElements:elements]; - UIEdgeInsets sectionInset = _propertiesProviderFlags.sectionInsetForElements ? [propertiesProvider sectionInsetForElements:elements] : UIEdgeInsetsZero; - CGFloat lineSpacing = _propertiesProviderFlags.minimumLineSpacingForElements ? [propertiesProvider minimumLineSpacingForElements:elements] : 0.0; - CGFloat interitemSpacing = _propertiesProviderFlags.minimumInteritemSpacingForElements ? [propertiesProvider minimumInteritemSpacingForElements:elements] : 0.0; + CGSize itemSize = [propertiesProvider galleryLayoutDelegate:self sizeForElements:elements]; + UIEdgeInsets sectionInset = _propertiesProviderFlags.sectionInsetForElements ? [propertiesProvider galleryLayoutDelegate:self sectionInsetForElements:elements] : UIEdgeInsetsZero; + CGFloat lineSpacing = _propertiesProviderFlags.minimumLineSpacingForElements ? [propertiesProvider galleryLayoutDelegate:self minimumLineSpacingForElements:elements] : 0.0; + CGFloat interitemSpacing = _propertiesProviderFlags.minimumInteritemSpacingForElements ? [propertiesProvider galleryLayoutDelegate:self minimumInteritemSpacingForElements:elements] : 0.0; return [[_ASCollectionGalleryLayoutInfo alloc] initWithItemSize:itemSize minimumLineSpacing:lineSpacing minimumInteritemSpacing:interitemSpacing diff --git a/Source/Details/ASCollectionViewLayoutInspector.h b/Source/Details/ASCollectionViewLayoutInspector.h index 668526c5b8..03658a30fc 100644 --- a/Source/Details/ASCollectionViewLayoutInspector.h +++ b/Source/Details/ASCollectionViewLayoutInspector.h @@ -88,9 +88,6 @@ extern ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *colle * @warning This class is not meant to be subclassed and will be restricted in the future. */ @interface ASCollectionViewLayoutInspector : NSObject - -- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED_MSG("Use -init instead."); - @end diff --git a/Source/Details/ASCollectionViewLayoutInspector.m b/Source/Details/ASCollectionViewLayoutInspector.m index 4b2cc7c49f..a39c74fd52 100644 --- a/Source/Details/ASCollectionViewLayoutInspector.m +++ b/Source/Details/ASCollectionViewLayoutInspector.m @@ -47,13 +47,6 @@ ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *collectionVi } _delegateFlags; } -#pragma mark Lifecycle - -- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView -{ - return [self init]; -} - #pragma mark ASCollectionViewLayoutInspecting - (void)didChangeCollectionViewDelegate:(id)delegate diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 00dd9b36b9..775d40fb89 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -245,8 +245,11 @@ extern NSString * const ASCollectionInvalidUpdateException; * * @discussion Used to respond to a change in size of the containing view * (e.g. ASTableView or ASCollectionView after an orientation change). + * + * The invalidationBlock is called after flushing the ASMainSerialQueue, which ensures that any in-progress + * layout calculations have been applied. The block will not be called if data hasn't been loaded. */ -- (void)relayoutAllNodes; +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)(void))invalidationBlock; /** * Re-measures given nodes in the backing store. @@ -259,7 +262,7 @@ extern NSString * const ASCollectionInvalidUpdateException; * See ASCollectionNode.h for full documentation of these methods. */ @property (nonatomic, readonly) BOOL isProcessingUpdates; -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion; +- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion; - (void)waitUntilAllUpdatesAreProcessed; /** diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index ea6258a6c1..4464937b5c 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -542,7 +542,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; ASMutableElementMap *mutableMap = [previousMap mutableCopy]; // Step 1.1: Update the mutable copies to match the data source's state - [self _updateSectionContextsInMap:mutableMap changeSet:changeSet]; + [self _updateSectionsInMap:mutableMap changeSet:changeSet]; ASPrimitiveTraitCollection existingTraitCollection = [self.node primitiveTraitCollection]; [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate) previousMap:previousMap]; @@ -601,43 +601,38 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; /** * Update sections based on the given change set. */ -- (void)_updateSectionContextsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet +- (void)_updateSectionsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet { ASDisplayNodeAssertMainThread(); - if (!_dataSourceFlags.contextForSection) { - return; - } - if (changeSet.includesReloadData) { - [map removeAllSectionContexts]; + [map removeAllSections]; NSUInteger sectionCount = [self itemCountsFromDataSource].size(); NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; - [self _insertSectionContextsIntoMap:map indexes:sectionIndexes]; + [self _insertSectionsIntoMap:map indexes:sectionIndexes]; // Return immediately because reloadData can't be used in conjuntion with other updates. return; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { - [map removeSectionContextsAtIndexes:change.indexSet]; + [map removeSectionsAtIndexes:change.indexSet]; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { - [self _insertSectionContextsIntoMap:map indexes:change.indexSet]; + [self _insertSectionsIntoMap:map indexes:change.indexSet]; } } -- (void)_insertSectionContextsIntoMap:(ASMutableElementMap *)map indexes:(NSIndexSet *)sectionIndexes +- (void)_insertSectionsIntoMap:(ASMutableElementMap *)map indexes:(NSIndexSet *)sectionIndexes { ASDisplayNodeAssertMainThread(); - - if (!_dataSourceFlags.contextForSection) { - return; - } - + [sectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - id context = [_dataSource dataController:self contextForSection:idx]; + id context; + if (_dataSourceFlags.contextForSection) { + context = [_dataSource dataController:self contextForSection:idx]; + } ASSection *section = [[ASSection alloc] initWithSectionID:_nextSectionID context:context]; [map insertSection:section atIndex:idx]; _nextSectionID++; @@ -761,7 +756,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; } } -- (void)relayoutAllNodes +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock { ASDisplayNodeAssertMainThread(); if (!_initialReloadDataHasBeenCalled) { @@ -772,6 +767,13 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _visibleMap LOG(@"Edit Command - relayoutRows"); [self _scheduleBlockOnMainSerialQueue:^{ + // Because -invalidateLayout doesn't trigger any operations by itself, and we answer queries from UICollectionView using layoutThatFits:, + // we invalidate the layout before we have updated all of the cells. Any cells that the collection needs the size of immediately will get + // -layoutThatFits: with a new constraint, on the main thread, and synchronously calculate them. Meanwhile, relayoutAllNodes will update + // the layout of any remaining nodes on background threads (and fast-return for any nodes that the UICV got to first). + if (invalidationBlock) { + invalidationBlock(); + } [self _relayoutAllNodes]; }]; } @@ -793,7 +795,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; element.constrainedSize = newConstrainedSize; // Node may not be allocated yet (e.g node virtualization or same size optimization) - // Call context.nodeIfAllocated here to avoid immature node allocation and layout + // Call context.nodeIfAllocated here to avoid premature node allocation and layout ASCellNode *node = element.nodeIfAllocated; if (node) { [self _layoutNode:node withConstrainedSize:newConstrainedSize]; diff --git a/Source/Details/ASElementMap.h b/Source/Details/ASElementMap.h index ab67c4e338..b8a19313a2 100644 --- a/Source/Details/ASElementMap.h +++ b/Source/Details/ASElementMap.h @@ -66,10 +66,20 @@ AS_SUBCLASSING_RESTRICTED @property (copy, readonly) NSArray *itemElements; /** - * Returns the index path that corresponds to the same element in @c map at the given @c indexPath. O(1) + * Returns the index path that corresponds to the same element in @c map at the given @c indexPath. + * O(1) for items, fast O(N) for sections. + * + * Note you can pass "section index paths" of length 1 and get a corresponding section index path. */ - (nullable NSIndexPath *)convertIndexPath:(NSIndexPath *)indexPath fromMap:(ASElementMap *)map; +/** + * Returns the section index into the receiver that corresponds to the same element in @c map at @c sectionIndex. Fast O(N). + * + * Returns @c NSNotFound if the section does not exist in the receiver. + */ +- (NSInteger)convertSection:(NSInteger)sectionIndex fromMap:(ASElementMap *)map; + /** * Returns the index path for the given element. O(1) */ diff --git a/Source/Details/ASElementMap.m b/Source/Details/ASElementMap.m index 5c01e6a54f..d4f0e7045f 100644 --- a/Source/Details/ASElementMap.m +++ b/Source/Details/ASElementMap.m @@ -49,6 +49,8 @@ - (instancetype)initWithSections:(NSArray *)sections items:(ASCollectionElementTwoDimensionalArray *)items supplementaryElements:(ASSupplementaryElementDictionary *)supplementaryElements { + NSCParameterAssert(items.count == sections.count); + if (self = [super init]) { _sections = [sections copy]; _sectionsOfItems = [[NSArray alloc] initWithArray:items copyItems:YES]; @@ -159,8 +161,25 @@ - (NSIndexPath *)convertIndexPath:(NSIndexPath *)indexPath fromMap:(ASElementMap *)map { - id element = [map elementForItemAtIndexPath:indexPath]; - return [self indexPathForElement:element]; + if (indexPath.item == NSNotFound) { + // Section index path + NSInteger result = [self convertSection:indexPath.section fromMap:map]; + return (result != NSNotFound ? [NSIndexPath indexPathWithIndex:result] : nil); + } else { + // Item index path + ASCollectionElement *element = [map elementForItemAtIndexPath:indexPath]; + return [self indexPathForElement:element]; + } +} + +- (NSInteger)convertSection:(NSInteger)sectionIndex fromMap:(ASElementMap *)map +{ + if (![map sectionIndexIsValid:sectionIndex assert:YES]) { + return NSNotFound; + } + + ASSection *section = map.sections[sectionIndex]; + return [_sections indexOfObjectIdenticalTo:section]; } #pragma mark - NSCopying @@ -220,7 +239,7 @@ - (BOOL)sectionIndexIsValid:(NSInteger)section assert:(BOOL)assert { NSInteger sectionCount = _sectionsOfItems.count; - if (section >= sectionCount) { + if (section >= sectionCount || section < 0) { if (assert) { ASDisplayNodeFailAssert(@"Invalid section index %zd when there are only %zd sections!", section, sectionCount); } @@ -248,7 +267,7 @@ NSInteger itemCount = _sectionsOfItems[section].count; NSInteger item = indexPath.item; - if (item >= itemCount) { + if (item >= itemCount || item < 0) { if (assert) { ASDisplayNodeFailAssert(@"Invalid item index %zd in section %zd which only has %zd items!", item, section, itemCount); } diff --git a/Source/Details/ASLayoutRangeType.h b/Source/Details/ASLayoutRangeType.h index 45e1635441..2f2be52063 100644 --- a/Source/Details/ASLayoutRangeType.h +++ b/Source/Details/ASLayoutRangeType.h @@ -74,6 +74,3 @@ typedef NS_ENUM(NSInteger, ASLayoutRangeType) { }; static NSInteger const ASLayoutRangeTypeCount = 2; - -#define ASLayoutRangeTypeRender ASLayoutRangeTypeDisplay -#define ASLayoutRangeTypeFetchData ASLayoutRangeTypePreload diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 6427c23af4..7c80d36a03 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -26,14 +26,20 @@ #import #import -#if __has_include () +#if __has_include () #define PIN_ANIMATED_AVAILABLE 1 -#import +#import #import #else #define PIN_ANIMATED_AVAILABLE 0 #endif +#if __has_include() +#define PIN_WEBP_AVAILABLE 1 +#else +#define PIN_WEBP_AVAILABLE 0 +#endif + #import #import #import @@ -44,40 +50,37 @@ @end -@interface PINAnimatedImage (ASPINRemoteImageDownloader) +@interface PINCachedAnimatedImage (ASPINRemoteImageDownloader) @end -@implementation PINAnimatedImage (ASPINRemoteImageDownloader) - -- (void)setCoverImageReadyCallback:(void (^)(UIImage * _Nonnull))coverImageReadyCallback -{ - self.infoCompletion = coverImageReadyCallback; -} - -- (void (^)(UIImage * _Nonnull))coverImageReadyCallback -{ - return self.infoCompletion; -} - -- (void)setPlaybackReadyCallback:(dispatch_block_t)playbackReadyCallback -{ - self.fileReady = playbackReadyCallback; -} - -- (dispatch_block_t)playbackReadyCallback -{ - return self.fileReady; -} +@implementation PINCachedAnimatedImage (ASPINRemoteImageDownloader) - (BOOL)isDataSupported:(NSData *)data { - return [data pin_isGIF]; + if ([data pin_isGIF]) { + return YES; + } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return YES; + } +#endif + return NO; } @end #endif +// Declare two key methods on PINCache objects, avoiding a direct dependency on PINCache.h +@protocol ASPINCache +- (id)diskCache; +@end + +@protocol ASPINDiskCache +@property (assign) NSUInteger byteLimit; +@end + @interface ASPINRemoteImageManager : PINRemoteImageManager @end @@ -86,7 +89,21 @@ //Share image cache with sharedImageManager image cache. - (id )defaultImageCache { - return [[PINRemoteImageManager sharedImageManager] cache]; + static dispatch_once_t onceToken; + static id cache = nil; + dispatch_once(&onceToken, ^{ + cache = [[PINRemoteImageManager sharedImageManager] cache]; + if ([cache respondsToSelector:@selector(diskCache)]) { + id diskCache = [(id )cache diskCache]; + if ([diskCache respondsToSelector:@selector(setByteLimit:)]) { + // Set a default byteLimit. PINCache recently implemented a 50MB default (PR #201). + // Ensure that older versions of PINCache also have a byteLimit applied. + // NOTE: Using 20MB limit while large cache initialization is being optimized (Issue #144). + ((id )diskCache).byteLimit = 20 * 1024 * 1024; + } + } + }); + return cache; } @end @@ -166,7 +183,7 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; #if PIN_ANIMATED_AVAILABLE - (nullable id )animatedImageWithData:(NSData *)animatedImageData { - return [[PINAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; + return [[PINCachedAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; } #endif @@ -187,15 +204,11 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; callbackQueue:(dispatch_queue_t)callbackQueue completion:(ASImageCacherCompletion)completion { - // We do not check the cache here and instead check it in downloadImageWithURL to avoid checking the cache twice. - // If we're targeting the main queue and we're on the main thread, complete immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { - completion(nil); - } else { - dispatch_async(callbackQueue, ^{ - completion(nil); - }); - } + [[self sharedPINRemoteImageManager] imageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ + completion(result.image); + }]; + }]; } - (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL @@ -212,20 +225,16 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; downloadProgress:(ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion; { - return [[self sharedPINRemoteImageManager] downloadImageWithURL:URL options:PINRemoteImageManagerDownloadOptionsSkipDecode progressDownload:^(int64_t completedBytes, int64_t totalBytes) { + PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { if (downloadProgress == nil) { return; } - /// If we're targeting the main queue and we're on the main thread, call immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ downloadProgress(completedBytes / (CGFloat)totalBytes); - } else { - dispatch_async(callbackQueue, ^{ - downloadProgress(completedBytes / (CGFloat)totalBytes); - }); - } - } completion:^(PINRemoteImageManagerResult * _Nonnull result) { - /// If we're targeting the main queue and we're on the main thread, complete immediately. - if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) { + }]; + }; + + PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ #if PIN_ANIMATED_AVAILABLE if (result.alternativeRepresentation) { completion(result.alternativeRepresentation, result.error, result.UUID); @@ -235,20 +244,19 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; #else completion(result.image, result.error, result.UUID); #endif - } else { - dispatch_async(callbackQueue, ^{ -#if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); - } else { - completion(result.image, result.error, result.UUID); - } -#else - completion(result.image, result.error, result.UUID); -#endif - }); - } - }]; + }]; + }; + + // add "IgnoreCache" option since we have a caching API so we already checked it, not worth checking again. + // PINRemoteImage is responsible for coalescing downloads, and even if it wasn't, the tiny probability of + // extra downloads isn't worth the effort of rechecking caches every single time. In order to provide + // feedback to the consumer about whether images are cached, we can't simply make the cache a no-op and + // check the cache as part of this download. + return [[self sharedPINRemoteImageManager] downloadImageWithURL:URL + options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache + progressImage:nil + progressDownload:progressDownload + completion:imageCompletion]; } - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier @@ -307,10 +315,40 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; if ([data pin_isGIF]) { return data; } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return data; + } +#endif + #endif return nil; } +#pragma mark - Private + +/** + * If on main thread and queue is main, perform now. + * If queue is nil, assert and perform now. + * Otherwise, dispatch async to queue. + */ ++ (void)_performWithCallbackQueue:(dispatch_queue_t)queue work:(void (^)(void))work +{ + if (work == nil) { + // No need to assert here, really. We aren't expecting any feedback from this method. + return; + } + + if (ASDisplayNodeThreadIsMain() && queue == dispatch_get_main_queue()) { + work(); + } else if (queue == nil) { + ASDisplayNodeFailAssert(@"Callback queue should not be nil."); + work(); + } else { + dispatch_async(queue, work); + } +} + @end #endif diff --git a/Source/Details/ASThread.h b/Source/Details/ASThread.h index ca049d72a4..989db9a26c 100644 --- a/Source/Details/ASThread.h +++ b/Source/Details/ASThread.h @@ -52,12 +52,6 @@ static inline BOOL ASDisplayNodeThreadIsMain() #include -/** - For use with ASDN::StaticMutex only. - */ -#define ASDISPLAYNODE_MUTEX_INITIALIZER {PTHREAD_MUTEX_INITIALIZER} -#define ASDISPLAYNODE_MUTEX_RECURSIVE_INITIALIZER {PTHREAD_RECURSIVE_MUTEX_INITIALIZER} - // This MUST always execute, even when assertions are disabled. Otherwise all lock operations become no-ops! // (To be explicit, do not turn this into an NSAssert, assert(), or any other kind of statement where the // evaluation of x_ can be compiled out.) @@ -65,7 +59,7 @@ static inline BOOL ASDisplayNodeThreadIsMain() _Pragma("clang diagnostic push"); \ _Pragma("clang diagnostic ignored \"-Wunused-variable\""); \ volatile int res = (x_); \ - assert(res == 0); \ + ASDisplayNodeCAssert(res == 0, @"Expected %@ to return 0, got %d instead", @#x_, res); \ _Pragma("clang diagnostic pop"); \ } while (0) @@ -142,7 +136,7 @@ namespace ASDN { #if !TIME_LOCKER SharedLocker (std::shared_ptr const& l) ASDISPLAYNODE_NOTHROW : _l (l) { - assert(_l != nullptr); + ASDisplayNodeCAssertTrue(_l != nullptr); _l->lock (); } @@ -217,12 +211,12 @@ namespace ASDN { mach_port_t thread_id = pthread_mach_thread_np(pthread_self()); if (thread_id != _owner) { // New owner. Since this mutex can't be acquired by another thread if there is an existing owner, _owner and _count must be 0. - assert(0 == _owner); - assert(0 == _count); + ASDisplayNodeCAssertTrue(0 == _owner); + ASDisplayNodeCAssertTrue(0 == _count); _owner = thread_id; } else { // Existing owner tries to reacquire this (recursive) mutex. _count must already be positive. - assert(_count > 0); + ASDisplayNodeCAssertTrue(_count > 0); } ++_count; #endif @@ -232,9 +226,9 @@ namespace ASDN { #if CHECK_LOCKING_SAFETY mach_port_t thread_id = pthread_mach_thread_np(pthread_self()); // Unlocking a mutex on an unowning thread causes undefined behaviour. Assert and fail early. - assert(thread_id == _owner); + ASDisplayNodeCAssertTrue(thread_id == _owner); // Current thread owns this mutex. _count must be positive. - assert(_count > 0); + ASDisplayNodeCAssertTrue(_count > 0); --_count; if (0 == _count) { // Current thread is no longer the owner. @@ -297,18 +291,19 @@ namespace ASDN { typedef SharedUnlocker MutexSharedUnlocker; /** - If you are creating a static mutex, use StaticMutex and specify its default value as one of ASDISPLAYNODE_MUTEX_INITIALIZER - or ASDISPLAYNODE_MUTEX_RECURSIVE_INITIALIZER. This avoids expensive constructor overhead at startup (or worse, ordering + If you are creating a static mutex, use StaticMutex. This avoids expensive constructor overhead at startup (or worse, ordering issues between different static objects). It also avoids running a destructor on app exit time (needless expense). Note that you can, but should not, use StaticMutex for non-static objects. It will leak its mutex on destruction, so avoid that! - - If you fail to specify a default value (like ASDISPLAYNODE_MUTEX_INITIALIZER) an assert will be thrown when you attempt to lock. */ struct StaticMutex { - pthread_mutex_t _m; // public so it can be provided by ASDISPLAYNODE_MUTEX_INITIALIZER and friends + StaticMutex () : _m (PTHREAD_MUTEX_INITIALIZER) {} + + // non-copyable. + StaticMutex(const StaticMutex&) = delete; + StaticMutex &operator=(const StaticMutex&) = delete; void lock () { ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_lock (this->mutex())); @@ -320,8 +315,8 @@ namespace ASDN { pthread_mutex_t *mutex () { return &_m; } - StaticMutex(const StaticMutex&) = delete; - StaticMutex &operator=(const StaticMutex&) = delete; + private: + pthread_mutex_t _m; }; typedef Locker StaticMutexLocker; diff --git a/Source/Details/ASTraitCollection.h b/Source/Details/ASTraitCollection.h index 760919c0dd..26714aa649 100644 --- a/Source/Details/ASTraitCollection.h +++ b/Source/Details/ASTraitCollection.h @@ -42,7 +42,7 @@ typedef struct ASPrimitiveTraitCollection { /** * Creates ASPrimitiveTraitCollection with default values. */ -extern ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault(); +extern ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault(void); /** * Creates a ASPrimitiveTraitCollection from a given UITraitCollection. @@ -66,10 +66,6 @@ extern NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollecti */ extern void ASTraitCollectionPropagateDown(id element, ASPrimitiveTraitCollection traitCollection); -/// For backward compatibility reasons we redefine the old layout element trait collection struct name -#define ASEnvironmentTraitCollection ASPrimitiveTraitCollection -#define ASEnvironmentTraitCollectionMakeDefault ASPrimitiveTraitCollectionMakeDefault - ASDISPLAYNODE_EXTERN_C_END /** @@ -92,13 +88,6 @@ ASDISPLAYNODE_EXTERN_C_END */ - (ASTraitCollection *)asyncTraitCollection; -/** - * Deprecated and should be replaced by the methods from above - */ -- (ASEnvironmentTraitCollection)environmentTraitCollection; -- (void)setEnvironmentTraitCollection:(ASEnvironmentTraitCollection)traitCollection; - - @end #define ASPrimitiveTraitCollectionDefaults \ @@ -111,16 +100,6 @@ ASDISPLAYNODE_EXTERN_C_END _primitiveTraitCollection = traitCollection;\ }\ -#define ASPrimitiveTraitCollectionDeprecatedImplementation \ -- (ASEnvironmentTraitCollection)environmentTraitCollection\ -{\ - return self.primitiveTraitCollection;\ -}\ -- (void)setEnvironmentTraitCollection:(ASEnvironmentTraitCollection)traitCollection\ -{\ - [self setPrimitiveTraitCollection:traitCollection];\ -}\ - #define ASLayoutElementCollectionTableSetTraitCollection(lock) \ - (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection\ {\ diff --git a/Source/Details/ASTraitCollection.m b/Source/Details/ASTraitCollection.m index 0c845e9f63..04eaea608e 100644 --- a/Source/Details/ASTraitCollection.m +++ b/Source/Details/ASTraitCollection.m @@ -48,7 +48,7 @@ ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITra environmentTraitCollection.horizontalSizeClass = traitCollection.horizontalSizeClass; environmentTraitCollection.verticalSizeClass = traitCollection.verticalSizeClass; environmentTraitCollection.userInterfaceIdiom = traitCollection.userInterfaceIdiom; - if (AS_AT_LEAST_IOS9) { + if (AS_AVAILABLE_IOS(9)) { environmentTraitCollection.forceTouchCapability = traitCollection.forceTouchCapability; } return environmentTraitCollection; @@ -67,17 +67,28 @@ BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTr // Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceIdiom(UIUserInterfaceIdiom idiom) { - switch (idiom) { - case UIUserInterfaceIdiomTV: - return @"TV"; - case UIUserInterfaceIdiomPad: - return @"Pad"; - case UIUserInterfaceIdiomPhone: - return @"Phone"; - case UIUserInterfaceIdiomCarPlay: - return @"CarPlay"; - default: - return @"Unspecified"; + if (AS_AVAILABLE_IOS(9)) { + switch (idiom) { + case UIUserInterfaceIdiomTV: + return @"TV"; + case UIUserInterfaceIdiomPad: + return @"Pad"; + case UIUserInterfaceIdiomPhone: + return @"Phone"; + case UIUserInterfaceIdiomCarPlay: + return @"CarPlay"; + default: + return @"Unspecified"; + } + } else { + switch (idiom) { + case UIUserInterfaceIdiomPad: + return @"Pad"; + case UIUserInterfaceIdiomPhone: + return @"Phone"; + default: + return @"Unspecified"; + } } } @@ -167,7 +178,10 @@ NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection trai + (instancetype)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection containerSize:(CGSize)windowSize { - UIForceTouchCapability forceTouch = AS_AT_LEAST_IOS9 ? traitCollection.forceTouchCapability : UIForceTouchCapabilityUnknown; + UIForceTouchCapability forceTouch = UIForceTouchCapabilityUnknown; + if(AS_AVAILABLE_IOS(9)) { + forceTouch = traitCollection.forceTouchCapability; + } return [self traitCollectionWithDisplayScale:traitCollection.displayScale userInterfaceIdiom:traitCollection.userInterfaceIdiom horizontalSizeClass:traitCollection.horizontalSizeClass diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 537a4ae9e7..452dcee6e5 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -71,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) CGRect bounds; @property (nonatomic, assign) CGRect frame; // Only for use with nodes wrapping synchronous views @property (nonatomic, assign) UIViewContentMode contentMode; -@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute; +@property (nonatomic, assign) UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); @property (nonatomic, assign, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; @property (nonatomic, assign, getter=isExclusiveTouch) BOOL exclusiveTouch; @property (nonatomic, assign, getter=asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer; @@ -82,12 +82,15 @@ NS_ASSUME_NONNULL_BEGIN as they are already on NSObject @property (nonatomic, assign) BOOL isAccessibilityElement; - @property (nonatomic, copy) NSString *accessibilityLabel; - @property (nonatomic, copy) NSString *accessibilityHint; - @property (nonatomic, copy) NSString *accessibilityValue; + @property (nonatomic, copy, nullable) NSString *accessibilityLabel; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedLabel API_AVAILABLE(ios(11.0),tvos(11.0)); + @property (nonatomic, copy, nullable) NSString *accessibilityHint; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedHint API_AVAILABLE(ios(11.0),tvos(11.0)); + @property (nonatomic, copy, nullable) NSString *accessibilityValue; + @property (nonatomic, copy, nullable) NSAttributedString *accessibilityAttributedValue API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, assign) UIAccessibilityTraits accessibilityTraits; @property (nonatomic, assign) CGRect accessibilityFrame; - @property (nonatomic, strong) NSString *accessibilityLanguage; + @property (nonatomic, strong, nullable) NSString *accessibilityLanguage; @property (nonatomic, assign) BOOL accessibilityElementsHidden; @property (nonatomic, assign) BOOL accessibilityViewIsModal; @property (nonatomic, assign) BOOL shouldGroupAccessibilityChildren; diff --git a/Source/Details/_ASDisplayLayer.mm b/Source/Details/_ASDisplayLayer.mm index e44265f195..efe1ff8992 100644 --- a/Source/Details/_ASDisplayLayer.mm +++ b/Source/Details/_ASDisplayLayer.mm @@ -25,6 +25,7 @@ #import #import #import +#import @implementation _ASDisplayLayer { @@ -101,6 +102,7 @@ - (void)setNeedsLayout { ASDisplayNodeAssertMainThread(); + as_log_verbose(ASNodeLog(), "%s on %@", sel_getName(_cmd), self); [super setNeedsLayout]; } #endif diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 93f3187f6a..2c2d2c383f 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -345,13 +345,11 @@ } } -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_6_0 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. return [node gestureRecognizerShouldBegin:gestureRecognizer]; } -#endif - (void)tintColorDidChange { diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 80ae2a9223..734cf2b7fb 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -18,6 +18,7 @@ #ifndef ASDK_ACCESSIBILITY_DISABLE #import +#import #import #import #import @@ -83,6 +84,13 @@ static void SortAccessibilityElements(NSMutableArray *elements) accessibilityElement.accessibilityHint = node.accessibilityHint; accessibilityElement.accessibilityValue = node.accessibilityValue; accessibilityElement.accessibilityTraits = node.accessibilityTraits; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS(11)) { + accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue; + } +#endif return accessibilityElement; } @@ -169,8 +177,24 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, _ } SortAccessibilityElements(labeledNodes); - NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; - accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS(11)) { + NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; + NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; + [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (idx != 0) { + [attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]]; + } + [attributedLabel appendAttributedString:(NSAttributedString *)obj]; + }]; + accessiblityElement.accessibilityAttributedLabel = attributedLabel; + } else +#endif + { + NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; + accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + } SortAccessibilityElements(actions); accessiblityElement.accessibilityCustomActions = actions; diff --git a/Source/Layout/ASAbsoluteLayoutElement.h b/Source/Layout/ASAbsoluteLayoutElement.h index 39ce04f1b1..384830f5e1 100644 --- a/Source/Layout/ASAbsoluteLayoutElement.h +++ b/Source/Layout/ASAbsoluteLayoutElement.h @@ -16,7 +16,6 @@ // #import -#import NS_ASSUME_NONNULL_BEGIN @@ -30,11 +29,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGPoint layoutPosition; - -#pragma mark Deprecated - -@property (nonatomic, assign) ASRelativeSizeRange sizeRange ASDISPLAYNODE_DEPRECATED; - @end NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASAbsoluteLayoutSpec.h b/Source/Layout/ASAbsoluteLayoutSpec.h index 7d218dce0e..0c250ec689 100644 --- a/Source/Layout/ASAbsoluteLayoutSpec.h +++ b/Source/Layout/ASAbsoluteLayoutSpec.h @@ -50,13 +50,4 @@ NS_ASSUME_NONNULL_BEGIN @end - -#pragma mark - Deprecated - -@interface ASStaticLayoutSpec : ASAbsoluteLayoutSpec - -+ (instancetype)staticLayoutSpecWithChildren:(NSArray> *)children AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED; - -@end - NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASAbsoluteLayoutSpec.mm b/Source/Layout/ASAbsoluteLayoutSpec.mm index c8d136a582..96ab7a705b 100644 --- a/Source/Layout/ASAbsoluteLayoutSpec.mm +++ b/Source/Layout/ASAbsoluteLayoutSpec.mm @@ -107,18 +107,3 @@ @end -#pragma mark - ASStaticLayoutSpec - -@implementation ASStaticLayoutSpec : ASAbsoluteLayoutSpec - -+ (instancetype)staticLayoutSpecWithChildren:(NSArray> *)children -{ - return [self absoluteLayoutSpecWithSizing:ASAbsoluteLayoutSpecSizingSizeToFit children:children]; -} - -- (instancetype)initWithChildren:(NSArray *)children -{ - return [super initWithSizing:ASAbsoluteLayoutSpecSizingSizeToFit children:children]; -} - -@end diff --git a/Source/Layout/ASCornerLayoutSpec.h b/Source/Layout/ASCornerLayoutSpec.h new file mode 100644 index 0000000000..23d09d0179 --- /dev/null +++ b/Source/Layout/ASCornerLayoutSpec.h @@ -0,0 +1,79 @@ +// +// ASCornerLayoutSpec.h +// Texture +// +// Copyright (c) 2017-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 + +/** + The corner location for positioning corner element. + */ +typedef NS_ENUM(NSInteger, ASCornerLayoutLocation) { + ASCornerLayoutLocationTopLeft, + ASCornerLayoutLocationTopRight, + ASCornerLayoutLocationBottomLeft, + ASCornerLayoutLocationBottomRight, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + A layout spec that positions a corner element which relatives to the child element. + + @warning Both child element and corner element must have valid preferredSize for layout calculation. + */ +@interface ASCornerLayoutSpec : ASLayoutSpec + +/** + A layout spec that positions a corner element which relatives to the child element. + + @param child A child that is laid out to determine the size of this spec. + @param corner A layoutElement object that is laid out to a corner on the child. + @param location The corner position option. + @return An ASCornerLayoutSpec object with a given child and an layoutElement that act as corner. + */ +- (instancetype)initWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location AS_WARN_UNUSED_RESULT; + +/** + A layout spec that positions a corner element which relatives to the child element. + + @param child A child that is laid out to determine the size of this spec. + @param corner A layoutElement object that is laid out to a corner on the child. + @param location The corner position option. + @return An ASCornerLayoutSpec object with a given child and an layoutElement that act as corner. + */ ++ (instancetype)cornerLayoutSpecWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location AS_WARN_UNUSED_RESULT; + +/** + A layoutElement object that is laid out to a corner on the child. + */ +@property (nonatomic, strong) id corner; + +/** + The corner position option. + */ +@property (nonatomic, assign) ASCornerLayoutLocation cornerLocation; + +/** + The point which offsets from the corner location. Use this property to make delta + distance from the default corner location. Default is CGPointZero. + */ +@property (nonatomic, assign) CGPoint offset; + +/** + Whether should include corner element into layout size calculation. If included, + the layout size will be the union size of both child and corner; If not included, + the layout size will be only child's size. Default is NO. + */ +@property (nonatomic, assign) BOOL wrapsCorner; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASCornerLayoutSpec.mm b/Source/Layout/ASCornerLayoutSpec.mm new file mode 100644 index 0000000000..d1104089b3 --- /dev/null +++ b/Source/Layout/ASCornerLayoutSpec.mm @@ -0,0 +1,169 @@ +// +// ASCornerLayoutSpec.mm +// Texture +// +// Copyright (c) 2017-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 +#import +#import + +CGPoint as_calculatedCornerOriginIn(CGRect baseFrame, CGSize cornerSize, ASCornerLayoutLocation cornerLocation, CGPoint offset) +{ + CGPoint cornerOrigin = CGPointZero; + CGPoint baseOrigin = baseFrame.origin; + CGSize baseSize = baseFrame.size; + + switch (cornerLocation) { + case ASCornerLayoutLocationTopLeft: + cornerOrigin.x = baseOrigin.x - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y - cornerSize.height / 2; + break; + case ASCornerLayoutLocationTopRight: + cornerOrigin.x = baseOrigin.x + baseSize.width - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y - cornerSize.height / 2; + break; + case ASCornerLayoutLocationBottomLeft: + cornerOrigin.x = baseOrigin.x - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y + baseSize.height - cornerSize.height / 2; + break; + case ASCornerLayoutLocationBottomRight: + cornerOrigin.x = baseOrigin.x + baseSize.width - cornerSize.width / 2; + cornerOrigin.y = baseOrigin.y + baseSize.height - cornerSize.height / 2; + break; + } + + cornerOrigin.x += offset.x; + cornerOrigin.y += offset.y; + + return cornerOrigin; +} + +static NSUInteger const kBaseChildIndex = 0; +static NSUInteger const kCornerChildIndex = 1; + +@interface ASCornerLayoutSpec() +@end + +@implementation ASCornerLayoutSpec + +- (instancetype)initWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location +{ + self = [super init]; + if (self) { + self.child = child; + self.corner = corner; + self.cornerLocation = location; + } + return self; +} + ++ (instancetype)cornerLayoutSpecWithChild:(id )child corner:(id )corner location:(ASCornerLayoutLocation)location +{ + return [[self alloc] initWithChild:child corner:corner location:location]; +} + +#pragma mark - Children + +- (void)setChild:(id)child +{ + ASDisplayNodeAssertNotNil(child, @"Child shouldn't be nil."); + [super setChild:child atIndex:kBaseChildIndex]; +} + +- (id)child +{ + return [super childAtIndex:kBaseChildIndex]; +} + +- (void)setCorner:(id)corner +{ + ASDisplayNodeAssertNotNil(corner, @"Corner element cannot be nil."); + [super setChild:corner atIndex:kCornerChildIndex]; +} + +- (id)corner +{ + return [super childAtIndex:kCornerChildIndex]; +} + +#pragma mark - Calculation + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + CGSize size = { + ASPointsValidForSize(constrainedSize.max.width) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.width, + ASPointsValidForSize(constrainedSize.max.height) == NO ? ASLayoutElementParentDimensionUndefined : constrainedSize.max.height + }; + + id child = self.child; + id corner = self.corner; + + // Element validation + [self _validateElement:child]; + [self _validateElement:corner]; + + CGRect childFrame = CGRectZero; + CGRect cornerFrame = CGRectZero; + + // Layout child + ASLayout *childLayout = [child layoutThatFits:constrainedSize parentSize:size]; + childFrame.size = childLayout.size; + + // Layout corner + ASLayout *cornerLayout = [corner layoutThatFits:constrainedSize parentSize:size]; + cornerFrame.size = cornerLayout.size; + + // Calculate corner's position + CGPoint relativePosition = as_calculatedCornerOriginIn(childFrame, cornerFrame.size, _cornerLocation, _offset); + + // Update corner's position + cornerFrame.origin = relativePosition; + + // Calculate size + CGRect frame = childFrame; + if (_wrapsCorner) { + frame = CGRectUnion(childFrame, cornerFrame); + frame.size = ASSizeRangeClamp(constrainedSize, frame.size); + } + + // Shift sublayouts' positions if they are off the bounds. + if (frame.origin.x != 0) { + CGFloat deltaX = frame.origin.x; + childFrame.origin.x -= deltaX; + cornerFrame.origin.x -= deltaX; + } + + if (frame.origin.y != 0) { + CGFloat deltaY = frame.origin.y; + childFrame.origin.y -= deltaY; + cornerFrame.origin.y -= deltaY; + } + + childLayout.position = childFrame.origin; + cornerLayout.position = cornerFrame.origin; + + return [ASLayout layoutWithLayoutElement:self size:frame.size sublayouts:@[childLayout, cornerLayout]]; +} + +- (void)_validateElement:(id )element +{ + // Validate non-nil element + if (element == nil) { + ASDisplayNodeAssertNotNil(element, @"[%@]: Must have a non-nil child/corner for layout calculation.", self.class); + } + // Validate preferredSize if needed + CGSize size = element.style.preferredSize; + if (!CGSizeEqualToSize(size, CGSizeZero) && !ASIsCGSizeValidForSize(size) && (size.width < 0 || (size.height < 0))) { + ASDisplayNodeFailAssert(@"[%@]: Should give a valid preferredSize value for %@ before corner's position calculation.", self.class, element); + } +} + +@end diff --git a/Source/Layout/ASDimensionDeprecated.h b/Source/Layout/ASDimensionDeprecated.h deleted file mode 100644 index 94483edc2e..0000000000 --- a/Source/Layout/ASDimensionDeprecated.h +++ /dev/null @@ -1,102 +0,0 @@ -// -// ASDimensionDeprecated.h -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. 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 -// - -#pragma once -#import -#import - -ASDISPLAYNODE_EXTERN_C_BEGIN -NS_ASSUME_NONNULL_BEGIN - -/** - * A dimension relative to constraints to be provided in the future. - * A ASDimension can be one of three types: - * - * "Auto" - This indicated "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances. - * - * "Points" - Just a number. It will always resolve to exactly this amount. - * - * "Percent" - Multiplied to a provided parent amount to resolve a final amount. - */ -typedef NS_ENUM(NSInteger, ASRelativeDimensionType) { - /** This indicates "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances. */ - ASRelativeDimensionTypeAuto, - /** Just a number. It will always resolve to exactly this amount. This is the default type. */ - ASRelativeDimensionTypePoints, - /** Multiplied to a provided parent amount to resolve a final amount. */ - ASRelativeDimensionTypeFraction, -}; - -#define ASRelativeDimension ASDimension -#define ASRelativeSize ASLayoutSize -#define ASRelativeDimensionMakeWithPoints ASDimensionMakeWithPoints -#define ASRelativeDimensionMakeWithFraction ASDimensionMakeWithFraction - -/** - * Function is deprecated. Use ASSizeRangeMake instead. - */ -extern AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMakeExactSize(CGSize size) ASDISPLAYNODE_DEPRECATED_MSG("Use ASSizeRangeMake instead."); - -/** - Expresses an inclusive range of relative sizes. Used to provide additional constraint to layout. - Used by ASStaticLayoutSpec. - */ -typedef struct { - ASLayoutSize min; - ASLayoutSize max; -} ASRelativeSizeRange; - -extern ASRelativeSizeRange const ASRelativeSizeRangeUnconstrained; - -#pragma mark - ASRelativeDimension - -extern ASDimension ASRelativeDimensionMake(ASRelativeDimensionType type, CGFloat value) ASDISPLAYNODE_DEPRECATED; - -#pragma mark - ASRelativeSize - -extern ASLayoutSize ASRelativeSizeMake(ASRelativeDimension width, ASRelativeDimension height) ASDISPLAYNODE_DEPRECATED; - -/** Convenience constructor to provide size in points. */ -extern ASLayoutSize ASRelativeSizeMakeWithCGSize(CGSize size) ASDISPLAYNODE_DEPRECATED; - -/** Convenience constructor to provide size as a fraction. */ -extern ASLayoutSize ASRelativeSizeMakeWithFraction(CGFloat fraction) ASDISPLAYNODE_DEPRECATED; - -extern BOOL ASRelativeSizeEqualToRelativeSize(ASLayoutSize lhs, ASLayoutSize rhs) ASDISPLAYNODE_DEPRECATED; - -extern NSString *NSStringFromASRelativeSize(ASLayoutSize size) ASDISPLAYNODE_DEPRECATED; - -#pragma mark - ASRelativeSizeRange - -extern ASRelativeSizeRange ASRelativeSizeRangeMake(ASLayoutSize min, ASLayoutSize max) ASDISPLAYNODE_DEPRECATED; - -#pragma mark Convenience constructors to provide an exact size (min == max). -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeSize(ASLayoutSize exact) ASDISPLAYNODE_DEPRECATED; - -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactCGSize(CGSize exact) ASDISPLAYNODE_DEPRECATED; - -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactFraction(CGFloat fraction) ASDISPLAYNODE_DEPRECATED; - -extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeDimensions(ASRelativeDimension exactWidth, ASRelativeDimension exactHeight) ASDISPLAYNODE_DEPRECATED; - -extern BOOL ASRelativeSizeRangeEqualToRelativeSizeRange(ASRelativeSizeRange lhs, ASRelativeSizeRange rhs) ASDISPLAYNODE_DEPRECATED; - -/** Provided a parent size, compute final dimensions for this RelativeSizeRange to arrive at a SizeRange. */ -extern ASSizeRange ASRelativeSizeRangeResolve(ASRelativeSizeRange relativeSizeRange, CGSize parentSize) ASDISPLAYNODE_DEPRECATED; - -NS_ASSUME_NONNULL_END -ASDISPLAYNODE_EXTERN_C_END diff --git a/Source/Layout/ASDimensionDeprecated.mm b/Source/Layout/ASDimensionDeprecated.mm deleted file mode 100644 index bca453d08a..0000000000 --- a/Source/Layout/ASDimensionDeprecated.mm +++ /dev/null @@ -1,102 +0,0 @@ -// -// ASDimensionDeprecated.mm -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. 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 - -ASDimension ASRelativeDimensionMake(ASRelativeDimensionType type, CGFloat value) -{ - if (type == ASRelativeDimensionTypePoints) { - return ASDimensionMakeWithPoints(value); - } else if (type == ASRelativeDimensionTypeFraction) { - return ASDimensionMakeWithFraction(value); - } - - ASDisplayNodeCAssert(NO, @"ASRelativeDimensionMake does not support the given ASRelativeDimensionType"); - return ASDimensionMakeWithPoints(0); -} - -ASSizeRange ASSizeRangeMakeExactSize(CGSize size) -{ - return ASSizeRangeMake(size); -} - -ASRelativeSizeRange const ASRelativeSizeRangeUnconstrained = {}; - -#pragma mark - ASRelativeSize - -ASLayoutSize ASRelativeSizeMake(ASRelativeDimension width, ASRelativeDimension height) -{ - return ASLayoutSizeMake(width, height); -} - -ASLayoutSize ASRelativeSizeMakeWithCGSize(CGSize size) -{ - return ASRelativeSizeMake(ASRelativeDimensionMakeWithPoints(size.width), - ASRelativeDimensionMakeWithPoints(size.height)); -} - -ASLayoutSize ASRelativeSizeMakeWithFraction(CGFloat fraction) -{ - return ASRelativeSizeMake(ASRelativeDimensionMakeWithFraction(fraction), - ASRelativeDimensionMakeWithFraction(fraction)); -} - -BOOL ASRelativeSizeEqualToRelativeSize(ASLayoutSize lhs, ASLayoutSize rhs) -{ - return ASDimensionEqualToDimension(lhs.width, rhs.width) - && ASDimensionEqualToDimension(lhs.height, rhs.height); -} - - -#pragma mark - ASRelativeSizeRange - -ASRelativeSizeRange ASRelativeSizeRangeMake(ASLayoutSize min, ASLayoutSize max) -{ - ASRelativeSizeRange sizeRange; sizeRange.min = min; sizeRange.max = max; return sizeRange; -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeSize(ASLayoutSize exact) -{ - return ASRelativeSizeRangeMake(exact, exact); -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactCGSize(CGSize exact) -{ - return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMakeWithCGSize(exact)); -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactFraction(CGFloat fraction) -{ - return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMakeWithFraction(fraction)); -} - -ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeDimensions(ASRelativeDimension exactWidth, ASRelativeDimension exactHeight) -{ - return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMake(exactWidth, exactHeight)); -} - -BOOL ASRelativeSizeRangeEqualToRelativeSizeRange(ASRelativeSizeRange lhs, ASRelativeSizeRange rhs) -{ - return ASRelativeSizeEqualToRelativeSize(lhs.min, rhs.min) && ASRelativeSizeEqualToRelativeSize(lhs.max, rhs.max); -} - -ASSizeRange ASRelativeSizeRangeResolve(ASRelativeSizeRange relativeSizeRange, - CGSize parentSize) -{ - return ASSizeRangeMake(ASLayoutSizeResolveSize(relativeSizeRange.min, parentSize, parentSize), - ASLayoutSizeResolveSize(relativeSizeRange.max, parentSize, parentSize)); -} diff --git a/Source/Layout/ASLayout.h b/Source/Layout/ASLayout.h index a0aff5b7b1..fa13719854 100644 --- a/Source/Layout/ASLayout.h +++ b/Source/Layout/ASLayout.h @@ -147,23 +147,6 @@ ASDISPLAYNODE_EXTERN_C_END @end -#pragma mark - Deprecated - -@interface ASLayout (Deprecated) - -- (id )layoutableObject ASDISPLAYNODE_DEPRECATED; - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size ASDISPLAYNODE_DEPRECATED; - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size - sublayouts:(nullable NSArray *)sublayouts AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED; - -@end - #pragma mark - Debugging @interface ASLayout (Debugging) diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 22904b8b6b..f6fbe91279 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -23,9 +23,10 @@ #import #import +#import #import #import -#import +#import CGPoint const ASPointNull = {NAN, NAN}; @@ -85,7 +86,7 @@ ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsFlattened(ASLayout *la */ @property (nonatomic, strong) NSMutableArray> *sublayoutLayoutElements; -@property (nonatomic, strong, readonly) ASRectTable, id> *elementToRectTable; +@property (nonatomic, strong, readonly) ASRectMap *elementToRectMap; @end @@ -142,9 +143,9 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( _sublayouts = sublayouts != nil ? [sublayouts copy] : @[]; if (_sublayouts.count > 0) { - _elementToRectTable = [ASRectTable rectTableForWeakObjectPointers]; + _elementToRectMap = [ASRectMap rectMapForWeakObjectPointers]; for (ASLayout *layout in sublayouts) { - [_elementToRectTable setRect:layout.frame forKey:layout.layoutElement]; + [_elementToRectMap setRect:layout.frame forKey:layout.layoutElement]; } } @@ -271,6 +272,28 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( return layout; } +#pragma mark - Equality Checking + +- (BOOL)isEqual:(id)object +{ + ASLayout *layout = ASDynamicCast(object, ASLayout); + if (layout == nil) { + return NO; + } + + if (!CGSizeEqualToSize(_size, layout.size)) return NO; + + if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position)) + || CGPointEqualToPoint(self.position, layout.position))) return NO; + if (_layoutElement != layout.layoutElement) return NO; + + if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) { + return NO; + } + + return YES; +} + #pragma mark - Accessors - (ASLayoutElementType)type @@ -280,7 +303,7 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( - (CGRect)frameForElement:(id)layoutElement { - return _elementToRectTable ? [_elementToRectTable rectForKey:layoutElement] : CGRectNull; + return _elementToRectMap ? [_elementToRectMap rectForKey:layoutElement] : CGRectNull; } - (CGRect)frame @@ -353,30 +376,6 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( @end -@implementation ASLayout (Deprecation) - -- (id )layoutableObject -{ - return self.layoutElement; -} - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size -{ - return [self layoutWithLayoutElement:layoutElement size:size]; -} - -+ (instancetype)layoutWithLayoutableObject:(id)layoutElement - constrainedSizeRange:(ASSizeRange)constrainedSizeRange - size:(CGSize)size - sublayouts:(nullable NSArray *)sublayouts -{ - return [self layoutWithLayoutElement:layoutElement size:size sublayouts:sublayouts]; -} - -@end - ASLayout *ASCalculateLayout(id layoutElement, const ASSizeRange sizeRange, const CGSize parentSize) { ASDisplayNodeCAssertNotNil(layoutElement, @"Not valid layoutElement passed in."); diff --git a/Source/Layout/ASLayoutElement.h b/Source/Layout/ASLayoutElement.h index 2ebd55a46c..ca56c67222 100644 --- a/Source/Layout/ASLayoutElement.h +++ b/Source/Layout/ASLayoutElement.h @@ -147,22 +147,6 @@ typedef NS_ENUM(NSUInteger, ASLayoutElementType) { - (BOOL)implementsLayoutMethod; -#pragma mark - Deprecated - -#define ASLayoutable ASLayoutElement - -/** - * @abstract Calculate a layout based on given size range. - * - * @param constrainedSize The minimum and maximum sizes the receiver should fit in. - * - * @return An ASLayout instance defining the layout of the receiver and its children. - * - * @deprecated Deprecated in version 2.0: Use layoutThatFits: or layoutThatFits:parentSize: if used in - * ASLayoutSpec subclasses - */ -- (nonnull ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize ASDISPLAYNODE_DEPRECATED_MSG("Use layoutThatFits: instead."); - @end #pragma mark - ASLayoutElementStyle diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index 8d647c09aa..9fc52b3bfb 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -804,22 +804,4 @@ do {\ #endif /* YOGA */ -#pragma mark Deprecated - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -- (ASRelativeSizeRange)sizeRange -{ - return ASRelativeSizeRangeMake(self.minLayoutSize, self.maxLayoutSize); -} - -- (void)setSizeRange:(ASRelativeSizeRange)sizeRange -{ - self.minLayoutSize = sizeRange.min; - self.maxLayoutSize = sizeRange.max; -} - -#pragma clang diagnostic pop - @end diff --git a/Source/Layout/ASLayoutElementPrivate.h b/Source/Layout/ASLayoutElementPrivate.h index 68c46de611..96c520e5d7 100644 --- a/Source/Layout/ASLayoutElementPrivate.h +++ b/Source/Layout/ASLayoutElementPrivate.h @@ -37,9 +37,9 @@ extern int32_t const ASLayoutElementContextDefaultTransitionID; // Does not currently support nesting – there must be no current context. extern void ASLayoutElementPushContext(ASLayoutElementContext * context); -extern ASLayoutElementContext * _Nullable ASLayoutElementGetCurrentContext(); +extern ASLayoutElementContext * _Nullable ASLayoutElementGetCurrentContext(void); -extern void ASLayoutElementPopContext(); +extern void ASLayoutElementPopContext(void); NS_ASSUME_NONNULL_END @@ -51,11 +51,6 @@ NS_ASSUME_NONNULL_END return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max];\ }\ \ -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize\ -{\ - return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max];\ -}\ -\ - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize\ {\ return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize];\ @@ -117,134 +112,3 @@ typedef struct ASLayoutElementStyleExtensions { return [self.style layoutOptionExtensionEdgeInsetsAtIndex:idx];\ }\ -#pragma mark ASLayoutElementStyleForwardingDeclaration (Deprecated) - -#define ASLayoutElementStyleForwardingDeclaration \ -@property (nonatomic, readwrite) CGFloat spacingBefore ASDISPLAYNODE_DEPRECATED_MSG("Use style.spacingBefore"); \ -@property (nonatomic, readwrite) CGFloat spacingAfter ASDISPLAYNODE_DEPRECATED_MSG("Use style.spacingAfter"); \ -@property (nonatomic, readwrite) CGFloat flexGrow ASDISPLAYNODE_DEPRECATED_MSG("Use style.flexGrow"); \ -@property (nonatomic, readwrite) CGFloat flexShrink ASDISPLAYNODE_DEPRECATED_MSG("Use style.flexShrink"); \ -@property (nonatomic, readwrite) ASDimension flexBasis ASDISPLAYNODE_DEPRECATED_MSG("Use style.flexBasis"); \ -@property (nonatomic, readwrite) ASStackLayoutAlignSelf alignSelf ASDISPLAYNODE_DEPRECATED_MSG("Use style.alignSelf"); \ -@property (nonatomic, readwrite) CGFloat ascender ASDISPLAYNODE_DEPRECATED_MSG("Use style.ascender"); \ -@property (nonatomic, readwrite) CGFloat descender ASDISPLAYNODE_DEPRECATED_MSG("Use style.descender"); \ -@property (nonatomic, assign) ASRelativeSizeRange sizeRange ASDISPLAYNODE_DEPRECATED_MSG("Don't use sizeRange anymore instead set style.width or style.height"); \ -@property (nonatomic, assign) CGPoint layoutPosition ASDISPLAYNODE_DEPRECATED_MSG("Use style.layoutPosition"); \ - - -#pragma mark - ASLayoutElementStyleForwarding (Deprecated) - -// For the time beeing we are forwading all style related properties on ASDisplayNode and ASLayoutSpec. This define -// help us to not have duplicate code while moving from 1.x to 2.0s -#define ASLayoutElementStyleForwarding \ -\ -@dynamic spacingBefore, spacingAfter, flexGrow, flexShrink, flexBasis, alignSelf, ascender, descender, sizeRange, layoutPosition;\ -\ -_Pragma("mark - ASStackLayoutElement")\ -\ -- (void)setSpacingBefore:(CGFloat)spacingBefore\ -{\ - self.style.spacingBefore = spacingBefore;\ -}\ -\ -- (CGFloat)spacingBefore\ -{\ - return self.style.spacingBefore;\ -}\ -\ -- (void)setSpacingAfter:(CGFloat)spacingAfter\ -{\ - self.style.spacingAfter = spacingAfter;\ -}\ -\ -- (CGFloat)spacingAfter\ -{\ - return self.style.spacingAfter;\ -}\ -\ -- (void)setFlexGrow:(CGFloat)flexGrow\ -{\ - self.style.flexGrow = flexGrow;\ -}\ -\ -- (CGFloat)flexGrow\ -{\ - return self.style.flexGrow;\ -}\ -\ -- (void)setFlexShrink:(CGFloat)flexShrink\ -{\ - self.style.flexShrink = flexShrink;\ -}\ -\ -- (CGFloat)flexShrink\ -{\ - return self.style.flexShrink;\ -}\ -\ -- (void)setFlexBasis:(ASDimension)flexBasis\ -{\ - self.style.flexBasis = flexBasis;\ -}\ -\ -- (ASDimension)flexBasis\ -{\ - return self.style.flexBasis;\ -}\ -\ -- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf\ -{\ - self.style.alignSelf = alignSelf;\ -}\ -\ -- (ASStackLayoutAlignSelf)alignSelf\ -{\ - return self.style.alignSelf;\ -}\ -\ -- (void)setAscender:(CGFloat)ascender\ -{\ - self.style.ascender = ascender;\ -}\ -\ -- (CGFloat)ascender\ -{\ - return self.style.ascender;\ -}\ -\ -- (void)setDescender:(CGFloat)descender\ -{\ - self.style.descender = descender;\ -}\ -\ -- (CGFloat)descender\ -{\ - return self.style.descender;\ -}\ -\ -_Pragma("mark - ASAbsoluteLayoutElement")\ -\ -- (void)setLayoutPosition:(CGPoint)layoutPosition\ -{\ - self.style.layoutPosition = layoutPosition;\ -}\ -\ -- (CGPoint)layoutPosition\ -{\ - return self.style.layoutPosition;\ -}\ -\ -_Pragma("clang diagnostic push")\ -_Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")\ -\ -- (void)setSizeRange:(ASRelativeSizeRange)sizeRange\ -{\ - self.style.sizeRange = sizeRange;\ -}\ -\ -- (ASRelativeSizeRange)sizeRange\ -{\ - return self.style.sizeRange;\ -}\ -\ -_Pragma("clang diagnostic pop")\ diff --git a/Source/Layout/ASLayoutSpec.h b/Source/Layout/ASLayoutSpec.h index e4d9d42db1..ac02bacd01 100644 --- a/Source/Layout/ASLayoutSpec.h +++ b/Source/Layout/ASLayoutSpec.h @@ -104,10 +104,4 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface ASLayoutSpec (Deprecated) - -ASLayoutElementStyleForwardingDeclaration - -@end - NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASLayoutSpec.mm b/Source/Layout/ASLayoutSpec.mm index 6600f50867..76901d1ac7 100644 --- a/Source/Layout/ASLayoutSpec.mm +++ b/Source/Layout/ASLayoutSpec.mm @@ -35,17 +35,6 @@ @dynamic layoutElementType; @synthesize debugName = _debugName; -#pragma mark - Class - -+ (void)initialize -{ - [super initialize]; - if (self != [ASLayoutSpec class]) { - ASDisplayNodeAssert(!ASSubclassOverridesSelector([ASLayoutSpec class], self, @selector(measureWithSizeRange:)), @"Subclass %@ must not override measureWithSizeRange: method. Instead override calculateLayoutThatFits:", NSStringFromClass(self)); - } -} - - #pragma mark - Lifecycle - (instancetype)init @@ -167,7 +156,6 @@ ASLayoutElementLayoutCalculationDefaults } ASPrimitiveTraitCollectionDefaults -ASPrimitiveTraitCollectionDeprecatedImplementation #pragma mark - ASLayoutElementStyleExtensibility @@ -353,11 +341,3 @@ ASLayoutElementStyleExtensibilityForwarding } @end - -#pragma mark - ASLayoutSpec (Deprecated) - -@implementation ASLayoutSpec (Deprecated) - -ASLayoutElementStyleForwarding - -@end diff --git a/Source/Layout/ASYogaUtilities.h b/Source/Layout/ASYogaUtilities.h index 2986c0195f..b229b34358 100644 --- a/Source/Layout/ASYogaUtilities.h +++ b/Source/Layout/ASYogaUtilities.h @@ -15,9 +15,11 @@ #if YOGA /* YOGA */ #import +#import #import -#define ASYogaLog(...) //NSLog(__VA_ARGS__) +// Should pass a string literal, not an NSString as the first argument to ASYogaLog +#define ASYogaLog(x, ...) as_log_verbose(ASLayoutLog(), x, ##__VA_ARGS__); @interface ASDisplayNode (YogaHelpers) diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index ffc9b28430..c02c6b5cd5 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -71,18 +71,35 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh - (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); - CGSize viewportSize = [self _viewportSize]; - CGPoint contentOffset = _collectionNode.contentOffset; + + Class layoutDelegateClass = [_layoutDelegate class]; + ASCollectionLayoutCache *layoutCache = _layoutCache; + ASCollectionNode *collectionNode = _collectionNode; + if (collectionNode == nil) { + return [[ASCollectionLayoutContext alloc] initWithViewportSize:CGSizeZero + initialContentOffset:CGPointZero + scrollableDirections:ASScrollDirectionNone + elements:[[ASElementMap alloc] init] + layoutDelegateClass:layoutDelegateClass + layoutCache:layoutCache + additionalInfo:nil]; + } + + ASScrollDirection scrollableDirections = [_layoutDelegate scrollableDirections]; + CGSize viewportSize = [ASCollectionLayout _viewportSizeForCollectionNode:collectionNode scrollableDirections:scrollableDirections]; + CGPoint contentOffset = collectionNode.contentOffset; + id additionalInfo = nil; if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } + return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize initialContentOffset:contentOffset - scrollableDirections:[_layoutDelegate scrollableDirections] + scrollableDirections:scrollableDirections elements:elements - layoutDelegateClass:[_layoutDelegate class] - layoutCache:_layoutCache + layoutDelegateClass:layoutDelegateClass + layoutCache:layoutCache additionalInfo:additionalInfo]; } @@ -210,21 +227,41 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { - return (! CGSizeEqualToSize([self _viewportSize], newBounds.size)); + return (! CGSizeEqualToSize([ASCollectionLayout _boundsForCollectionNode:_collectionNode], newBounds.size)); } #pragma mark - Private methods -- (CGSize)_viewportSize ++ (CGSize)_boundsForCollectionNode:(nonnull ASCollectionNode *)collectionNode { - ASCollectionNode *collectionNode = _collectionNode; - if (collectionNode != nil && !collectionNode.isNodeLoaded) { + if (collectionNode == nil) { + return CGSizeZero; + } + + if (!collectionNode.isNodeLoaded) { // TODO consider calculatedSize as well return collectionNode.threadSafeBounds.size; - } else { - ASDisplayNodeAssertMainThread(); - return self.collectionView.bounds.size; } + + ASDisplayNodeAssertMainThread(); + return collectionNode.view.bounds.size; +} + ++ (CGSize)_viewportSizeForCollectionNode:(nonnull ASCollectionNode *)collectionNode scrollableDirections:(ASScrollDirection)scrollableDirections +{ + if (collectionNode == nil) { + return CGSizeZero; + } + + CGSize result = [ASCollectionLayout _boundsForCollectionNode:collectionNode]; + // TODO: Consider using adjustedContentInset on iOS 11 and later, to include the safe area of the scroll view + UIEdgeInsets contentInset = collectionNode.contentInset; + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { + result.height -= (contentInset.top + contentInset.bottom); + } else { + result.width -= (contentInset.left + contentInset.right); + } + return result; } /** diff --git a/Source/Private/ASCollectionView+Undeprecated.h b/Source/Private/ASCollectionView+Undeprecated.h index 1602a5c11b..507b6c904f 100644 --- a/Source/Private/ASCollectionView+Undeprecated.h +++ b/Source/Private/ASCollectionView+Undeprecated.h @@ -76,6 +76,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id layoutInspector; +@property (nonatomic, assign) UIEdgeInsets contentInset; + @property (nonatomic, assign) CGPoint contentOffset; /** @@ -153,7 +155,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchAnimated:(BOOL)animated updates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Perform a batch of updates asynchronously. This method must be called from the main thread. @@ -164,7 +166,7 @@ NS_ASSUME_NONNULL_BEGIN * Boolean parameter that contains the value YES if all of the related animations completed successfully or * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. */ -- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; +- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)(void))updates completion:(nullable void (^)(BOOL finished))completion; /** * Triggers a relayout of all nodes. @@ -295,6 +297,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode AS_WARN_UNUSED_RESULT; +/** + * Invalidates and recalculates the cached sizes stored for pass-through cells used in interop mode. + */ +- (void)invalidateFlowLayoutDelegateMetrics; + - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; @end diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index 3e0fb5ab2e..963a542f85 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -23,6 +23,8 @@ #import #import #import +#import + @interface ASDrawingContext : NSObject @@ -318,6 +320,8 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + ASDisplayNodeAssertMainThread(); + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; ASDisplayNodeFlags flags; @@ -339,6 +343,9 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c BOOL opaque = self.opaque; CGRect bounds = self.bounds; + UIColor *backgroundColor = self.backgroundColor; + CGColorRef borderColor = self.borderColor; + CGFloat borderWidth = self.borderWidth; CGFloat contentsScaleForDisplay = _contentsScaleForDisplay; __instanceLock__.unlock(); @@ -363,7 +370,7 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c // If [UIColor clearColor] or another semitransparent background color is used, include alpha channel when rasterizing. // Unlike CALayer drawing, we include the backgroundColor as a base during rasterization. - opaque = opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f; + opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f; displayBlock = ^id{ CHECK_CANCELLED_AND_RETURN_NIL(); @@ -406,22 +413,10 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c CGContextRef currentContext = UIGraphicsGetCurrentContext(); UIImage *image = nil; - - ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = nil; - ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = nil; - if (currentContext) { - __instanceLock__.lock(); - willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; - didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; - __instanceLock__.unlock(); - } - - + // For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or // _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs. - if (willDisplayNodeContentWithRenderingContext != nil) { - willDisplayNodeContentWithRenderingContext(currentContext, drawParameters); - } + [self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters]; if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly. image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock]; @@ -429,9 +424,7 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c [self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; } - if (didDisplayNodeContentWithRenderingContext != nil) { - didDisplayNodeContentWithRenderingContext(currentContext, drawParameters); - } + [self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor]; if (shouldCreateGraphicsContext) { CHECK_CANCELLED_AND_RETURN_NIL( [context popCurrent]; ); @@ -465,6 +458,91 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c return displayBlock; } +- (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawParameters:(id _Nullable)drawParameters +{ + if (context) { + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) { + ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + // TODO: This clip path should be removed if we are rasterizing. + CGRect boundingBox = CGContextGetClipBoundingBox(context); + [[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip]; + } + + if (willDisplayNodeContentWithRenderingContext) { + willDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + +} +- (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:(UIImage **)image drawParameters:(id _Nullable)drawParameters backgroundColor:(UIColor *)backgroundColor borderWidth:(CGFloat)borderWidth borderColor:(CGColorRef)borderColor +{ + if (context == NULL && *image == NULL) { + return; + } + + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + CGFloat contentsScale = _contentsScaleForDisplay; + ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (context != NULL) { + if (didDisplayNodeContentWithRenderingContext) { + didDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) { + CGRect bounds = CGRectZero; + if (context == NULL) { + bounds = self.threadSafeBounds; + bounds.size.width *= contentsScale; + bounds.size.height *= contentsScale; + CGFloat white = 0.0f, alpha = 0.0f; + [backgroundColor getWhite:&white alpha:&alpha]; + UIGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale); + [*image drawInRect:bounds]; + } else { + bounds = CGContextGetClipBoundingBox(context); + } + + ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + + UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds]; + [roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]]; + roundedHole.usesEvenOddFillRule = YES; + + UIBezierPath *roundedPath = nil; + if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0 + CGFloat strokeThickness = borderWidth * contentsScale; + CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f; + roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset) + cornerRadius:_cornerRadius * contentsScale]; + roundedPath.lineWidth = strokeThickness; + [[UIColor colorWithCGColor:borderColor] setStroke]; + } + + // Punch out the corners by copying the backgroundColor over them. + // This works for everything from clearColor to opaque colors. + [backgroundColor setFill]; + [roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f]; + + [roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil. + + if (*image) { + *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + } +} + - (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously { ASDisplayNodeAssertMainThread(); @@ -477,6 +555,7 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c } CALayer *layer = _layer; + BOOL rasterizesSubtree = _flags.rasterizesSubtree; __instanceLock__.unlock(); @@ -507,7 +586,7 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c return; } - ASDisplayNodeAssert(_layer, @"Expect _layer to be not nil"); + ASDisplayNodeAssert(layer, @"Expect _layer to be not nil"); // This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ @@ -522,11 +601,23 @@ static void DrawingContextDataProviderReleaseDataCallback(void *info, __unused c layer.contents = (id)image.CGImage; } [self didDisplayAsyncLayer:self.asyncLayer]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node didDisplayAsyncLayer:node.asyncLayer]; + }); + } } }; // Call willDisplay immediately in either case [self willDisplayAsyncLayer:self.asyncLayer asynchronously:asynchronously]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node willDisplayAsyncLayer:node.asyncLayer asynchronously:asynchronously]; + }); + } if (asynchronously) { // Async rendering operations are contained by a transaction, which allows them to proceed and concurrently diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index ebcaf23c1c..dbb5207879 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -99,6 +99,29 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]]; } +#define HIERARCHY_STATE_DELTA(Name) ({ \ + if ((oldState & ASHierarchyState##Name) != (newState & ASHierarchyState##Name)) { \ + [changes appendFormat:@"%c%s ", (newState & ASHierarchyState##Name ? '+' : '-'), #Name]; \ + } \ +}) + +__unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarchyState oldState, ASHierarchyState newState) +{ + if (oldState == newState) { + return @"{ }"; + } + + NSMutableString *changes = [NSMutableString stringWithString:@"{ "]; + HIERARCHY_STATE_DELTA(Rasterized); + HIERARCHY_STATE_DELTA(RangeManaged); + HIERARCHY_STATE_DELTA(TransitioningSupernodes); + HIERARCHY_STATE_DELTA(LayoutPending); + [changes appendString:@"}"]; + return changes; +} + +#undef HIERARCHY_STATE_DELTA + @interface ASDisplayNode () { @protected @@ -225,7 +248,7 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. */ -- (void)_setNeedsLayoutFromAbove; +- (void)_u_setNeedsLayoutFromAbove; /** * @abstract Subclass hook for nodes that are acting as root nodes. This method is called if one of the subnodes @@ -237,7 +260,7 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat * This method will confirm that the layout is up to date (and update if needed). * Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). */ -- (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds; +- (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds; /** * Layout all of the subnodes based on the sublayouts diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index 88cb4fda2a..491fb38941 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -185,14 +185,30 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo - (CGFloat)cornerRadius { - _bridge_prologue_read; - return _getFromLayer(cornerRadius); + ASDN::MutexLocker l(__instanceLock__); + if (_cornerRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + return self.layerCornerRadius; + } else { + return _cornerRadius; + } } - (void)setCornerRadius:(CGFloat)newCornerRadius { - _bridge_prologue_write; - _setToLayer(cornerRadius, newCornerRadius); + ASDN::MutexLocker l(__instanceLock__); + [self updateCornerRoundingWithType:_cornerRoundingType cornerRadius:newCornerRadius]; +} + +- (ASCornerRoundingType)cornerRoundingType +{ + ASDN::MutexLocker l(__instanceLock__); + return _cornerRoundingType; +} + +- (void)setCornerRoundingType:(ASCornerRoundingType)newRoundingType +{ + ASDN::MutexLocker l(__instanceLock__); + [self updateCornerRoundingWithType:newRoundingType cornerRadius:_cornerRadius]; } - (NSString *)contentsGravity @@ -860,6 +876,21 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo @end +@implementation ASDisplayNode (InternalPropertyBridge) + +- (CGFloat)layerCornerRadius +{ + _bridge_prologue_read; + return _getFromLayer(cornerRadius); +} + +- (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius +{ + _bridge_prologue_write; + _setToLayer(cornerRadius, newLayerCornerRadius); +} + +@end #pragma mark - UIViewBridgeAccessibility @@ -882,6 +913,13 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi @implementation ASDisplayNode (UIViewBridgeAccessibility) +// iOS 11 only properties. Add this to silence "unimplemented selector" warnings +// in old SDKs. If the caller doesn't respect our API_AVAILABLE attributes, then they +// get an appropriate "unrecognized selector" runtime error. +#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_11_0 +@dynamic accessibilityAttributedLabel, accessibilityAttributedHint, accessibilityAttributedValue; +#endif + - (BOOL)isAccessibilityElement { _bridge_prologue_read; @@ -904,8 +942,29 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityLabel, accessibilityLabel, accessibilityLabel); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS(11)) { + NSAttributedString *accessibilityAttributedLabel = accessibilityLabel ? [[NSAttributedString alloc] initWithString:accessibilityLabel] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); + } +#endif } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel); +} + +- (void)setAccessibilityAttributedLabel:(NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); } + { _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityAttributedLabel.string, accessibilityLabel, accessibilityAttributedLabel.string); } +} +#endif + - (NSString *)accessibilityHint { _bridge_prologue_read; @@ -916,8 +975,30 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityHint, accessibilityHint, accessibilityHint); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS(11)) { + NSAttributedString *accessibilityAttributedHint = accessibilityHint ? [[NSAttributedString alloc] initWithString:accessibilityHint] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); + } +#endif } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedHint, accessibilityAttributedHint); +} + +- (void)setAccessibilityAttributedHint:(NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); } + + { _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityAttributedHint.string, accessibilityHint, accessibilityAttributedHint.string); } +} +#endif + - (NSString *)accessibilityValue { _bridge_prologue_read; @@ -928,8 +1009,29 @@ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, vi { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityValue, accessibilityValue, accessibilityValue); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS(11)) { + NSAttributedString *accessibilityAttributedValue = accessibilityValue ? [[NSAttributedString alloc] initWithString:accessibilityValue] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); + } +#endif } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedValue, accessibilityAttributedValue); +} + +- (void)setAccessibilityAttributedValue:(NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); } + { _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityAttributedValue.string, accessibilityValue, accessibilityAttributedValue.string); } +} +#endif + - (UIAccessibilityTraits)accessibilityTraits { _bridge_prologue_read; diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 36683954dd..1be9e8c94c 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -52,8 +52,6 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4, ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5, ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6, - ASDisplayNodeMethodOverrideFetchData = 1 << 7, - ASDisplayNodeMethodOverrideClearFetchedData = 1 << 8 }; typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags) @@ -175,6 +173,10 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo // keeps track of nodes/subnodes that have not finished display, used with placeholders ASWeakSet *_pendingDisplayNodes; + + CGFloat _cornerRadius; + ASCornerRoundingType _cornerRoundingType; + CALayer *_clipCornerLayers[4]; ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; @@ -182,8 +184,11 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo // Accessibility support BOOL _isAccessibilityElement; NSString *_accessibilityLabel; + NSAttributedString *_accessibilityAttributedLabel; NSString *_accessibilityHint; + NSAttributedString *_accessibilityAttributedHint; NSString *_accessibilityValue; + NSAttributedString *_accessibilityAttributedValue; UIAccessibilityTraits _accessibilityTraits; CGRect _accessibilityFrame; NSString *_accessibilityLanguage; @@ -276,6 +281,9 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo /// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. - (void)displayImmediately; +/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition radius or rounding type. +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius; + /// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. - (instancetype)initWithViewClass:(Class)viewClass; @@ -320,4 +328,10 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo @end +@interface ASDisplayNode (InternalPropertyBridge) + +@property (nonatomic, assign) CGFloat layerCornerRadius; + +@end + NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASImageNode+AnimatedImagePrivate.h b/Source/Private/ASImageNode+AnimatedImagePrivate.h index 6c08ce617e..bd2a4d3099 100644 --- a/Source/Private/ASImageNode+AnimatedImagePrivate.h +++ b/Source/Private/ASImageNode+AnimatedImagePrivate.h @@ -21,12 +21,12 @@ extern NSString *const ASAnimatedImageDefaultRunLoopMode; @interface ASImageNode () { - ASDN::RecursiveMutex _animatedImageLock; ASDN::Mutex _displayLinkLock; id _animatedImage; BOOL _animatedImagePaused; NSString *_animatedImageRunLoopMode; CADisplayLink *_displayLink; + NSUInteger _lastSuccessfulFrameIndex; //accessed on main thread only CFTimeInterval _playHead; diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index 31766b366a..dc67700e13 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -32,15 +32,15 @@ BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL sele IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block); /// Dispatches the given block to the main queue if not already running on the main thread -void ASPerformBlockOnMainThread(void (^block)()); +void ASPerformBlockOnMainThread(void (^block)(void)); /// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue -void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT +void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT /// For deallocation of objects on a background thread without GCD overhead / thread explosion -void ASPerformBackgroundDeallocation(id object); +void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object); -CGFloat ASScreenScale(); +CGFloat ASScreenScale(void); CGSize ASFloorSizeValues(CGSize s); @@ -80,7 +80,7 @@ ASDISPLAYNODE_INLINE BOOL ASImageAlphaInfoIsOpaque(CGImageAlphaInfo info) { @param withoutAnimation Set to `YES` to perform given block without animation @param block Perform UIView geometry changes within the passed block */ -ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { +ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)(void)) { if (withoutAnimation) { [UIView performWithoutAnimation:block]; } else { diff --git a/Source/Private/ASInternalHelpers.m b/Source/Private/ASInternalHelpers.m index 088ddccea7..ad55f295aa 100644 --- a/Source/Private/ASInternalHelpers.m +++ b/Source/Private/ASInternalHelpers.m @@ -60,7 +60,7 @@ IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block) } } -void ASPerformBlockOnMainThread(void (^block)()) +void ASPerformBlockOnMainThread(void (^block)(void)) { if (block == nil){ return; @@ -72,7 +72,7 @@ void ASPerformBlockOnMainThread(void (^block)()) } } -void ASPerformBlockOnBackgroundThread(void (^block)()) +void ASPerformBlockOnBackgroundThread(void (^block)(void)) { if (block == nil){ return; @@ -84,13 +84,14 @@ void ASPerformBlockOnBackgroundThread(void (^block)()) } } -void ASPerformBackgroundDeallocation(id object) +void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object) { [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object]; } BOOL ASClassRequiresMainThreadDeallocation(Class c) { + // Specific classes if (c == [UIImage class] || c == [UIColor class]) { return NO; } @@ -101,10 +102,16 @@ BOOL ASClassRequiresMainThreadDeallocation(Class c) return YES; } + // Apple classes with prefix const char *name = class_getName(c); if (strncmp(name, "UI", 2) == 0 || strncmp(name, "AV", 2) == 0 || strncmp(name, "CA", 2) == 0) { return YES; } + + // Specific Texture classes + if (strncmp(name, "ASTextKitComponents", 19) == 0) { + return YES; + } return NO; } @@ -113,14 +120,14 @@ Class _Nullable ASGetClassFromType(const char * _Nullable type) { // Class types all start with @" if (type == NULL || strncmp(type, "@\"", 2) != 0) { - return nil; + return Nil; } // Ensure length >= 3 size_t typeLength = strlen(type); if (typeLength < 3) { ASDisplayNodeCFailAssert(@"Got invalid type-encoding: %s", type); - return nil; + return Nil; } // Copy type[2..(end-1)]. So @"UIImage" -> UIImage diff --git a/Source/Private/ASMutableElementMap.h b/Source/Private/ASMutableElementMap.h index 6d20b12e85..2e5ceb5452 100644 --- a/Source/Private/ASMutableElementMap.h +++ b/Source/Private/ASMutableElementMap.h @@ -39,10 +39,10 @@ AS_SUBCLASSING_RESTRICTED - (void)insertSection:(ASSection *)section atIndex:(NSInteger)index; -- (void)removeAllSectionContexts; +- (void)removeAllSections; /// Only modifies the array of ASSection * objects -- (void)removeSectionContextsAtIndexes:(NSIndexSet *)indexes; +- (void)removeSectionsAtIndexes:(NSIndexSet *)indexes; - (void)removeAllElements; diff --git a/Source/Private/ASMutableElementMap.m b/Source/Private/ASMutableElementMap.m index 20e74f5f91..9a29cc0720 100644 --- a/Source/Private/ASMutableElementMap.m +++ b/Source/Private/ASMutableElementMap.m @@ -50,7 +50,7 @@ typedef NSMutableDictionary +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A category for indexing weak pointers to CGRects. Similar to ASIntegerMap. + */ +@interface ASRectMap : NSObject + +/** + * Creates a new rect map. The keys are never retained. + */ ++ (ASRectMap *)rectMapForWeakObjectPointers; + +/** + * Retrieves the rect for a given key, or CGRectNull if the key is not found. + * + * @param key An object to lookup the rect for. + */ +- (CGRect)rectForKey:(id)key; + +/** + * Sets the given rect for the associated key. Key *will not be retained!* + * + * @param rect The rect to store as value. + * @param key The key to use for the rect. + */ +- (void)setRect:(CGRect)rect forKey:(id)key; + +/** + * Removes the rect for the given key, if one exists. + * + * @param key The key to remove. + */ +- (void)removeRectForKey:(id)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASRectMap.mm b/Source/Private/ASRectMap.mm new file mode 100644 index 0000000000..cb76810806 --- /dev/null +++ b/Source/Private/ASRectMap.mm @@ -0,0 +1,78 @@ +// +// ASRectMap.mm +// Texture +// +// Copyright (c) 2017-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 "ASRectMap.h" +#import "ASObjectDescriptionHelpers.h" +#import +#import + +@implementation ASRectMap { + std::unordered_map _map; +} + ++ (ASRectMap *)rectMapForWeakObjectPointers +{ + return [[self alloc] init]; +} + +- (CGRect)rectForKey:(id)key +{ + auto result = _map.find((__bridge void *)key); + if (result != _map.end()) { + // result->first is the key; result->second is the value, a CGRect. + return result->second; + } else { + return CGRectNull; + } +} + +- (void)setRect:(CGRect)rect forKey:(id)key +{ + if (key) { + _map[(__bridge void *)key] = rect; + } +} + +- (void)removeRectForKey:(id)key +{ + if (key) { + _map.erase((__bridge void *)key); + } +} + +- (id)copyWithZone:(NSZone *)zone +{ + ASRectMap *copy = [ASRectMap rectMapForWeakObjectPointers]; + copy->_map = _map; + return copy; +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + // { ptr1->rect1 ptr2->rect2 ptr3->rect3 } + NSMutableString *str = [NSMutableString string]; + for (auto it = _map.begin(); it != _map.end(); it++) { + [str appendFormat:@" %@->%@", it->first, NSStringFromCGRect(it->second)]; + } + [result addObject:@{ @"ASRectMap": str }]; + + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMakeWithoutObject([self propertiesForDescription]); +} + +@end diff --git a/Source/Private/ASRectTable.h b/Source/Private/ASRectTable.h deleted file mode 100644 index af47f59b6a..0000000000 --- a/Source/Private/ASRectTable.h +++ /dev/null @@ -1,73 +0,0 @@ -// -// ASRectTable.h -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. 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 -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * An alias for an NSMapTable created to store rects. - * - * You should not call -objectForKey:, -setObject:forKey:, or -allObjects - * on these objects. - */ -typedef NSMapTable ASRectTable; - -/** - * A category for creating & using map tables meant for storing CGRects. - * - * This category is private, so name collisions are not worth worrying about. - */ -@interface NSMapTable (ASRectTableMethods) - -/** - * Creates a new rect table with (NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) for keys. - */ -+ (ASRectTable *)rectTableForStrongObjectPointers; - -/** - * Creates a new rect table with (NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) for keys. - */ -+ (ASRectTable *)rectTableForWeakObjectPointers; - -/** - * Retrieves the rect for a given key, or CGRectNull if the key is not found. - * - * @param key An object to lookup the rect for. - */ -- (CGRect)rectForKey:(KeyType)key; - -/** - * Sets the given rect for the associated key. - * - * @param rect The rect to store as value. - * @param key The key to use for the rect. - */ -- (void)setRect:(CGRect)rect forKey:(KeyType)key; - -/** - * Removes the rect for the given key, if one exists. - * - * @param key The key to remove. - */ -- (void)removeRectForKey:(KeyType)key; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASRectTable.m b/Source/Private/ASRectTable.m deleted file mode 100644 index 5a98a3f702..0000000000 --- a/Source/Private/ASRectTable.m +++ /dev/null @@ -1,80 +0,0 @@ -// -// ASRectTable.m -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. 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 "ASRectTable.h" - -__attribute__((const)) -static NSUInteger ASRectSize(const void *ptr) -{ - return sizeof(CGRect); -} - -@implementation NSMapTable (ASRectTableMethods) - -+ (NSMapTable *)rectTableWithKeyPointerFunctions:(NSPointerFunctions *)keyFuncs -{ - static NSPointerFunctions *cgRectFuncs; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - cgRectFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStructPersonality | NSPointerFunctionsCopyIn | NSPointerFunctionsMallocMemory]; - cgRectFuncs.sizeFunction = &ASRectSize; - }); - - return [[NSMapTable alloc] initWithKeyPointerFunctions:keyFuncs valuePointerFunctions:cgRectFuncs capacity:0]; -} - -+ (NSMapTable *)rectTableForStrongObjectPointers -{ - static NSPointerFunctions *strongObjectPointerFuncs; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - strongObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; - }); - return [self rectTableWithKeyPointerFunctions:strongObjectPointerFuncs]; -} - -+ (NSMapTable *)rectTableForWeakObjectPointers -{ - static NSPointerFunctions *weakObjectPointerFuncs; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - weakObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality]; - }); - return [self rectTableWithKeyPointerFunctions:weakObjectPointerFuncs]; -} - -- (CGRect)rectForKey:(id)key -{ - CGRect *ptr = (__bridge CGRect *)[self objectForKey:key]; - if (ptr == NULL) { - return CGRectNull; - } - return *ptr; -} - -- (void)setRect:(CGRect)rect forKey:(id)key -{ - __unsafe_unretained id obj = (__bridge id)▭ - [self setObject:obj forKey:key]; -} - -- (void)removeRectForKey:(id)key -{ - [self removeObjectForKey:key]; -} - -@end diff --git a/Source/Private/ASSection.h b/Source/Private/ASSection.h index 57df74c6fa..889be53dc9 100644 --- a/Source/Private/ASSection.h +++ b/Source/Private/ASSection.h @@ -18,17 +18,31 @@ #ifndef MINIMAL_ASDK #import +#import @protocol ASSectionContext; +NS_ASSUME_NONNULL_BEGIN + +/** + * An object representing the metadata for a section of elements in a collection. + * + * Its sectionID is namespaced to the data controller that created the section. + * + * These are useful for tracking the movement & lifetime of sections, independent of + * their contents. + */ +AS_SUBCLASSING_RESTRICTED @interface ASSection : NSObject -@property (nonatomic, assign, readonly) NSInteger sectionID; -@property (nonatomic, strong, nullable, readonly) id context; +@property (assign, readonly) NSInteger sectionID; +@property (strong, nullable, readonly) id context; -- (nullable instancetype)init __unavailable; -- (nullable instancetype)initWithSectionID:(NSInteger)sectionID context:(nullable id)context NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithSectionID:(NSInteger)sectionID context:(nullable id)context NS_DESIGNATED_INITIALIZER; @end +NS_ASSUME_NONNULL_END + #endif diff --git a/Source/Private/ASTableView+Undeprecated.h b/Source/Private/ASTableView+Undeprecated.h index 70098940fe..7ddc9c28e7 100644 --- a/Source/Private/ASTableView+Undeprecated.h +++ b/Source/Private/ASTableView+Undeprecated.h @@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id asyncDelegate; @property (nonatomic, weak) id asyncDataSource; +@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) CGPoint contentOffset; @property (nonatomic, assign) BOOL automaticallyAdjustsContentOffset; @property (nonatomic, assign) BOOL inverted; @@ -145,7 +146,7 @@ NS_ASSUME_NONNULL_BEGIN * the main thread. * @warning This method is substantially more expensive than UITableView's version. */ --(void)reloadDataWithCompletion:(void (^ _Nullable)())completion; +-(void)reloadDataWithCompletion:(void (^ _Nullable)(void))completion; /** * Reload everything from scratch, destroying the working range and all cached nodes. @@ -154,14 +155,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData; -/** - * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. - * - * @warning This method is substantially more expensive than UITableView's version and will block the main thread while - * all the cells load. - */ -- (void)reloadDataImmediately; - /** * Triggers a relayout of all nodes. * diff --git a/Source/Private/ASWeakMap.h b/Source/Private/ASWeakMap.h index fb53de15a2..18bf07184f 100644 --- a/Source/Private/ASWeakMap.h +++ b/Source/Private/ASWeakMap.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED @interface ASWeakMapEntry : NSObject -@property (nonatomic, retain, readonly) Value value; +@property (atomic, strong, readonly) Value value; @end @@ -49,7 +49,7 @@ AS_SUBCLASSING_RESTRICTED * The underlying storage is a hash table and the Key type should implement `hash` and `isEqual:`. */ AS_SUBCLASSING_RESTRICTED -@interface ASWeakMap<__covariant Key : NSObject *, Value> : NSObject +@interface ASWeakMap<__covariant Key, Value> : NSObject /** * Read from the cache. The Value object is accessible from the returned ASWeakMapEntry. diff --git a/Source/Private/ASWeakMap.m b/Source/Private/ASWeakMap.m index 2d9eb55230..5d5b3dc1ad 100644 --- a/Source/Private/ASWeakMap.m +++ b/Source/Private/ASWeakMap.m @@ -18,12 +18,13 @@ #import @interface ASWeakMapEntry () -@property (nonatomic, strong) NSObject *key; +@property (nonatomic, strong, readonly) id key; +@property (atomic, strong) id value; @end @implementation ASWeakMapEntry -- (instancetype)initWithKey:(NSObject *)key value:(NSObject *)value +- (instancetype)initWithKey:(id)key value:(id)value { self = [super init]; if (self) { @@ -33,16 +34,11 @@ return self; } -- (void)setValue:(NSObject *)value -{ - _value = value; -} - @end @interface ASWeakMap () -@property (nonatomic, strong) NSMapTable *hashTable; +@property (nonatomic, strong, readonly) NSMapTable *hashTable; @end /** @@ -69,12 +65,12 @@ return self; } -- (ASWeakMapEntry *)entryForKey:(NSObject *)key +- (ASWeakMapEntry *)entryForKey:(id)key { return [self.hashTable objectForKey:key]; } -- (ASWeakMapEntry *)setObject:(NSObject *)value forKey:(NSObject *)key +- (ASWeakMapEntry *)setObject:(id)value forKey:(id)key { ASWeakMapEntry *entry = [self.hashTable objectForKey:key]; if (entry != nil) { diff --git a/Source/Private/Layout/ASStackUnpositionedLayout.mm b/Source/Private/Layout/ASStackUnpositionedLayout.mm index 54a17bb089..1dff933485 100644 --- a/Source/Private/Layout/ASStackUnpositionedLayout.mm +++ b/Source/Private/Layout/ASStackUnpositionedLayout.mm @@ -71,7 +71,7 @@ static ASLayout *crossChildLayout(const ASStackLayoutSpecChild &child, crossMax); const ASSizeRange childSizeRange = directionSizeRange(style.direction, stackMin, stackMax, childCrossMin, childCrossMax); ASLayout *layout = [child.element layoutThatFits:childSizeRange parentSize:parentSize]; - ASDisplayNodeCAssertNotNil(layout, @"ASLayout returned from measureWithSizeRange: must not be nil: %@", child.element); + ASDisplayNodeCAssertNotNil(layout, @"ASLayout returned from -layoutThatFits:parentSize: must not be nil: %@", child.element); return layout ? : [ASLayout layoutWithLayoutElement:child.element size:{0, 0}]; } diff --git a/Source/Private/TextExperiment/Component/ASTextDebugOption.m b/Source/Private/TextExperiment/Component/ASTextDebugOption.m index e823d328b1..2e0a706eaa 100755 --- a/Source/Private/TextExperiment/Component/ASTextDebugOption.m +++ b/Source/Private/TextExperiment/Component/ASTextDebugOption.m @@ -16,14 +16,14 @@ static pthread_mutex_t _sharedDebugLock; static CFMutableSetRef _sharedDebugTargets = nil; static ASTextDebugOption *_sharedDebugOption = nil; -static const void* _sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) { +static const void* _as_sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) { return value; } -static void _sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) { +static void _as_sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) { } -void _sharedDebugSetFunction(const void *value, void *context) { +void _as_sharedDebugSetFunction(const void *value, void *context) { id target = (__bridge id)(value); [target setDebugOption:_sharedDebugOption]; } @@ -33,8 +33,8 @@ static void _initSharedDebug() { dispatch_once(&onceToken, ^{ pthread_mutex_init(&_sharedDebugLock, NULL); CFSetCallBacks callbacks = kCFTypeSetCallBacks; - callbacks.retain = _sharedDebugSetRetain; - callbacks.release = _sharedDebugSetRelease; + callbacks.retain = _as_sharedDebugSetRetain; + callbacks.release = _as_sharedDebugSetRelease; _sharedDebugTargets = CFSetCreateMutable(CFAllocatorGetDefault(), 0, &callbacks); }); } @@ -43,7 +43,7 @@ static void _setSharedDebugOption(ASTextDebugOption *option) { _initSharedDebug(); pthread_mutex_lock(&_sharedDebugLock); _sharedDebugOption = option.copy; - CFSetApplyFunction(_sharedDebugTargets, _sharedDebugSetFunction, NULL); + CFSetApplyFunction(_sharedDebugTargets, _as_sharedDebugSetFunction, NULL); pthread_mutex_unlock(&_sharedDebugLock); } diff --git a/Source/Private/TextExperiment/Utility/ASTextUtilities.h b/Source/Private/TextExperiment/Utility/ASTextUtilities.h index c434cc9012..024fb269c4 100755 --- a/Source/Private/TextExperiment/Utility/ASTextUtilities.h +++ b/Source/Private/TextExperiment/Utility/ASTextUtilities.h @@ -164,13 +164,13 @@ static inline CGRect ASTextEmojiGetGlyphBoundingRectWithFontSize(CGFloat fontSiz Get the character set which should rotate in vertical form. @return The shared character set. */ -NSCharacterSet *ASTextVerticalFormRotateCharacterSet(); +NSCharacterSet *ASTextVerticalFormRotateCharacterSet(void); /** Get the character set which should rotate and move in vertical form. @return The shared character set. */ -NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet(); +NSCharacterSet *ASTextVerticalFormRotateAndMoveCharacterSet(void); /// Get the transform rotation. diff --git a/Source/Private/_ASCollectionGalleryLayoutItem.mm b/Source/Private/_ASCollectionGalleryLayoutItem.mm index 8a5605593d..d503b6cef9 100644 --- a/Source/Private/_ASCollectionGalleryLayoutItem.mm +++ b/Source/Private/_ASCollectionGalleryLayoutItem.mm @@ -40,7 +40,6 @@ ASLayoutElementStyleExtensibilityForwarding ASPrimitiveTraitCollectionDefaults -ASPrimitiveTraitCollectionDeprecatedImplementation - (ASTraitCollection *)asyncTraitCollection { diff --git a/Source/Private/_ASHierarchyChangeSet.mm b/Source/Private/_ASHierarchyChangeSet.mm index d6e4324b0b..77ad5d959d 100644 --- a/Source/Private/_ASHierarchyChangeSet.mm +++ b/Source/Private/_ASHierarchyChangeSet.mm @@ -25,19 +25,11 @@ #import #import -// If assertions are enabled and they haven't forced us to suppress the exception, -// then throw, otherwise log. +// If assertions are enabled, throw. Otherwise log. #if ASDISPLAYNODE_ASSERTIONS_ENABLED #define ASFailUpdateValidation(...)\ - _Pragma("clang diagnostic push")\ - _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")\ - if ([ASDisplayNode suppressesInvalidCollectionUpdateExceptions]) {\ - NSLog(__VA_ARGS__);\ - } else {\ - NSLog(__VA_ARGS__);\ - [NSException raise:@"ASCollectionInvalidUpdateException" format:__VA_ARGS__];\ - }\ - _Pragma("clang diagnostic pop") + NSLog(__VA_ARGS__);\ + [NSException raise:ASCollectionInvalidUpdateException format:__VA_ARGS__]; #else #define ASFailUpdateValidation(...) NSLog(__VA_ARGS__); #endif diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 411a1f9f8d..4374401d87 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -20,8 +20,9 @@ #import #import #import -#import +#import #import +#import #define __shouldSetNeedsDisplay(layer) (flags.needsDisplay \ || (flags.setOpaque && opaque != (layer).opaque)\ @@ -73,8 +74,11 @@ typedef struct { int setEdgeAntialiasingMask:1; int setIsAccessibilityElement:1; int setAccessibilityLabel:1; + int setAccessibilityAttributedLabel:1; int setAccessibilityHint:1; + int setAccessibilityAttributedHint:1; int setAccessibilityValue:1; + int setAccessibilityAttributedValue:1; int setAccessibilityTraits:1; int setAccessibilityFrame:1; int setAccessibilityLanguage:1; @@ -121,8 +125,11 @@ typedef struct { BOOL asyncTransactionContainer; BOOL isAccessibilityElement; NSString *accessibilityLabel; + NSAttributedString *accessibilityAttributedLabel; NSString *accessibilityHint; + NSAttributedString *accessibilityAttributedHint; NSString *accessibilityValue; + NSAttributedString *accessibilityAttributedValue; UIAccessibilityTraits accessibilityTraits; CGRect accessibilityFrame; NSString *accessibilityLanguage; @@ -134,7 +141,7 @@ typedef struct { NSArray *accessibilityHeaderElements; CGPoint accessibilityActivationPoint; UIBezierPath *accessibilityPath; - UISemanticContentAttribute semanticContentAttribute; + UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); ASPendingStateFlags _flags; } @@ -271,8 +278,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; borderColor = blackColorRef; isAccessibilityElement = NO; accessibilityLabel = nil; + accessibilityAttributedLabel = nil; accessibilityHint = nil; + accessibilityAttributedHint = nil; accessibilityValue = nil; + accessibilityAttributedValue = nil; accessibilityTraits = UIAccessibilityTraitNone; accessibilityFrame = CGRectZero; accessibilityLanguage = nil; @@ -285,7 +295,9 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; accessibilityActivationPoint = CGPointZero; accessibilityPath = nil; edgeAntialiasingMask = (kCALayerLeftEdge | kCALayerRightEdge | kCALayerTopEdge | kCALayerBottomEdge); - semanticContentAttribute = UISemanticContentAttributeUnspecified; + if (AS_AVAILABLE_IOS(9)) { + semanticContentAttribute = UISemanticContentAttributeUnspecified; + } return self; } @@ -563,7 +575,7 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; _flags.setAsyncTransactionContainer = YES; } -- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute { +- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AVAILABLE(ios(9.0), tvos(9.0)) { semanticContentAttribute = attribute; _flags.setSemanticContentAttribute = YES; } @@ -586,9 +598,26 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityLabel:(NSString *)newAccessibilityLabel { - _flags.setAccessibilityLabel = YES; - if (accessibilityLabel != newAccessibilityLabel) { - accessibilityLabel = [newAccessibilityLabel copy]; + if (! ASObjectIsEqual(accessibilityLabel, newAccessibilityLabel)) { + _flags.setAccessibilityLabel = YES; + _flags.setAccessibilityAttributedLabel = YES; + accessibilityLabel = newAccessibilityLabel ? [newAccessibilityLabel copy] : nil; + accessibilityAttributedLabel = newAccessibilityLabel ? [[NSAttributedString alloc] initWithString:newAccessibilityLabel] : nil; + } +} + +- (NSAttributedString *)accessibilityAttributedLabel +{ + 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; } } @@ -599,8 +628,27 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityHint:(NSString *)newAccessibilityHint { - _flags.setAccessibilityHint = YES; - accessibilityHint = [newAccessibilityHint copy]; + if (! ASObjectIsEqual(accessibilityHint, newAccessibilityHint)) { + _flags.setAccessibilityHint = YES; + _flags.setAccessibilityAttributedHint = YES; + accessibilityHint = newAccessibilityHint ? [newAccessibilityHint copy] : nil; + accessibilityAttributedHint = newAccessibilityHint ? [[NSAttributedString alloc] initWithString:newAccessibilityHint] : nil; + } +} + +- (NSAttributedString *)accessibilityAttributedHint +{ + 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; + } } - (NSString *)accessibilityValue @@ -610,8 +658,27 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; - (void)setAccessibilityValue:(NSString *)newAccessibilityValue { - _flags.setAccessibilityValue = YES; - accessibilityValue = [newAccessibilityValue copy]; + if (! ASObjectIsEqual(accessibilityValue, newAccessibilityValue)) { + _flags.setAccessibilityValue = YES; + _flags.setAccessibilityAttributedValue = YES; + accessibilityValue = newAccessibilityValue ? [newAccessibilityValue copy] : nil; + accessibilityAttributedValue = newAccessibilityValue ? [[NSAttributedString alloc] initWithString:newAccessibilityValue] : nil; + } +} + +- (NSAttributedString *)accessibilityAttributedValue +{ + 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; + } } - (UIAccessibilityTraits)accessibilityTraits @@ -984,8 +1051,10 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; if (flags.setOpaque) ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); - if (flags.setSemanticContentAttribute) { - view.semanticContentAttribute = semanticContentAttribute; + if (AS_AVAILABLE_IOS(9)) { + if (flags.setSemanticContentAttribute) { + view.semanticContentAttribute = semanticContentAttribute; + } } if (flags.setIsAccessibilityElement) @@ -994,12 +1063,21 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; 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 (flags.setAccessibilityTraits) view.accessibilityTraits = accessibilityTraits; @@ -1137,11 +1215,20 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; pendingState.allowsGroupOpacity = layer.allowsGroupOpacity; pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; - pendingState.semanticContentAttribute = view.semanticContentAttribute; + if (AS_AVAILABLE_IOS(9)) { + pendingState.semanticContentAttribute = view.semanticContentAttribute; + } pendingState.isAccessibilityElement = view.isAccessibilityElement; pendingState.accessibilityLabel = view.accessibilityLabel; pendingState.accessibilityHint = view.accessibilityHint; pendingState.accessibilityValue = view.accessibilityValue; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS(11)) { + pendingState.accessibilityAttributedLabel = view.accessibilityAttributedLabel; + pendingState.accessibilityAttributedHint = view.accessibilityAttributedHint; + pendingState.accessibilityAttributedValue = view.accessibilityAttributedValue; + } +#endif pendingState.accessibilityTraits = view.accessibilityTraits; pendingState.accessibilityFrame = view.accessibilityFrame; pendingState.accessibilityLanguage = view.accessibilityLanguage; @@ -1219,8 +1306,11 @@ static BOOL defaultAllowsEdgeAntialiasing = NO; || flags.setSemanticContentAttribute || flags.setIsAccessibilityElement || flags.setAccessibilityLabel + || flags.setAccessibilityAttributedLabel || flags.setAccessibilityHint + || flags.setAccessibilityAttributedHint || flags.setAccessibilityValue + || flags.setAccessibilityAttributedValue || flags.setAccessibilityTraits || flags.setAccessibilityFrame || flags.setAccessibilityLanguage diff --git a/Source/TextKit/ASTextKitComponents.h b/Source/TextKit/ASTextKitComponents.h index 9fe5201c82..20acc2c472 100644 --- a/Source/TextKit/ASTextKitComponents.h +++ b/Source/TextKit/ASTextKitComponents.h @@ -20,6 +20,12 @@ NS_ASSUME_NONNULL_BEGIN +@interface ASTextKitComponentsTextView : UITextView +- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder __unavailable; +- (instancetype)init __unavailable; +@end + AS_SUBCLASSING_RESTRICTED @interface ASTextKitComponents : NSObject @@ -52,14 +58,13 @@ AS_SUBCLASSING_RESTRICTED */ - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth; - - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth forMaxNumberOfLines:(NSInteger)numberOfLines; @property (nonatomic, strong, readonly) NSTextStorage *textStorage; @property (nonatomic, strong, readonly) NSTextContainer *textContainer; @property (nonatomic, strong, readonly) NSLayoutManager *layoutManager; -@property (nullable, nonatomic, strong) UITextView *textView; +@property (nonatomic, strong, nullable) ASTextKitComponentsTextView *textView; @end diff --git a/Source/TextKit/ASTextKitComponents.mm b/Source/TextKit/ASTextKitComponents.mm index 2854ad6997..7e4a7ae68f 100644 --- a/Source/TextKit/ASTextKitComponents.mm +++ b/Source/TextKit/ASTextKitComponents.mm @@ -1,5 +1,5 @@ // -// ASTextKitComponents.m +// ASTextKitComponents.mm // Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. @@ -16,9 +16,41 @@ // #import +#import #import +@interface ASTextKitComponentsTextView () +@property (atomic, assign) CGRect threadSafeBounds; +@end + +@implementation ASTextKitComponentsTextView + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer +{ + self = [super initWithFrame:frame textContainer:textContainer]; + if (self) { + _threadSafeBounds = self.bounds; + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + ASDisplayNodeAssertMainThread(); + [super setFrame:frame]; + self.threadSafeBounds = self.bounds; +} + +- (void)setBounds:(CGRect)bounds +{ + ASDisplayNodeAssertMainThread(); + [super setBounds:bounds]; + self.threadSafeBounds = bounds; +} + +@end + @interface ASTextKitComponents () // read-write redeclarations @@ -30,6 +62,8 @@ @implementation ASTextKitComponents +#pragma mark - Class + + (instancetype)componentsWithAttributedSeedString:(NSAttributedString *)attributedSeedString textContainerSize:(CGSize)textContainerSize { @@ -58,13 +92,27 @@ return components; } +#pragma mark - Lifecycle + +- (void)dealloc +{ + // Nil out all delegates to prevent crash + if (_textView) { + ASDisplayNodeAssertMainThread(); + _textView.delegate = nil; + } + _layoutManager.delegate = nil; +} + +#pragma mark - Sizing + - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth { ASTextKitComponents *components = self; // If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation. // Otherwise, we create a temporary stack to size for `constrainedWidth`. - if (CGRectGetWidth(components.textView.bounds) != constrainedWidth) { + if (CGRectGetWidth(components.textView.threadSafeBounds) != constrainedWidth) { components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; } @@ -86,7 +134,7 @@ // Always use temporary stack in case of threading issues components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; - + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by - usedRectForTextContainer:). [components.layoutManager ensureLayoutForTextContainer:components.textContainer]; diff --git a/Source/TextKit/ASTextKitContext.mm b/Source/TextKit/ASTextKitContext.mm index 64fcf65092..f8b3d15655 100755 --- a/Source/TextKit/ASTextKitContext.mm +++ b/Source/TextKit/ASTextKitContext.mm @@ -40,8 +40,9 @@ { if (self = [super init]) { // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. - static ASDN::Mutex __staticMutex; - ASDN::MutexLocker l(__staticMutex); + // Allocate __staticMutex on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) + static ASDN::StaticMutex& __staticMutex = *new ASDN::StaticMutex; + ASDN::StaticMutexLocker l(__staticMutex); __instanceLock__ = std::make_shared(); diff --git a/Tests/ASCollectionViewTests.mm b/Tests/ASCollectionViewTests.mm index f3567b4373..06c6fcd890 100644 --- a/Tests/ASCollectionViewTests.mm +++ b/Tests/ASCollectionViewTests.mm @@ -620,7 +620,7 @@ [window makeKeyAndVisible]; for (NSInteger i = 0; i < 2; i++) { - // NOTE: waitUntilAllUpdatesAreProcessed or reloadDataImmediately is not sufficient here!! + // NOTE: reloadData and waitUntilAllUpdatesAreProcessed are not sufficient here!! XCTestExpectation *done = [self expectationWithDescription:[NSString stringWithFormat:@"Reload #%td complete", i]]; [cn reloadDataWithCompletion:^{ [done fulfill]; @@ -867,7 +867,7 @@ [self waitForExpectationsWithTimeout:3 handler:nil]; } -- (void)testThatMultipleBatchFetchesDontHappenUnnecessarily +- (void)disabled_testThatMultipleBatchFetchesDontHappenUnnecessarily { UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; diff --git a/Tests/ASCornerLayoutSpecSnapshotTests.mm b/Tests/ASCornerLayoutSpecSnapshotTests.mm new file mode 100644 index 0000000000..fcbbfe9850 --- /dev/null +++ b/Tests/ASCornerLayoutSpecSnapshotTests.mm @@ -0,0 +1,219 @@ +// +// ASCornerLayoutSpecSnapshotTests.mm +// Texture +// +// Copyright (c) 2017-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 "ASLayoutSpecSnapshotTestsHelper.h" +#import +#import + +typedef NS_ENUM(NSInteger, ASCornerLayoutSpecSnapshotTestsOffsetOption) { + ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter, + ASCornerLayoutSpecSnapshotTestsOffsetOptionInner, + ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter, +}; + + +@interface ASCornerLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase + +@property (nonatomic, strong) UIColor *boxColor; +@property (nonatomic, strong) UIColor *baseColor; +@property (nonatomic, strong) UIColor *cornerColor; +@property (nonatomic, strong) UIColor *contextColor; + +@property (nonatomic, assign) CGSize baseSize; +@property (nonatomic, assign) CGSize cornerSize; +@property (nonatomic, assign) CGSize contextSize; + +@property (nonatomic, assign) ASSizeRange contextSizeRange; + +@end + + +@implementation ASCornerLayoutSpecSnapshotTests + +- (void)setUp +{ + [super setUp]; + + self.recordMode = NO; + + _boxColor = [UIColor greenColor]; + _baseColor = [UIColor blueColor]; + _cornerColor = [UIColor orangeColor]; + _contextColor = [UIColor lightGrayColor]; + + _baseSize = CGSizeMake(60, 60); + _cornerSize = CGSizeMake(20, 20); + _contextSize = CGSizeMake(120, 120); + + _contextSizeRange = ASSizeRangeMake(CGSizeZero, _contextSize); +} + +- (void)testCornerSpecForAllLocations +{ + ASCornerLayoutSpecSnapshotTestsOffsetOption center = ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:center wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:center wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:center wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:center wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:center wrapsCorner:YES]; +} + +- (void)testCornerSpecForAllLocationsWithInnerOffset +{ + ASCornerLayoutSpecSnapshotTestsOffsetOption inner = ASCornerLayoutSpecSnapshotTestsOffsetOptionInner; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:inner wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:inner wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:inner wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:inner wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:inner wrapsCorner:YES]; +} + +- (void)testCornerSpecForAllLocationsWithOuterOffset +{ + ASCornerLayoutSpecSnapshotTestsOffsetOption outer = ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopLeft offsetOption:outer wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationTopRight offsetOption:outer wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomLeft offsetOption:outer wrapsCorner:YES]; + + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:outer wrapsCorner:NO]; + [self testCornerSpecWithLocation:ASCornerLayoutLocationBottomRight offsetOption:outer wrapsCorner:YES]; +} + +- (void)testCornerSpecWithLocation:(ASCornerLayoutLocation)location + offsetOption:(ASCornerLayoutSpecSnapshotTestsOffsetOption)offsetOption + wrapsCorner:(BOOL)wrapsCorner +{ + ASDisplayNode *baseNode = ASDisplayNodeWithBackgroundColor(_baseColor, _baseSize); + ASDisplayNode *cornerNode = ASDisplayNodeWithBackgroundColor(_cornerColor, _cornerSize); + ASDisplayNode *debugBoxNode = ASDisplayNodeWithBackgroundColor(_boxColor); + + baseNode.style.layoutPosition = CGPointMake((_contextSize.width - _baseSize.width) / 2, + (_contextSize.height - _baseSize.height) / 2); + + ASCornerLayoutSpec *cornerSpec = [ASCornerLayoutSpec cornerLayoutSpecWithChild:baseNode + corner:cornerNode + location:location]; + + CGPoint delta = (CGPoint){ _cornerSize.width / 2, _cornerSize.height / 2 }; + cornerSpec.offset = [self offsetForOption:offsetOption location:location delta:delta]; + cornerSpec.wrapsCorner = wrapsCorner; + + ASBackgroundLayoutSpec *backgroundSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:cornerSpec + background:debugBoxNode]; + + [self testLayoutSpec:backgroundSpec + sizeRange:_contextSizeRange + subnodes:@[debugBoxNode, baseNode, cornerNode] + identifier:[self suffixWithLocation:location option:offsetOption wrapsCorner:wrapsCorner]]; +} + +- (CGPoint)offsetForOption:(ASCornerLayoutSpecSnapshotTestsOffsetOption)option + location:(ASCornerLayoutLocation)location + delta:(CGPoint)delta +{ + CGFloat x = delta.x; + CGFloat y = delta.y; + + switch (option) { + + case ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter: + return CGPointZero; + + case ASCornerLayoutSpecSnapshotTestsOffsetOptionInner: + + switch (location) { + case ASCornerLayoutLocationTopLeft: return (CGPoint){ x, y }; + case ASCornerLayoutLocationTopRight: return (CGPoint){ -x, y }; + case ASCornerLayoutLocationBottomLeft: return (CGPoint){ x, -y }; + case ASCornerLayoutLocationBottomRight: return (CGPoint){ -x, -y }; + } + + case ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter: + + switch (location) { + case ASCornerLayoutLocationTopLeft: return (CGPoint){ -x, -y }; + case ASCornerLayoutLocationTopRight: return (CGPoint){ x, -y }; + case ASCornerLayoutLocationBottomLeft: return (CGPoint){ -x, y }; + case ASCornerLayoutLocationBottomRight: return (CGPoint){ x, y }; + } + + } + +} + +- (NSString *)suffixWithLocation:(ASCornerLayoutLocation)location + option:(ASCornerLayoutSpecSnapshotTestsOffsetOption)option + wrapsCorner:(BOOL)wrapsCorner +{ + NSMutableString *desc = [NSMutableString string]; + + switch (location) { + case ASCornerLayoutLocationTopLeft: + [desc appendString:@"topLeft"]; + break; + case ASCornerLayoutLocationTopRight: + [desc appendString:@"topRight"]; + break; + case ASCornerLayoutLocationBottomLeft: + [desc appendString:@"bottomLeft"]; + break; + case ASCornerLayoutLocationBottomRight: + [desc appendString:@"bottomRight"]; + break; + } + + [desc appendString:@"_"]; + + switch (option) { + case ASCornerLayoutSpecSnapshotTestsOffsetOptionCenter: + [desc appendString:@"center"]; + break; + case ASCornerLayoutSpecSnapshotTestsOffsetOptionInner: + [desc appendString:@"inner"]; + break; + case ASCornerLayoutSpecSnapshotTestsOffsetOptionOuter: + [desc appendString:@"outer"]; + break; + } + + [desc appendString:@"_"]; + + if (wrapsCorner) { + [desc appendString:@"fullSize"]; + } else { + [desc appendString:@"childSize"]; + } + + return desc.copy; +} + +@end diff --git a/Tests/ASDisplayLayerTests.m b/Tests/ASDisplayLayerTests.m index 1abcc71541..1e5b19abac 100644 --- a/Tests/ASDisplayLayerTests.m +++ b/Tests/ASDisplayLayerTests.m @@ -148,7 +148,7 @@ typedef NS_ENUM(NSUInteger, _ASDisplayLayerTestDelegateClassModes) { // for _ASDisplayLayerTestDelegateModeClassDisplay @property (nonatomic, assign) NSUInteger displayCount; -@property (nonatomic, copy) UIImage *(^displayLayerBlock)(); +@property (nonatomic, copy) UIImage *(^displayLayerBlock)(void); // for _ASDisplayLayerTestDelegateModeClassDrawInContext @property (nonatomic, assign) NSUInteger drawRectCount; @@ -472,7 +472,7 @@ static _ASDisplayLayerTestDelegateClassModes _class_modes; layer1.displaysAsynchronously = YES; dispatch_semaphore_t displayAsyncLayer1Sema = dispatch_semaphore_create(0); - layer1Delegate.displayLayerBlock = ^(_ASDisplayLayer *asyncLayer) { + layer1Delegate.displayLayerBlock = ^UIImage *{ dispatch_semaphore_wait(displayAsyncLayer1Sema, DISPATCH_TIME_FOREVER); return bogusImage(); }; diff --git a/Tests/ASDisplayNodeImplicitHierarchyTests.m b/Tests/ASDisplayNodeImplicitHierarchyTests.m index a3a7e160f3..11e2ce4afa 100644 --- a/Tests/ASDisplayNodeImplicitHierarchyTests.m +++ b/Tests/ASDisplayNodeImplicitHierarchyTests.m @@ -2,8 +2,6 @@ // ASDisplayNodeImplicitHierarchyTests.m // Texture // -// Created by Levi McCallum on 2/1/16. -// // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // This source code is licensed under the BSD-style license found in the // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional @@ -20,6 +18,7 @@ #import #import +#import #import "ASDisplayNodeTestsHelper.h" @interface ASSpecTestDisplayNode : ASDisplayNode @@ -101,6 +100,47 @@ XCTAssertEqual(node.subnodes[4], node5); } +- (void)testInitialNodeInsertionWhenEnterPreloadState +{ + static CGSize kSize = {100, 100}; + + static NSInteger subnodeCount = 5; + NSMutableArray *subnodes = [NSMutableArray arrayWithCapacity:subnodeCount]; + for (NSInteger i = 0; i < subnodeCount; i++) { + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + // As we will involve a stack spec we have to give the nodes an intrinsic content size + subnode.style.preferredSize = kSize; + [subnodes addObject:subnode]; + } + + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { + ASAbsoluteLayoutSpec *absoluteLayout = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[subnodes[3]]]; + + ASStackLayoutSpec *stack1 = [[ASStackLayoutSpec alloc] init]; + [stack1 setChildren:@[subnodes[0], subnodes[1]]]; + + ASStackLayoutSpec *stack2 = [[ASStackLayoutSpec alloc] init]; + [stack2 setChildren:@[subnodes[2], absoluteLayout]]; + + return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[stack1, stack2, subnodes[4]]]; + }; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + [node recursivelySetInterfaceState:ASInterfaceStatePreload]; + + // No premature view allocation + XCTAssertFalse(node.isNodeLoaded); + // Subnodes should be inserted, laid out and entered preload state + XCTAssertTrue([subnodes isEqualToArray:node.subnodes]); + for (NSInteger i = 0; i < subnodeCount; i++) { + ASDisplayNode *subnode = subnodes[i]; + XCTAssertTrue(CGSizeEqualToSize(kSize, subnode.bounds.size)); + XCTAssertTrue(ASInterfaceStateIncludesPreload(subnode.interfaceState)); + } +} + - (void)testCalculatedLayoutHierarchyTransitions { static CGSize kSize = {100, 100}; diff --git a/Tests/ASDisplayNodeLayoutTests.mm b/Tests/ASDisplayNodeLayoutTests.mm index a998e98285..00512e8012 100644 --- a/Tests/ASDisplayNodeLayoutTests.mm +++ b/Tests/ASDisplayNodeLayoutTests.mm @@ -44,7 +44,7 @@ ASXCTAssertEqualSizes(displayNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); ASXCTAssertEqualSizes(buttonNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); - // Trigger view creation and layout pass without a manual measure: call before so the automatic measurement + // Trigger view creation and layout pass without a manual -layoutThatFits: call before so the automatic measurement // pass will trigger in the layout pass [displayNode.view layoutIfNeeded]; diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index 12801b3a33..f2f6d68ef1 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -22,9 +22,9 @@ #import #import +#import #import #import -#import #import #import "ASDisplayNodeTestsHelper.h" #import @@ -116,6 +116,10 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @property (nonatomic) BOOL hasPreloaded; @property (nonatomic) BOOL preloadStateChangedToYES; @property (nonatomic) BOOL preloadStateChangedToNO; + +@property (nonatomic, assign) NSUInteger displayWillStartCount; +@property (nonatomic, assign) NSUInteger didDisplayCount; + @end @interface ASTestResponderNode : ASTestDisplayNode @@ -160,6 +164,18 @@ for (ASDisplayNode *n in @[ nodes ]) {\ } } +- (void)displayDidFinish +{ + [super displayDidFinish]; + _didDisplayCount++; +} + +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + [super displayWillStartAsynchronously:asynchronously]; + _displayWillStartCount++; +} + @end @interface UIDisplayNodeTestView : UIView @@ -342,6 +358,11 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertEqual((id)nil, node.accessibilityLabel, @"default accessibilityLabel is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityHint, @"default accessibilityHint is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityValue, @"default accessibilityValue is broken %@", hasLoadedView); +// if (AS_AT_LEAST_IOS11) { +// XCTAssertEqual((id)nil, node.accessibilityAttributedLabel, @"default accessibilityAttributedLabel is broken %@", hasLoadedView); +// XCTAssertEqual((id)nil, node.accessibilityAttributedHint, @"default accessibilityAttributedHint is broken %@", hasLoadedView); +// XCTAssertEqual((id)nil, node.accessibilityAttributedValue, @"default accessibilityAttributedValue is broken %@", hasLoadedView); +// } XCTAssertEqual(UIAccessibilityTraitNone, node.accessibilityTraits, @"default accessibilityTraits is broken %@", hasLoadedView); XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.accessibilityFrame), @"default accessibilityFrame is broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.accessibilityLanguage, @"default accessibilityLanguage is broken %@", hasLoadedView); @@ -440,6 +461,13 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertEqualObjects(@"Ship love", node.accessibilityLabel, @"accessibilityLabel broken %@", hasLoadedView); XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityHint, @"accessibilityHint broken %@", hasLoadedView); XCTAssertEqualObjects(@"1 of 2", node.accessibilityValue, @"accessibilityValue broken %@", hasLoadedView); + + // setting the accessibilityLabel, accessibilityHint and accessibilityValue is supposed to be bridged to the attributed versions +// if (AS_AT_LEAST_IOS11) { +// XCTAssertEqualObjects(@"Ship love", node.accessibilityAttributedLabel.string, @"accessibilityAttributedLabel is broken %@", hasLoadedView); +// XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityAttributedHint.string, @"accessibilityAttributedHint is broken %@", hasLoadedView); +// XCTAssertEqualObjects(@"1 of 2", node.accessibilityAttributedValue.string, @"accessibilityAttributedValue is broken %@", hasLoadedView); +// } XCTAssertEqual(UIAccessibilityTraitSelected | UIAccessibilityTraitButton, node.accessibilityTraits, @"accessibilityTraits broken %@", hasLoadedView); XCTAssertTrue(CGRectEqualToRect(CGRectMake(1, 2, 3, 4), node.accessibilityFrame), @"accessibilityFrame broken %@", hasLoadedView); XCTAssertEqualObjects(@"mas", node.accessibilityLanguage, @"accessibilityLanguage broken %@", hasLoadedView); @@ -495,9 +523,19 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.debugName = @"quack like a duck"; node.isAccessibilityElement = YES; - node.accessibilityLabel = @"Ship love"; - node.accessibilityHint = @"Awesome things will happen"; - node.accessibilityValue = @"1 of 2"; + + for (int i = 0; i < 4; i++) { + if (i % 2 == 0) { + XCTAssertNoThrow(node.accessibilityLabel = nil); + XCTAssertNoThrow(node.accessibilityHint = nil); + XCTAssertNoThrow(node.accessibilityValue = nil); + } else { + node.accessibilityLabel = @"Ship love"; + node.accessibilityHint = @"Awesome things will happen"; + node.accessibilityValue = @"1 of 2"; + } + } + node.accessibilityTraits = UIAccessibilityTraitSelected | UIAccessibilityTraitButton; node.accessibilityFrame = CGRectMake(1, 2, 3, 4); node.accessibilityLanguage = @"mas"; @@ -678,46 +716,54 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Setup CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* outer node's coordinate space to inner node's coordinate space node.frame = CGRectMake(100, 100, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(105, 105), correctPoint = CGPointMake(95, 95); + originalPoint = CGPointMake(105, 105); + correctPoint = CGPointMake(95, 95); convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* inner node's coordinate space to outer node's coordinate space node.frame = CGRectMake(100, 100, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(15, 15); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(15, 15); convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in inner node's coordinate space *TO* outer node's coordinate space node.frame = CGRectMake(100, 100, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(95, 95), correctPoint = CGPointMake(105, 105); + originalPoint = CGPointMake(95, 95); + correctPoint = CGPointMake(105, 105); convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in outer node's coordinate space *TO* inner node's coordinate space node.frame = CGRectMake(0, 0, 100, 100); innerNode.frame = CGRectMake(10, 10, 20, 20); - originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(-5, -5); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(-5, -5); convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); } @@ -731,7 +777,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Setup CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* outer node's coordinate space to inner node's coordinate space @@ -740,12 +787,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(20, 20, 100, 100); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(42, 42), correctPoint = CGPointMake(36, 36); + originalPoint = CGPointMake(42, 42); + correctPoint = CGPointMake(36, 36); convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* inner node's coordinate space to outer node's coordinate space @@ -754,12 +803,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(-1000, -1000, 1337, 1337); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(11, 11); + originalPoint = CGPointMake(5, 5); + correctPoint = CGPointMake(11, 11); convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in inner node's coordinate space *TO* outer node's coordinate space @@ -768,12 +819,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(20, 20, 100, 100); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(36, 36), correctPoint = CGPointMake(42, 42); + originalPoint = CGPointMake(36, 36); + correctPoint = CGPointMake(42, 42); convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in outer node's coordinate space *TO* inner node's coordinate space @@ -782,7 +835,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(-1000, -1000, 1337, 1337); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(11, 11), correctPoint = CGPointMake(5, 5); + originalPoint = CGPointMake(11, 11); + correctPoint = CGPointMake(5, 5); convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); } @@ -795,7 +849,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Setup CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* outer node's coordinate space to inner node's coordinate space @@ -803,12 +858,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.75, 1); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(42, 42), correctPoint = CGPointMake(51, 56); + originalPoint = CGPointMake(42, 42); + correctPoint = CGPointMake(51, 56); convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point *FROM* inner node's coordinate space to outer node's coordinate space @@ -816,12 +873,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.3, 0.3); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(55, 55), correctPoint = CGPointMake(1, 1); + originalPoint = CGPointMake(55, 55); + correctPoint = CGPointMake(1, 1); convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in inner node's coordinate space *TO* outer node's coordinate space @@ -829,12 +888,14 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.75, 1); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 20, 20); - originalPoint = CGPointMake(51, 56), correctPoint = CGPointMake(42, 42); + originalPoint = CGPointMake(51, 56); + correctPoint = CGPointMake(42, 42); convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); // Setup - node = [[ASDisplayNode alloc] init], innerNode = [[ASDisplayNode alloc] init]; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; [node addSubnode:innerNode]; // Convert point in outer node's coordinate space *TO* inner node's coordinate space @@ -842,7 +903,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ innerNode.anchorPoint = CGPointMake(0.3, 0.3); innerNode.position = CGPointMake(23, 23); innerNode.bounds = CGRectMake(17, 17, 200, 200); - originalPoint = CGPointMake(1, 1), correctPoint = CGPointMake(55, 55); + originalPoint = CGPointMake(1, 1); + correctPoint = CGPointMake(55, 55); convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); } @@ -2024,6 +2086,39 @@ static bool stringContainsPointer(NSString *description, id p) { XCTAssertThrows([rasterizedSupernode addSubnode:subnode]); } +- (void)testThatSubnodesGetDisplayUpdatesIfRasterized +{ + ASTestDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; + supernode.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [supernode enableSubtreeRasterization]; + + ASTestDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *subSubnode = [[ASTestDisplayNode alloc] init]; + + ASSetDebugNames(supernode, subnode); + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + [subnode addSubnode:subSubnode]; + [supernode addSubnode:subnode]; + [window addSubnode:supernode]; + [window makeKeyAndVisible]; + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subnode.didDisplayCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subSubnode.didDisplayCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subnode.displayWillStartCount == 1); + })); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (subSubnode.displayWillStartCount == 1); + })); +} + // Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2011 - (void)testThatLayerBackedSubnodesAreMarkedInvisibleBeforeDeallocWhenSupernodesViewIsRemovedFromHierarchyWhileBeingRetained { @@ -2133,27 +2228,6 @@ static bool stringContainsPointer(NSString *description, id p) { XCTAssertEqualObjects(calls, expected); } -- (void)testPreferredFrameSizeDeprecated -{ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - - ASDisplayNode *node = [ASDisplayNode new]; - - // Default auto preferred frame size will be CGSizeZero - XCTAssert(CGSizeEqualToSize(node.preferredFrameSize, CGSizeZero)); - - // Set a specific preferredFrameSize - node.preferredFrameSize = CGSizeMake(100, 100); - ASXCTAssertEqualSizes(node.preferredFrameSize, CGSizeMake(100, 100)); - - // CGSizeZero should be returned if width or height is not of unit type points - node.style.width = ASDimensionMakeWithFraction(0.5); - ASXCTAssertEqualSizes(node.preferredFrameSize, CGSizeZero); - -#pragma clang diagnostic pop -} - - (void)testSettingPropertiesViaStyllableProtocol { ASDisplayNode *node = [[ASDisplayNode alloc] init]; diff --git a/Tests/ASLayoutEngineTests.mm b/Tests/ASLayoutEngineTests.mm new file mode 100644 index 0000000000..550dffa477 --- /dev/null +++ b/Tests/ASLayoutEngineTests.mm @@ -0,0 +1,517 @@ +// +// ASLayoutEngineTests.mm +// Texture +// +// Copyright (c) 2017-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 "ASTestCase.h" +#import "ASLayoutTestNode.h" +#import "ASXCTExtensions.h" +#import "ASTLayoutFixture.h" + +@interface ASLayoutEngineTests : ASTestCase + +@end + +@implementation ASLayoutEngineTests { + ASLayoutTestNode *nodeA; + ASLayoutTestNode *nodeB; + ASLayoutTestNode *nodeC; + ASLayoutTestNode *nodeD; + ASLayoutTestNode *nodeE; + ASTLayoutFixture *fixture1; + ASTLayoutFixture *fixture2; + ASTLayoutFixture *fixture3; + ASTLayoutFixture *fixture4; + + // fixtures 1 and 3 share the same exact node A layout spec block. + // we don't want the infra to call -setNeedsLayout when we switch fixtures + // so we need to use the same exact block. + ASLayoutSpecBlock fixture1and3NodeALayoutSpecBlock; + + UIWindow *window; + UIViewController *vc; + NSArray *allNodes; + NSTimeInterval verifyDelay; + // See -stubCalculatedLayoutDidChange. + BOOL stubbedCalculatedLayoutDidChange; +} + +- (void)setUp +{ + [super setUp]; + verifyDelay = 3; + window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 10, 1)]; + vc = [[UIViewController alloc] init]; + nodeA = [ASLayoutTestNode new]; + nodeA.backgroundColor = [UIColor redColor]; + + // NOTE: nodeB has flexShrink, the others don't + nodeB = [ASLayoutTestNode new]; + nodeB.style.flexShrink = 1; + nodeB.backgroundColor = [UIColor orangeColor]; + + nodeC = [ASLayoutTestNode new]; + nodeC.backgroundColor = [UIColor yellowColor]; + nodeD = [ASLayoutTestNode new]; + nodeD.backgroundColor = [UIColor greenColor]; + nodeE = [ASLayoutTestNode new]; + nodeE.backgroundColor = [UIColor blueColor]; + allNodes = @[ nodeA, nodeB, nodeC, nodeD, nodeE ]; + ASSetDebugNames(nodeA, nodeB, nodeC, nodeD, nodeE); + ASLayoutSpecBlock b = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeD ]]; + }; + fixture1and3NodeALayoutSpecBlock = b; + fixture1 = [self createFixture1]; + fixture2 = [self createFixture2]; + fixture3 = [self createFixture3]; + fixture4 = [self createFixture4]; + + nodeA.frame = vc.view.bounds; + nodeA.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [vc.view addSubnode:nodeA]; + + window.rootViewController = vc; + [window makeKeyAndVisible]; +} + +- (void)tearDown +{ + nodeA.layoutSpecBlock = nil; + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + [super tearDown]; +} + +- (void)testFirstLayoutPassWhenInWindow +{ + [self runFirstLayoutPassWithFixture:fixture1]; +} + +- (void)testSetNeedsLayoutAndNormalLayoutPass +{ + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // skip nodeB because its layout doesn't change. + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread(); + }]; + OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread(); + } + + [window layoutIfNeeded]; + [self verifyFixture:fixture2]; +} + +/** + * Transition from fixture1 to Fixture2 on node A. + * + * Expect A and D to calculate once off main, and + * to receive calculatedLayoutDidChange on main, + * then to get the measurement completion call on main, + * then to get animateLayoutTransition: and didCompleteLayoutTransition: on main. + */ +- (void)testLayoutTransitionWithAsyncMeasurement +{ + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // Expect A, C, E to calculate new layouts off-main + // dispatch_once onto main to run our injectedMainThread work while the transition calculates. + __block dispatch_block_t injectedMainThreadWork = nil; + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .offMainThread() + .andDo(^(NSInvocation *inv) { + // On first calculateLayoutThatFits, schedule our injected main thread work. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + injectedMainThreadWork(); + }); + }); + }); + }]; + } + + // The code in this section is designed to move in time order, all on the main thread: + + OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread(); + OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread(); + + // Trigger the layout transition. + __block dispatch_block_t measurementCompletionBlock = nil; + [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{ + measurementCompletionBlock(); + }]; + + // This block will get run after bg layout calculate starts, but before measurementCompletion + __block BOOL injectedMainThreadWorkDone = NO; + injectedMainThreadWork = ^{ + injectedMainThreadWorkDone = YES; + + [window layoutIfNeeded]; + + // Ensure we're still on the old layout. We should stay on this until the transition completes. + [self verifyFixture:fixture1]; + }; + + measurementCompletionBlock = ^{ + XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran."); + }; + + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + + [self verifyFixture:fixture2]; +} + +/** + * Start at fixture 1. + * Trigger an async transition to fixture 2. + * While it's measuring, on main switch to fixture 4 (setNeedsLayout A, D) and run a CA layout pass. + * + * Correct behavior, we end up at fixture 4 since it's newer. + * Current incorrect behavior, we end up at fixture 2 and we remeasure surviving node C. + * Note: incorrect behavior likely introduced by the early check in __layout added in + * https://github.com/facebookarchive/AsyncDisplayKit/pull/2657 + */ +- (void)DISABLE_testASetNeedsLayoutInterferingWithTheCurrentTransition +{ + static BOOL enforceCorrectBehavior = NO; + + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture1]; + + [fixture2 apply]; + + // Expect A, C, E to calculate new layouts off-main + // dispatch_once onto main to run our injectedMainThread work while the transition calculates. + __block dispatch_block_t injectedMainThreadWork = nil; + for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) { + [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]) + .offMainThread() + .andDo(^(NSInvocation *inv) { + // On first calculateLayoutThatFits, schedule our injected main thread work. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + injectedMainThreadWork(); + }); + }); + }); + }]; + } + + // The code in this section is designed to move in time order, all on the main thread: + + // With the current behavior, the transition will continue and complete. + if (!enforceCorrectBehavior) { + OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread(); + OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread(); + } + + // Trigger the layout transition. + __block dispatch_block_t measurementCompletionBlock = nil; + [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{ + measurementCompletionBlock(); + }]; + + // Injected block will get run on main after bg layout calculate starts, but before measurementCompletion + __block BOOL injectedMainThreadWorkDone = NO; + injectedMainThreadWork = ^{ + as_log_verbose(OS_LOG_DEFAULT, "Begin injectedMainThreadWork"); + injectedMainThreadWorkDone = YES; + + [fixture4 apply]; + as_log_verbose(OS_LOG_DEFAULT, "Did apply new fixture"); + + if (enforceCorrectBehavior) { + // Correct measurement behavior here is unclear, may depend on whether the layouts which + // are common to both fixture2 and fixture4 are available from the cache. + } else { + // Incorrect behavior: nodeC will get measured against its new bounds on main. + auto cPendingSize = [fixture2 layoutForNode:nodeC].size; + OCMExpect([nodeC.mock calculateLayoutThatFits:ASSizeRangeMake(cPendingSize)]).onMainThread(); + } + [window layoutIfNeeded]; + as_log_verbose(OS_LOG_DEFAULT, "End injectedMainThreadWork"); + }; + + measurementCompletionBlock = ^{ + XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran."); + }; + + for (ASLayoutTestNode *node in allNodes) { + OCMVerifyAllWithDelay(node.mock, verifyDelay); + } + + // Incorrect behavior: The transition will "win" even though its transitioning to stale data. + if (enforceCorrectBehavior) { + [self verifyFixture:fixture4]; + } else { + [self verifyFixture:fixture2]; + } +} + +/** + * Start on fixture 3 where nodeB is force-shrunk via multipass layout. + * Apply fixture 1, which just changes nodeB's size and calls -setNeedsLayout on it. + * + * This behavior is currently broken. See implementation for correct behavior and incorrect behavior. + */ +- (void)testCallingSetNeedsLayoutOnANodeThatWasSubjectToMultipassLayout +{ + static BOOL const enforceCorrectBehavior = NO; + [self stubCalculatedLayoutDidChange]; + [self runFirstLayoutPassWithFixture:fixture3]; + + // Switch to fixture 1, updating nodeB's desired size and calling -setNeedsLayout + // Now nodeB will fit happily into the stack. + [fixture1 apply]; + + if (enforceCorrectBehavior) { + /* + * Correct behavior: nodeB is remeasured against the first (unconstrained) size + * and when it's discovered that now nodeB fits, nodeA will re-layout and we'll + * end up correctly at fixture1. + */ + OCMExpect([nodeB.mock calculateLayoutThatFits:[fixture3 firstSizeRangeForNode:nodeB]]); + + [fixture1 withSizeRangesForNode:nodeA block:^(ASSizeRange sizeRange) { + OCMExpect([nodeA.mock calculateLayoutThatFits:sizeRange]); + }]; + + [window layoutIfNeeded]; + [self verifyFixture:fixture1]; + } else { + /* + * Incorrect behavior: nodeB is remeasured against the second (fixed-width) constraint. + * The returned value (8) is clamped to the fixed with (7), and then compared to the previous + * width (7) and we decide not to propagate up the invalidation, and we stay stuck on the old + * layout (fixture3). + */ + OCMExpect([nodeB.mock calculateLayoutThatFits:nodeB.constrainedSizeForCalculatedLayout]); + [window layoutIfNeeded]; + [self verifyFixture:fixture3]; + } +} + +#pragma mark - Helpers + +- (void)verifyFixture:(ASTLayoutFixture *)fixture +{ + auto expected = fixture.layout; + + // Ensure expected == frames + auto frames = [fixture.rootNode currentLayoutBasedOnFrames]; + if (![expected isEqual:frames]) { + XCTFail(@"\n*** Layout verification failed – frames don't match expected. ***\nGot:\n%@\nExpected:\n%@", [frames recursiveDescription], [expected recursiveDescription]); + } + + // Ensure expected == calculatedLayout + auto calculated = fixture.rootNode.calculatedLayout; + if (![expected isEqual:calculated]) { + XCTFail(@"\n*** Layout verification failed – calculated layout doesn't match expected. ***\nGot:\n%@\nExpected:\n%@", [calculated recursiveDescription], [expected recursiveDescription]); + } +} + +/** + * Stubs calculatedLayoutDidChange for all nodes. + * + * It's not really a core layout engine method, and it's also + * currently bugged and gets called a lot so for most + * tests its better not to have expectations about it littered around. + * https://github.com/TextureGroup/Texture/issues/422 + */ +- (void)stubCalculatedLayoutDidChange +{ + stubbedCalculatedLayoutDidChange = YES; + for (ASLayoutTestNode *node in allNodes) { + OCMStub([node.mock calculatedLayoutDidChange]); + } +} + +/** + * Fixture 1: A basic horizontal stack, all single-pass. + * + * [A: HorizStack([B, C, D])]. B is (1x1), C is (2x1), D is (1x1) + */ +- (ASTLayoutFixture *)createFixture1 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{4,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]]; + fixture.layout = layoutA; + + [fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA]; + return fixture; +} + +/** + * Fixture 2: A simple transition away from fixture 1. + * + * [A: HorizStack([B, C, E])]. B is (1x1), C is (4x1), E is (1x1) + * + * From fixture 1: + * B survives with same layout + * C survives with new layout + * D is removed + * E joins with first layout + */ +- (ASTLayoutFixture *)createFixture2 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{4,1} position:{3,0} sublayouts:nil]; + + // nodeE + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE]; + auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutE ]]; + fixture.layout = layoutA; + + ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeE ]]; + }; + [fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA]; + return fixture; +} + +/** + * Fixture 3: Multipass stack layout + * + * [A: HorizStack([B, C, D])]. B is (7x1), C is (2x1), D is (1x1) + * + * nodeB (which has flexShrink=1) will return 8x1 for its size during the first + * stack pass, and it'll be subject to a second pass where it returns 7x1. + * + */ +- (ASTLayoutFixture *)createFixture3 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB wants 8,1 but it will settle for 7,1 + [fixture setReturnedSize:{8,1} forNode:nodeB]; + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + [fixture addSizeRange:{{7, 0}, {7, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{7,1} position:{0,0} sublayouts:nil]; + + // nodeC + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC]; + auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{7,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]]; + fixture.layout = layoutA; + + [fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA]; + return fixture; +} + +/** + * Fixture 4: A different simple transition away from fixture 1. + * + * [A: HorizStack([B, D, E])]. B is (1x1), D is (2x1), E is (1x1) + * + * From fixture 1: + * B survives with same layout + * C is removed + * D survives with new layout + * E joins with first layout + * + * From fixture 2: + * B survives with same layout + * C is removed + * D joins with first layout + * E survives with same layout + */ +- (ASTLayoutFixture *)createFixture4 +{ + auto fixture = [[ASTLayoutFixture alloc] init]; + + // nodeB + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB]; + auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil]; + + // nodeD + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD]; + auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{2,1} position:{4,0} sublayouts:nil]; + + // nodeE + [fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE]; + auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil]; + + [fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA]; + auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutD, layoutE ]]; + fixture.layout = layoutA; + + ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeD, nodeE ]]; + }; + [fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA]; + return fixture; +} + +- (void)runFirstLayoutPassWithFixture:(ASTLayoutFixture *)fixture +{ + [fixture apply]; + for (ASLayoutTestNode *node in fixture.allNodes) { + [fixture withSizeRangesForNode:node block:^(ASSizeRange sizeRange) { + OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread(); + }]; + + if (!stubbedCalculatedLayoutDidChange) { + OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread(); + } + } + + // Trigger CA layout pass. + [window layoutIfNeeded]; + + // Make sure it went through. + [self verifyFixture:fixture]; +} + +@end diff --git a/Tests/ASLayoutFlatteningTests.m b/Tests/ASLayoutFlatteningTests.m index 56f76c5d4a..fb04a95090 100644 --- a/Tests/ASLayoutFlatteningTests.m +++ b/Tests/ASLayoutFlatteningTests.m @@ -42,9 +42,9 @@ static ASLayout *layout(id element, NSArray *sublay NSMutableArray *layoutSpecs = [NSMutableArray array]; NSMutableArray *indirectSubnodes = [NSMutableArray array]; - ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; - ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; - ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)(void) = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)(void) = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; NSArray *sublayouts = @[ layout(subnode(), @[ @@ -118,7 +118,7 @@ static ASLayout *layout(id element, NSArray *sublay @autoreleasepool { ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; NSMutableArray *subnodes = [NSMutableArray array]; - ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, rootNode, @[ @@ -148,9 +148,9 @@ static ASLayout *layout(id element, NSArray *sublay NSMutableArray *indirectSubnodes = [NSMutableArray array]; NSMutableArray *reusedLayouts = [NSMutableArray array]; - ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; - ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; - ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + ASDisplayNode *(^subnode)(void) = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)(void) = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)(void) = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; ASLayout *(^reusedLayout)(ASDisplayNode *) = ^ASLayout *(ASDisplayNode *subnode) { [reusedLayouts addObject:layout(subnode, @[])]; return [reusedLayouts lastObject]; }; /* diff --git a/Tests/ASLayoutSpecTests.m b/Tests/ASLayoutSpecTests.m index 6300924648..5cfc3a66ce 100644 --- a/Tests/ASLayoutSpecTests.m +++ b/Tests/ASLayoutSpecTests.m @@ -2,8 +2,17 @@ // ASLayoutSpecTests.m // Texture // -// Created by Michael Schneider on 1/27/17. -// Copyright © 2017 Facebook. All rights reserved. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. 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 @@ -39,7 +48,7 @@ ASDK_STYLE_PROP_OBJ(NSString *, extendedName, setExtendedName); @end /* - * As the ASLayoutableStyle conforms to the ASDKExtendedLayoutable protocol now, ASDKExtendedLayoutable properties + * As the ASLayoutElementStyle conforms to the ASDKExtendedLayoutElement protocol now, ASDKExtendedLayoutElement properties * can be accessed in ASDKExtendedLayoutSpec */ @interface ASDKExtendedLayoutSpec : ASLayoutSpec diff --git a/Tests/ASLayoutTestNode.h b/Tests/ASLayoutTestNode.h new file mode 100644 index 0000000000..66fafee149 --- /dev/null +++ b/Tests/ASLayoutTestNode.h @@ -0,0 +1,42 @@ +// +// ASLayoutTestNode.h +// Texture +// +// Copyright (c) 2017-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 ASLayoutTestNode : ASDisplayNode + +/** + * Mocking ASDisplayNodes directly isn't very safe because when you pump mock objects + * into the guts of the framework, bad things happen e.g. direct-ivar-access on mock + * objects will return garbage data. + * + * Instead we create a strict mock for each node, and forward a selected set of calls to it. + */ +@property (nonatomic, strong, readonly) id mock; + +/** + * The size that this node will return in calculateLayoutThatFits (if it doesn't have a layoutSpecBlock). + * + * Changing this value will call -setNeedsLayout on the node. + */ +@property (nonatomic) CGSize testSize; + +/** + * Generate a layout based on the frame of this node and its subtree. + * + * The root layout will be unpositioned. This is so that the returned layout can be directly + * compared to `calculatedLayout` + */ +- (ASLayout *)currentLayoutBasedOnFrames; + +@end diff --git a/Tests/ASLayoutTestNode.mm b/Tests/ASLayoutTestNode.mm new file mode 100644 index 0000000000..3a112422a7 --- /dev/null +++ b/Tests/ASLayoutTestNode.mm @@ -0,0 +1,92 @@ +// +// ASLayoutTestNode.mm +// Texture +// +// Copyright (c) 2017-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 "ASLayoutTestNode.h" +#import +#import "OCMockObject+ASAdditions.h" + +@implementation ASLayoutTestNode + +- (instancetype)init +{ + if (self = [super init]) { + _mock = OCMStrictClassMock([ASDisplayNode class]); + + // If errors occur (e.g. unexpected method) we need to quickly figure out + // which node is at fault, so we inject the node name into the mock instance + // description. + __weak __typeof(self) weakSelf = self; + [_mock setModifyDescriptionBlock:^(id mock, NSString *baseDescription){ + return [NSString stringWithFormat:@"Mock(%@)", weakSelf.description]; + }]; + } + return self; +} + +- (ASLayout *)currentLayoutBasedOnFrames +{ + return [self _currentLayoutBasedOnFramesForRootNode:YES]; +} + +- (ASLayout *)_currentLayoutBasedOnFramesForRootNode:(BOOL)isRootNode +{ + auto sublayouts = [NSMutableArray array]; + for (ASLayoutTestNode *subnode in self.subnodes) { + [sublayouts addObject:[subnode _currentLayoutBasedOnFramesForRootNode:NO]]; + } + CGPoint rootPosition = isRootNode ? ASPointNull : self.frame.origin; + return [ASLayout layoutWithLayoutElement:self size:self.frame.size position:rootPosition sublayouts:sublayouts]; +} + +- (void)setTestSize:(CGSize)testSize +{ + if (!CGSizeEqualToSize(testSize, _testSize)) { + _testSize = testSize; + [self setNeedsLayout]; + } +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + [_mock calculateLayoutThatFits:constrainedSize]; + + // If we have a layout spec block, or no test size, return super. + if (self.layoutSpecBlock || CGSizeEqualToSize(self.testSize, CGSizeZero)) { + return [super calculateLayoutThatFits:constrainedSize]; + } else { + // Interestingly, the infra will auto-clamp sizes from calculateSizeThatFits, but not from calculateLayoutThatFits. + auto size = ASSizeRangeClamp(constrainedSize, self.testSize); + return [ASLayout layoutWithLayoutElement:self size:size]; + } +} + +#pragma mark - Forwarding to mock + +- (void)calculatedLayoutDidChange +{ + [_mock calculatedLayoutDidChange]; + [super calculatedLayoutDidChange]; +} + +- (void)didCompleteLayoutTransition:(id)context +{ + [_mock didCompleteLayoutTransition:context]; + [super didCompleteLayoutTransition:context]; +} + +- (void)animateLayoutTransition:(id)context +{ + [_mock animateLayoutTransition:context]; + [super animateLayoutTransition:context]; +} + +@end diff --git a/Tests/ASNavigationControllerTests.m b/Tests/ASNavigationControllerTests.m new file mode 100644 index 0000000000..80f6ba87fa --- /dev/null +++ b/Tests/ASNavigationControllerTests.m @@ -0,0 +1,56 @@ +// +// ASNavigationControllerTests.m +// Texture +// +// Copyright (c) 2017-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 +#import "ASNavigationController.h" + +@interface ASNavigationControllerTests : XCTestCase +@end + +@implementation ASNavigationControllerTests + +- (void)testSetViewControllers { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController, secondController]; + ASNavigationController *navigationController = [ASNavigationController new]; + [navigationController setViewControllers:@[firstController, secondController]]; + XCTAssertEqual(navigationController.topViewController, secondController); + XCTAssertEqual(navigationController.visibleViewController, secondController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +- (void)testPopViewController { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController]; + ASNavigationController *navigationController = [ASNavigationController new]; + [navigationController setViewControllers:@[firstController, secondController]]; + [navigationController popViewControllerAnimated:false]; + XCTAssertEqual(navigationController.topViewController, firstController); + XCTAssertEqual(navigationController.visibleViewController, firstController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +- (void)testPushViewController { + ASViewController *firstController = [ASViewController new]; + ASViewController *secondController = [ASViewController new]; + NSArray *expectedViewControllerStack = @[firstController, secondController]; + ASNavigationController *navigationController = [[ASNavigationController new] initWithRootViewController:firstController]; + [navigationController pushViewController:secondController animated:false]; + XCTAssertEqual(navigationController.topViewController, secondController); + XCTAssertEqual(navigationController.visibleViewController, secondController); + XCTAssertTrue([navigationController.viewControllers isEqualToArray:expectedViewControllerStack]); +} + +@end diff --git a/Tests/ASPagerNodeTests.m b/Tests/ASPagerNodeTests.m index 4b45a8c1ee..6f71577685 100644 --- a/Tests/ASPagerNodeTests.m +++ b/Tests/ASPagerNodeTests.m @@ -50,7 +50,8 @@ @implementation ASPagerNodeTestController -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Populate these immediately so that they're not unexpectedly nil during tests. @@ -74,7 +75,8 @@ @implementation ASPagerNodeTests -- (void)testPagerReturnsIndexOfPages { +- (void)testPagerReturnsIndexOfPages +{ ASPagerNodeTestController *testController = [self testController]; ASCellNode *cellNode = [testController.pagerNode nodeForPageAtIndex:0]; @@ -82,7 +84,8 @@ XCTAssertEqual([testController.pagerNode indexOfPageWithNode:cellNode], 0); } -- (void)testPagerReturnsNotFoundForCellThatDontExistInPager { +- (void)testPagerReturnsNotFoundForCellThatDontExistInPager +{ ASPagerNodeTestController *testController = [self testController]; ASCellNode *badNode = [[ASCellNode alloc] init]; @@ -90,7 +93,17 @@ XCTAssertEqual([testController.pagerNode indexOfPageWithNode:badNode], NSNotFound); } -- (ASPagerNodeTestController *)testController { +- (void)testScrollPageToIndex +{ + ASPagerNodeTestController *testController = [self testController]; + testController.pagerNode.frame = CGRectMake(0, 0, 500, 500); + [testController.pagerNode scrollToPageAtIndex:1 animated:false]; + + XCTAssertEqual(testController.pagerNode.currentPageIndex, 1); +} + +- (ASPagerNodeTestController *)testController +{ ASPagerNodeTestController *testController = [[ASPagerNodeTestController alloc] initWithNibName:nil bundle:nil]; UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; [window makeKeyAndVisible]; @@ -138,7 +151,7 @@ XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); XCTAssertEqual(pagerNode.contentOffset.y, 0); - XCTAssertEqual(pagerNode.view.contentInset.top, 0); + XCTAssertEqual(pagerNode.contentInset.top, 0); e = [self expectationWithDescription:@"Transition completed"]; // Push another view controller @@ -168,7 +181,7 @@ XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(node.frame)); XCTAssertEqualObjects(NSStringFromCGRect(window.bounds), NSStringFromCGRect(cell.frame)); XCTAssertEqual(pagerNode.contentOffset.y, 0); - XCTAssertEqual(pagerNode.view.contentInset.top, 0); + XCTAssertEqual(pagerNode.contentInset.top, 0); } @end diff --git a/Tests/ASRectTableTests.m b/Tests/ASRectMapTests.m similarity index 78% rename from Tests/ASRectTableTests.m rename to Tests/ASRectMapTests.m index ca897a3571..357f3879a6 100644 --- a/Tests/ASRectTableTests.m +++ b/Tests/ASRectMapTests.m @@ -1,5 +1,5 @@ // -// ASRectTableTests.m +// ASRectMapTests.m // Texture // // Created by Adlai Holler on 2/24/17. @@ -8,17 +8,17 @@ #import -#import "ASRectTable.h" +#import "ASRectMap.h" #import "ASXCTExtensions.h" -@interface ASRectTableTests : XCTestCase +@interface ASRectMapTests : XCTestCase @end -@implementation ASRectTableTests +@implementation ASRectMapTests - (void)testThatItStoresRects { - ASRectTable *table = [ASRectTable rectTableForWeakObjectPointers]; + ASRectMap *table = [ASRectMap rectMapForWeakObjectPointers]; NSObject *key0 = [[NSObject alloc] init]; NSObject *key1 = [[NSObject alloc] init]; ASXCTAssertEqualRects([table rectForKey:key0], CGRectNull); @@ -35,13 +35,13 @@ - (void)testCopying { - ASRectTable *table = [ASRectTable rectTableForWeakObjectPointers]; + ASRectMap *table = [ASRectMap rectMapForWeakObjectPointers]; NSObject *key = [[NSObject alloc] init]; ASXCTAssertEqualRects([table rectForKey:key], CGRectNull); CGRect rect0 = CGRectMake(0, 0, 100, 100); CGRect rect1 = CGRectMake(0, 0, 50, 50); [table setRect:rect0 forKey:key]; - ASRectTable *copy = [table copy]; + ASRectMap *copy = [table copy]; [copy setRect:rect1 forKey:key]; ASXCTAssertEqualRects([table rectForKey:key], rect0); diff --git a/Tests/ASRunLoopQueueTests.m b/Tests/ASRunLoopQueueTests.m new file mode 100644 index 0000000000..ccf15ae7b7 --- /dev/null +++ b/Tests/ASRunLoopQueueTests.m @@ -0,0 +1,160 @@ +// +// ASRunLoopQueueTests.m +// Texture +// +// Copyright (c) 2017-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 + +static NSTimeInterval const kRunLoopRunTime = 0.001; // Allow the RunLoop to run for one millisecond each time. + +@interface ASRunLoopQueueTests : XCTestCase + +@end + +@implementation ASRunLoopQueueTests + +#pragma mark enqueue tests + +- (void)testEnqueueNilObjectsToQueue +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil]; + id object = nil; + [queue enqueue:object]; + XCTAssertTrue(queue.isEmpty); +} + +- (void)testEnqueueSameObjectTwiceToDefaultQueue +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block NSUInteger dequeuedCount = 0; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + dequeuedCount++; + } + }]; + [queue enqueue:object]; + [queue enqueue:object]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssert(dequeuedCount == 1); +} + +- (void)testEnqueueSameObjectTwiceToNonExclusiveMembershipQueue +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block NSUInteger dequeuedCount = 0; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + dequeuedCount++; + } + }]; + queue.ensureExclusiveMembership = NO; + [queue enqueue:object]; + [queue enqueue:object]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssert(dequeuedCount == 2); +} + +#pragma mark processQueue tests + +- (void)testDefaultQueueProcessObjectsOneAtATime +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + [NSThread sleepForTimeInterval:kRunLoopRunTime * 2]; // So each element takes more time than the available + }]; + [queue enqueue:[[NSObject alloc] init]]; + [queue enqueue:[[NSObject alloc] init]]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(queue.isEmpty); +} + +- (void)testQueueProcessObjectsInBatchesOfSpecifiedSize +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + [NSThread sleepForTimeInterval:kRunLoopRunTime * 2]; // So each element takes more time than the available + }]; + queue.batchSize = 2; + [queue enqueue:[[NSObject alloc] init]]; + [queue enqueue:[[NSObject alloc] init]]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(queue.isEmpty); +} + +- (void)testQueueOnlySendsIsDrainedForLastObjectInBatch +{ + id objectA = [[NSObject alloc] init]; + id objectB = [[NSObject alloc] init]; + __unsafe_unretained id weakObjectA = objectA; + __unsafe_unretained id weakObjectB = objectB; + __block BOOL isQueueDrainedWhenProcessingA = NO; + __block BOOL isQueueDrainedWhenProcessingB = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObjectA) { + isQueueDrainedWhenProcessingA = isQueueDrained; + } else if (dequeuedItem == weakObjectB) { + isQueueDrainedWhenProcessingB = isQueueDrained; + } + }]; + queue.batchSize = 2; + [queue enqueue:objectA]; + [queue enqueue:objectB]; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(isQueueDrainedWhenProcessingA); + XCTAssertTrue(isQueueDrainedWhenProcessingB); +} + +#pragma mark strong/weak tests + +- (void)testStrongQueueRetainsObjects +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block BOOL didProcessObject = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + didProcessObject = YES; + } + }]; + [queue enqueue:object]; + object = nil; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(didProcessObject); +} + +- (void)testWeakQueueDoesNotRetainsObjects +{ + id object = [[NSObject alloc] init]; + __unsafe_unretained id weakObject = object; + __block BOOL didProcessObject = NO; + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:NO handler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { + if (dequeuedItem == weakObject) { + didProcessObject = YES; + } + }]; + [queue enqueue:object]; + object = nil; + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertFalse(didProcessObject); +} + +- (void)testWeakQueueWithAllDeallocatedObjectsIsDrained +{ + ASRunLoopQueue *queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:NO handler:nil]; + id object = [[NSObject alloc] init]; + [queue enqueue:object]; + object = nil; + XCTAssertFalse(queue.isEmpty); + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopRunTime]]; + XCTAssertTrue(queue.isEmpty); +} + +@end diff --git a/Tests/ASScrollNodeTests.m b/Tests/ASScrollNodeTests.m new file mode 100644 index 0000000000..56b3fd731b --- /dev/null +++ b/Tests/ASScrollNodeTests.m @@ -0,0 +1,139 @@ +// +// ASScrollNodeTests.m +// Texture +// +// Copyright (c) 2017-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 +#import +#import "ASXCTExtensions.h" + +@interface ASScrollNodeTests : XCTestCase + +@property (nonatomic) ASScrollNode *scrollNode; +@property (nonatomic) ASDisplayNode *subnode; + +@end + +@implementation ASScrollNodeTests + +- (void)setUp +{ + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + self.subnode = subnode; + + self.scrollNode = [[ASScrollNode alloc] init]; + self.scrollNode.scrollableDirections = ASScrollDirectionVerticalDirections; + self.scrollNode.automaticallyManagesContentSize = YES; + self.scrollNode.automaticallyManagesSubnodes = YES; + self.scrollNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [[ASWrapperLayoutSpec alloc] initWithLayoutElement:subnode]; + }; + [self.scrollNode view]; +} + +- (void)testSubnodeLayoutCalculatedWithUnconstrainedMaxSizeInScrollableDirection +{ + CGSize parentSize = CGSizeMake(100, 100); + ASSizeRange sizeRange = ASSizeRangeMake(parentSize); + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + + ASSizeRange subnodeSizeRange = sizeRange; + subnodeSizeRange.max.height = CGFLOAT_MAX; + XCTAssertEqual(self.scrollNode.scrollableDirections, ASScrollDirectionVerticalDirections); + ASXCTAssertEqualSizeRanges(self.subnode.constrainedSizeForCalculatedLayout, subnodeSizeRange); + + // Same test for horizontal scrollable directions + self.scrollNode.scrollableDirections = ASScrollDirectionHorizontalDirections; + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + + subnodeSizeRange = sizeRange; + subnodeSizeRange.max.width = CGFLOAT_MAX; + + ASXCTAssertEqualSizeRanges(self.subnode.constrainedSizeForCalculatedLayout, subnodeSizeRange); +} + +- (void)testAutomaticallyManagesContentSizeUnderflow +{ + CGSize subnodeSize = CGSizeMake(100, 100); + CGSize parentSize = CGSizeMake(100, 200); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, parentSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeOverflow +{ + CGSize subnodeSize = CGSizeMake(100, 500); + CGSize parentSize = CGSizeMake(100, 200); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, parentSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithSizeRangeSmallerThanParentSize +{ + CGSize subnodeSize = CGSizeMake(100, 100); + CGSize parentSize = CGSizeMake(100, 500); + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(100, 100), CGSizeMake(100, 200)); + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, sizeRange.max); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithSizeRangeBiggerThanParentSize +{ + CGSize subnodeSize = CGSizeMake(100, 200); + CGSize parentSize = CGSizeMake(100, 100); + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(100, 150)); + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, sizeRange.min); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +- (void)testAutomaticallyManagesContentSizeWithInvalidCalculatedSizeForLayout +{ + CGSize subnodeSize = CGSizeMake(100, 200); + CGSize parentSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + + self.subnode.style.preferredSize = subnodeSize; + + [self.scrollNode layoutThatFits:sizeRange parentSize:parentSize]; + [self.scrollNode layout]; + + ASXCTAssertEqualSizes(self.scrollNode.calculatedSize, subnodeSize); + ASXCTAssertEqualSizes(self.scrollNode.view.contentSize, subnodeSize); +} + +@end diff --git a/Tests/ASTLayoutFixture.h b/Tests/ASTLayoutFixture.h new file mode 100644 index 0000000000..ef590220a4 --- /dev/null +++ b/Tests/ASTLayoutFixture.h @@ -0,0 +1,61 @@ +// +// ASTLayoutFixture.h +// Texture +// +// Copyright (c) 2017-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 +#import "ASTestCase.h" +#import "ASLayoutTestNode.h" + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASTLayoutFixture : NSObject + +/// The correct layout. The root should be unpositioned (same as -calculatedLayout). +@property (nonatomic, strong, nullable) ASLayout *layout; + +/// The layoutSpecBlocks for non-leaf nodes. +@property (nonatomic, strong, readonly) NSMapTable *layoutSpecBlocks; + +@property (nonatomic, strong, readonly) ASLayoutTestNode *rootNode; + +@property (nonatomic, strong, readonly) NSSet *allNodes; + +/// Get the (correct) layout for the specified node. +- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node; + +/// Add this to the list of expected size ranges for the given node. +- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node; + +/// If you have a node that wants a size different than it gets, set it here. +/// For any leaf nodes that you don't call this on, the node will return the correct size +/// based on the fixture's layout. This is useful for triggering multipass stack layout. +- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node; + +/// Get the first expected size range for the node. +- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node; + +/// Enumerate all the size ranges for the node. +- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange sizeRange))block; + +/// Configure the nodes for this fixture. Set testSize on leaf nodes, layoutSpecBlock on container nodes. +- (void)apply; + +@end + +@interface ASLayout (TestHelpers) + +@property (nonatomic, readonly) NSArray *allNodes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/ASTLayoutFixture.mm b/Tests/ASTLayoutFixture.mm new file mode 100644 index 0000000000..bdddbe5bf2 --- /dev/null +++ b/Tests/ASTLayoutFixture.mm @@ -0,0 +1,134 @@ +// +// ASTLayoutFixture.mm +// Texture +// +// Copyright (c) 2017-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 "ASTLayoutFixture.h" + +@interface ASTLayoutFixture () + +/// The size ranges against which nodes are expected to be measured. +@property (nonatomic, strong, readonly) NSMapTable *> *sizeRanges; + +/// The overridden returned sizes for nodes where you want to trigger multipass layout. +@property (nonatomic, strong, readonly) NSMapTable *returnedSizes; + +@end + +@implementation ASTLayoutFixture + +- (instancetype)init +{ + if (self = [super init]) { + _sizeRanges = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + _layoutSpecBlocks = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + _returnedSizes = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory]; + + } + return self; +} + +- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node +{ + auto ranges = [_sizeRanges objectForKey:node]; + if (ranges == nil) { + ranges = [NSMutableArray array]; + [_sizeRanges setObject:ranges forKey:node]; + } + [ranges addObject:[NSValue valueWithBytes:&sizeRange objCType:@encode(ASSizeRange)]]; +} + +- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node +{ + [_returnedSizes setObject:[NSValue valueWithCGSize:size] forKey:node]; +} + +- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node +{ + auto val = [_sizeRanges objectForKey:node].firstObject; + ASSizeRange r; + [val getValue:&r]; + return r; +} + +- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange))block +{ + for (NSValue *value in [_sizeRanges objectForKey:node]) { + ASSizeRange r; + [value getValue:&r]; + block(r); + } +} + +- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node +{ + NSMutableArray *allLayouts = [NSMutableArray array]; + [ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts]; + for (ASLayout *layout in allLayouts) { + if (layout.layoutElement == node) { + return layout; + } + } + return nil; +} + +/// A very dumb tree iteration approach. NSEnumerator or something would be way better. ++ (void)collectAllLayoutsFromLayout:(ASLayout *)layout array:(NSMutableArray *)array +{ + [array addObject:layout]; + for (ASLayout *sublayout in layout.sublayouts) { + [self collectAllLayoutsFromLayout:sublayout array:array]; + } +} + +- (ASLayoutTestNode *)rootNode +{ + return (ASLayoutTestNode *)self.layout.layoutElement; +} + +- (NSSet *)allNodes +{ + auto allLayouts = [NSMutableArray array]; + [ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts]; + return [NSSet setWithArray:[allLayouts valueForKey:@"layoutElement"]]; +} + +- (void)apply +{ + // Update layoutSpecBlock for parent nodes, set automatic subnode management + for (ASDisplayNode *node in _layoutSpecBlocks) { + auto block = [_layoutSpecBlocks objectForKey:node]; + if (node.layoutSpecBlock != block) { + node.automaticallyManagesSubnodes = YES; + node.layoutSpecBlock = block; + [node setNeedsLayout]; + } + } + + [self setTestSizesOfLeafNodesInLayout:self.layout]; +} + +/// Go through the given layout, and for all the leaf nodes, set their preferredSize +/// to the layout size if needed, then call -setNeedsLayout +- (void)setTestSizesOfLeafNodesInLayout:(ASLayout *)layout +{ + auto node = (ASLayoutTestNode *)layout.layoutElement; + if (layout.sublayouts.count == 0) { + auto override = [self.returnedSizes objectForKey:node]; + node.testSize = override ? override.CGSizeValue : layout.size; + } else { + node.testSize = CGSizeZero; + for (ASLayout *sublayout in layout.sublayouts) { + [self setTestSizesOfLeafNodesInLayout:sublayout]; + } + } +} + +@end diff --git a/Tests/ASTabBarControllerTests.m b/Tests/ASTabBarControllerTests.m new file mode 100644 index 0000000000..0e6d9b3d31 --- /dev/null +++ b/Tests/ASTabBarControllerTests.m @@ -0,0 +1,45 @@ +// +// ASTabBarControllerTests.m +// Texture +// +// Copyright (c) 2017-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 "ASTabBarController.h" +#import "ASViewController.h" + +@interface ASTabBarControllerTests: XCTestCase + +@end + +@implementation ASTabBarControllerTests + +- (void)testTabBarControllerSelectIndex { + ASViewController *firstViewController = [ASViewController new]; + ASViewController *secondViewController = [ASViewController new]; + NSArray *viewControllers = @[firstViewController, secondViewController]; + ASTabBarController *tabBarController = [ASTabBarController new]; + [tabBarController setViewControllers:viewControllers]; + [tabBarController setSelectedIndex:1]; + XCTAssertTrue([tabBarController.viewControllers isEqualToArray:viewControllers]); + XCTAssertEqual(tabBarController.selectedViewController, secondViewController); +} + +- (void)testTabBarControllerSelectViewController { + ASViewController *firstViewController = [ASViewController new]; + ASViewController *secondViewController = [ASViewController new]; + NSArray *viewControllers = @[firstViewController, secondViewController]; + ASTabBarController *tabBarController = [ASTabBarController new]; + [tabBarController setViewControllers:viewControllers]; + [tabBarController setSelectedViewController:secondViewController]; + XCTAssertTrue([tabBarController.viewControllers isEqualToArray:viewControllers]); + XCTAssertEqual(tabBarController.selectedViewController, secondViewController); +} + +@end diff --git a/Tests/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index 2bf7936418..939c910fb1 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -37,10 +37,10 @@ @implementation ASTestDataController -- (void)relayoutAllNodes +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock { _numberOfAllNodesRelayouts++; - [super relayoutAllNodes]; + [super relayoutAllNodesWithInvalidationBlock:invalidationBlock]; } @end @@ -253,7 +253,8 @@ tableView.asyncDelegate = delegate; tableView.asyncDataSource = dataSource; - [tableView reloadDataImmediately]; + [tableView reloadData]; + [tableView waitUntilAllUpdatesAreCommitted]; [tableView setNeedsLayout]; [tableView layoutIfNeeded]; diff --git a/Tests/ASTableViewThrashTests.m b/Tests/ASTableViewThrashTests.m index e547fe39cb..fe2e2efd19 100644 --- a/Tests/ASTableViewThrashTests.m +++ b/Tests/ASTableViewThrashTests.m @@ -226,7 +226,8 @@ static atomic_uint ASThrashTestSectionNextID = 1; #else _tableView.asyncDelegate = self; _tableView.asyncDataSource = self; - [_tableView reloadDataImmediately]; + [_tableView reloadData]; + [_tableView waitUntilAllUpdatesAreCommitted]; #endif [_tableView layoutIfNeeded]; } diff --git a/Tests/ASTextKitTests.mm b/Tests/ASTextKitTests.mm index a39a333b10..6872061787 100644 --- a/Tests/ASTextKitTests.mm +++ b/Tests/ASTextKitTests.mm @@ -20,11 +20,14 @@ #import -#import #import +#import +#import #import #import +#import + @interface ASTextKitTests : XCTestCase @end @@ -201,4 +204,28 @@ static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize XCTAssert([renderer rectsForTextRange:NSMakeRange(0, attributedString.length) measureOption:ASTextKitRendererMeasureOptionBlock].count > 0); } +- (void)testTextKitComponentsCanCalculateSizeInBackground +{ + NSAttributedString *attributedString = + [[NSAttributedString alloc] + initWithString:@"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers Odd Future master cleanse tattooed four dollar toast small batch kale chips leggings meh photo booth occupy irony. " attributes:@{ASTextKitEntityAttributeName : [[ASTextKitEntityAttribute alloc] initWithEntity:@"entity"]}]; + ASTextKitComponents *components = [ASTextKitComponents componentsWithAttributedSeedString:attributedString textContainerSize:CGSizeZero]; + components.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:components.textContainer]; + components.textView.frame = CGRectMake(0, 0, 20, 1000); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Components deallocated in background"]; + + ASPerformBlockOnBackgroundThread(^{ + // Use an autorelease pool here to ensure temporary components are (and can be) released in background + @autoreleasepool { + [components sizeForConstrainedWidth:100]; + [components sizeForConstrainedWidth:50 forMaxNumberOfLines:5]; + } + + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + @end diff --git a/Tests/Common/ASTestCase.h b/Tests/Common/ASTestCase.h index 4868f64d05..54231b64e6 100644 --- a/Tests/Common/ASTestCase.h +++ b/Tests/Common/ASTestCase.h @@ -12,6 +12,11 @@ #import +// Not strictly necessary, but convenient +#import +#import +#import "OCMockObject+ASAdditions.h" + NS_ASSUME_NONNULL_BEGIN @interface ASTestCase : XCTestCase diff --git a/Tests/Common/OCMockObject+ASAdditions.h b/Tests/Common/OCMockObject+ASAdditions.h index a8e7004915..f8617411bb 100644 --- a/Tests/Common/OCMockObject+ASAdditions.h +++ b/Tests/Common/OCMockObject+ASAdditions.h @@ -10,7 +10,7 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import @interface OCMockObject (ASAdditions) @@ -30,4 +30,31 @@ */ - (void)addImplementedOptionalProtocolMethods:(SEL)aSelector, ... NS_REQUIRES_NIL_TERMINATION; +/// An optional block to modify description text. Only used in OCClassMockObject currently. +@property (atomic) NSString *(^modifyDescriptionBlock)(OCMockObject *object, NSString *baseDescription); + +@end + +/** + * Additional stub recorders useful in ASDK. + */ +@interface OCMStubRecorder (ASProperties) + +/** + * Add a debug-break side effect to this stub/expectation. + * + * You will usually need to jump to frame 12 "fr s 12" + */ +#define andDebugBreak() _andDebugBreak() +@property (nonatomic, readonly) OCMStubRecorder *(^ _andDebugBreak)(void); + +#define ignoringNonObjectArgs() _ignoringNonObjectArgs() +@property (nonatomic, readonly) OCMStubRecorder *(^ _ignoringNonObjectArgs)(void); + +#define onMainThread() _onMainThread() +@property (nonatomic, readonly) OCMStubRecorder *(^ _onMainThread)(void); + +#define offMainThread() _offMainThread() +@property (nonatomic, readonly) OCMStubRecorder *(^ _offMainThread)(void); + @end diff --git a/Tests/Common/OCMockObject+ASAdditions.m b/Tests/Common/OCMockObject+ASAdditions.m index 86dcdbf9d8..50fabd5eb6 100644 --- a/Tests/Common/OCMockObject+ASAdditions.m +++ b/Tests/Common/OCMockObject+ASAdditions.m @@ -15,6 +15,8 @@ #import #import #import "ASTestCase.h" +#import +#import "debugbreak.h" @interface ASTestCase (OCMockObjectRegistering) @@ -32,9 +34,18 @@ method_exchangeImplementations(orig, new); // init <-> swizzled_init - Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init)); - Method newInit = class_getInstanceMethod(self, @selector(swizzled_init)); - method_exchangeImplementations(origInit, newInit); + { + Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init)); + Method newInit = class_getInstanceMethod(self, @selector(swizzled_init)); + method_exchangeImplementations(origInit, newInit); + } + + // (class mock) description <-> swizzled_classMockDescription + { + Method orig = class_getInstanceMethod(OCMockObject.classMockObjectClass, @selector(description)); + Method new = class_getInstanceMethod(self, @selector(swizzled_classMockDescription)); + method_exchangeImplementations(orig, new); + } } /// Since OCProtocolMockObject is private, use this method to get the class. @@ -49,6 +60,18 @@ return c; } +/// Since OCClassMockObject is private, use this method to get the class. ++ (Class)classMockObjectClass +{ + static Class c; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + c = NSClassFromString(@"OCClassMockObject"); + NSAssert(c != Nil, nil); + }); + return c; +} + /// Whether the user has opted-in to specify which optional methods are implemented for this object. - (BOOL)hasSpecifiedOptionalProtocolMethods { @@ -142,4 +165,77 @@ return self; } +- (NSString *)swizzled_classMockDescription +{ + NSString *orig = [self swizzled_classMockDescription]; + __auto_type block = self.modifyDescriptionBlock; + if (block) { + return block(self, orig); + } + return orig; +} + +- (void)setModifyDescriptionBlock:(NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock +{ + objc_setAssociatedObject(self, @selector(modifyDescriptionBlock), modifyDescriptionBlock, OBJC_ASSOCIATION_COPY); +} + +- (NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock +{ + return objc_getAssociatedObject(self, _cmd); +} + +@end + +@implementation OCMStubRecorder (ASProperties) + +@dynamic _ignoringNonObjectArgs; + +- (OCMStubRecorder *(^)(void))_ignoringNonObjectArgs +{ + id (^theBlock)(void) = ^ () + { + return [self ignoringNonObjectArgs]; + }; + return theBlock; +} + +@dynamic _onMainThread; + +- (OCMStubRecorder *(^)(void))_onMainThread +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + ASDisplayNodeAssertMainThread(); + }]; + }; + return theBlock; +} + +@dynamic _offMainThread; + +- (OCMStubRecorder *(^)(void))_offMainThread +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + ASDisplayNodeAssertNotMainThread(); + }]; + }; + return theBlock; +} + +@dynamic _andDebugBreak; + +- (OCMStubRecorder *(^)(void))_andDebugBreak +{ + id (^theBlock)(void) = ^ () + { + return [self andDo:^(NSInvocation *invocation) { + debug_break(); + }]; + }; + return theBlock; +} @end diff --git a/Tests/Common/debugbreak.h b/Tests/Common/debugbreak.h new file mode 100644 index 0000000000..5405e40de7 --- /dev/null +++ b/Tests/Common/debugbreak.h @@ -0,0 +1,146 @@ +// +// debugbreak.h +// Texture +// +// Copyright (c) 2017-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 +// + +/* Copyright (c) 2011-2015, Scott Tsai + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef DEBUG_BREAK_H +#define DEBUG_BREAK_H + +#ifdef _MSC_VER + +#define debug_break __debugbreak + +#else + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + /* gcc optimizers consider code after __builtin_trap() dead. + * Making __builtin_trap() unsuitable for breaking into the debugger */ + DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP = 0, +}; + +#if defined(__i386__) || defined(__x86_64__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + __asm__ volatile("int $0x03"); +} +#elif defined(__thumb__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +/* FIXME: handle __THUMB_INTERWORK__ */ +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'arm-linux-tdep.c' in GDB source. + * Both instruction sequences below work. */ +#if 1 + /* 'eabi_linux_thumb_le_breakpoint' */ + __asm__ volatile(".inst 0xde01"); +#else + /* 'eabi_linux_thumb2_le_breakpoint' */ + __asm__ volatile(".inst.w 0xf7f0a000"); +#endif + + /* Known problem: + * After a breakpoint hit, can't stepi, step, or continue in GDB. + * 'step' stuck on the same instruction. + * + * Workaround: a new GDB command, + * 'debugbreak-step' is defined in debugbreak-gdb.py + * that does: + * (gdb) set $instruction_len = 2 + * (gdb) tbreak *($pc + $instruction_len) + * (gdb) jump *($pc + $instruction_len) + */ +} +#elif defined(__arm__) && !defined(__thumb__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'arm-linux-tdep.c' in GDB source, + * 'eabi_linux_arm_le_breakpoint' */ + __asm__ volatile(".inst 0xe7f001f0"); + /* Has same known problem and workaround + * as Thumb mode */ +} +#elif defined(__aarch64__) +enum { HAVE_TRAP_INSTRUCTION = 1, }; +__attribute__((gnu_inline, always_inline)) +__inline__ static void trap_instruction(void) +{ + /* See 'aarch64-tdep.c' in GDB source, + * 'aarch64_default_breakpoint' */ + __asm__ volatile(".inst 0xd4200000"); +} +#else +enum { HAVE_TRAP_INSTRUCTION = 0, }; +#endif + +__attribute__((gnu_inline, always_inline)) +__inline__ static void debug_break(void) +{ + if (HAVE_TRAP_INSTRUCTION) { + trap_instruction(); + } else if (DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP) { + /* raises SIGILL on Linux x86{,-64}, to continue in gdb: + * (gdb) handle SIGILL stop nopass + * */ + __builtin_trap(); + } else { + #ifdef _WIN32 + /* SIGTRAP available only on POSIX-compliant operating systems + * use builtin trap instead */ + __builtin_trap(); + #else + raise(SIGTRAP); + #endif + } +} + +#ifdef __cplusplus +} +#endif + +#endif + +#endif diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_childSize@2x.png new file mode 100644 index 0000000000..3b78fb5e7d Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_fullSize@2x.png new file mode 100644 index 0000000000..3b78fb5e7d Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomLeft_inner_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_childSize@2x.png new file mode 100644 index 0000000000..34851067b9 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_fullSize@2x.png new file mode 100644 index 0000000000..34851067b9 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_bottomRight_inner_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_childSize@2x.png new file mode 100644 index 0000000000..aa4c3ee8d8 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_fullSize@2x.png new file mode 100644 index 0000000000..aa4c3ee8d8 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topLeft_inner_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_childSize@2x.png new file mode 100644 index 0000000000..23082ede8c Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_fullSize@2x.png new file mode 100644 index 0000000000..23082ede8c Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithInnerOffset_topRight_inner_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_fullSize@2x.png new file mode 100644 index 0000000000..393143a214 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomLeft_outer_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_fullSize@2x.png new file mode 100644 index 0000000000..12498681e0 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_bottomRight_outer_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_fullSize@2x.png new file mode 100644 index 0000000000..dc4f1ab2b3 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topLeft_outer_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_childSize@2x.png new file mode 100644 index 0000000000..18e211ae8e Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_fullSize@2x.png new file mode 100644 index 0000000000..fa7e15a554 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocationsWithOuterOffset_topRight_outer_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_childSize@2x.png new file mode 100644 index 0000000000..90f411aff2 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_fullSize@2x.png new file mode 100644 index 0000000000..6d49323c17 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomLeft_center_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_childSize@2x.png new file mode 100644 index 0000000000..9d23e2b646 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_fullSize@2x.png new file mode 100644 index 0000000000..58257ffef6 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_bottomRight_center_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_childSize@2x.png new file mode 100644 index 0000000000..3503fd79dd Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_fullSize@2x.png new file mode 100644 index 0000000000..263f50d29c Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topLeft_center_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_childSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_childSize@2x.png new file mode 100644 index 0000000000..492fc049b8 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_childSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_fullSize@2x.png b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_fullSize@2x.png new file mode 100644 index 0000000000..9e39a3c5c4 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_fullSize@2x.png differ diff --git a/Texture.podspec b/Texture.podspec index 9c53d9f4c1..73e9047728 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Texture' - spec.version = '2.4' + spec.version = '2.6' spec.license = { :type => 'BSD and Apache 2', } spec.homepage = 'http://texturegroup.org' spec.authors = { 'Huy Nguyen' => 'huy@pinterest.com', 'Garrett Moon' => 'garrett@excitedpixel.com', 'Scott Goodson' => 'scottgoodson@gmail.com', 'Michael Schneider' => 'schneider@pinterest.com', 'Adlai Holler' => 'adlai@pinterest.com' } @@ -45,7 +45,7 @@ Pod::Spec.new do |spec| end spec.subspec 'PINRemoteImage' do |pin| - pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.11' + pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.13' pin.dependency 'PINRemoteImage/PINCache' pin.dependency 'Texture/Core' end @@ -57,7 +57,7 @@ Pod::Spec.new do |spec| spec.subspec 'Yoga' do |yoga| yoga.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) YOGA=1' } - yoga.dependency 'Yoga', '1.5.0' + yoga.dependency 'Yoga', '1.6.0' yoga.dependency 'Texture/Core' end diff --git a/build.sh b/build.sh index 1684c2497d..bffaad4cc4 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/bin/bash -PLATFORM="platform=iOS Simulator,name=iPhone 7" -SDK="iphonesimulator" +PLATFORM="platform=iOS Simulator,OS=10.2,name=iPhone 7" +SDK="iphonesimulator11.0" DERIVED_DATA_PATH="~/ASDKDerivedData" diff --git a/docs/_docs/containers-asnodecontroller.md b/docs/_docs/containers-asnodecontroller.md index 7dd5034dab..09b644440f 100755 --- a/docs/_docs/containers-asnodecontroller.md +++ b/docs/_docs/containers-asnodecontroller.md @@ -60,7 +60,7 @@ All of this logic can be removed from where it previously existed in the "view" - + + ### Example using `ASDimension` @@ -68,8 +70,10 @@ self.rightStack.style.flexBasis = ASDimensionMake(@"60%"); self.leftStack.style.flexBasis = ASDimensionMake("40%") self.rightStack.style.flexBasis = ASDimensionMake("60%") -horizontalStack.children = [self.leftStack, self.rightStack]] +horizontalStack.children = [self.leftStack, self.rightStack] + + ## Sizes (`CGSize`, `ASLayoutSize`) @@ -187,5 +191,4 @@ func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec -
The `constrainedSize` passed to an `ASDisplayNode` subclass' `layoutSpecThatFits:` method is the minimum and maximum sizes that the node should fit in. The minimum and maximum `CGSize`s contained in `constrainedSize` can be used to size the node's layout elements. diff --git a/docs/_docs/layout2-layoutspec-types.md b/docs/_docs/layout2-layoutspec-types.md index e816ab3ca7..ad14cf0754 100755 --- a/docs/_docs/layout2-layoutspec-types.md +++ b/docs/_docs/layout2-layoutspec-types.md @@ -240,8 +240,8 @@ When using Automatic Subnode Management with the ASOverlayLayoutSpec override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - let backgroundNode = ASDisplayNodeWithBackgroundColor(UIColor.blue) - let foregroundNode = ASDisplayNodeWithBackgroundColor(UIColor.red) + let backgroundNode = ASDisplayNodeWithBackgroundColor(UIColor.red) + let foregroundNode = ASDisplayNodeWithBackgroundColor(UIColor.blue) return ASBackgroundLayoutSpec(child: foregroundNode, background: backgroundNode) } @@ -452,7 +452,7 @@ Another use of `ASLayoutSpec` is to be used as a spacer in a `ASStackLayoutSpec` ... // ASLayoutSpec as spacer ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; - spacer.flexGrow = true; + spacer.style.flexGrow = true; stack.children = @[imageNode, spacer, textNode]; ... diff --git a/docs/_docs/resources.md b/docs/_docs/resources.md index 62113da1e2..51a388a7d6 100755 --- a/docs/_docs/resources.md +++ b/docs/_docs/resources.md @@ -21,6 +21,7 @@ If you are new to Texture, we recommend that you start with the Building smooth and responsive UI with Texture [CocoaHeadsNL 2017]
  • AsyncDisplayKit 2.0: Defining the 7th Abstraction Layer [Pinterest HQ 2016]
  • Layout at Scale with AsyncDisplayKit 2.0 [NSMeetup 2016]
  • ASCollectionNode [Pinterest HQ 2016]
  • diff --git a/docs/_docs/subtree-rasterization.md b/docs/_docs/subtree-rasterization.md index 8671a524ff..20aa0761bd 100755 --- a/docs/_docs/subtree-rasterization.md +++ b/docs/_docs/subtree-rasterization.md @@ -14,10 +14,10 @@ With all Texture nodes, enabling precompositing is as simple as: SwiftObjective-C
    -rootNode.shouldRasterizeDescendants = YES;
    +[rootNode enableSubtreeRasterization];
     
    diff --git a/docs/_docs/synchronous-concurrency.md b/docs/_docs/synchronous-concurrency.md index 68e9aa4575..d4430b7516 100755 --- a/docs/_docs/synchronous-concurrency.md +++ b/docs/_docs/synchronous-concurrency.md @@ -12,7 +12,7 @@ By setting this property to YES, the main thread will be blocked until display h Using this option does not eliminate all of the performance advantages of Texture. Normally, a given node has been preloading and is almost done when it reaches the screen, so the blocking time is very short. Even if the rangeTuningParameters are set to 0 this option outperforms UIKit. While the main thread is waiting, all subnode display executes concurrently, thus synchronous concurrency. -See the NSSpain 2015 talk video for a visual walkthrough of this behavior. +See the NSSpain 2015 talk video for a visual walkthrough of this behavior.
    SwiftObjective-C diff --git a/docs/showcase.md b/docs/showcase.md index b90d9957a3..4b3e68233e 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -189,6 +189,44 @@ permalink: /showcase.html HakkerJobs + + +
    + TraceMe + + + + + + + + +
    + Pairs + + + + +
    + Sorted: Master Your Day + + + + +
    + Vingle + + + + + + + + +
    + Blendle + + diff --git a/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png b/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png new file mode 100644 index 0000000000..2a3ac02101 Binary files /dev/null and b/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png differ diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index fa093f80ad..3755b01d69 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -112,7 +112,7 @@ #pragma mark - ASCollectionGalleryLayoutPropertiesProviding -- (CGSize)sizeForElements:(ASElementMap *)elements +- (CGSize)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); return CGSizeMake(180, 90); diff --git a/examples/ASDKgram/Sample/PhotoFeedBaseController.m b/examples/ASDKgram/Sample/PhotoFeedBaseController.m index c901d737e2..1d883ba7b1 100644 --- a/examples/ASDKgram/Sample/PhotoFeedBaseController.m +++ b/examples/ASDKgram/Sample/PhotoFeedBaseController.m @@ -59,7 +59,7 @@ [_activityIndicatorView stopAnimating]; - [self insertNewRows:newPhotos]; + [self.tableView reloadData]; [self requestCommentsForPhotos:newPhotos]; // immediately start second larger fetch @@ -74,7 +74,8 @@ NSMutableArray *indexPaths = [NSMutableArray array]; NSInteger newTotalNumberOfPhotos = [_photoFeed numberOfItemsInFeed]; - for (NSInteger row = newTotalNumberOfPhotos - newPhotos.count; row < newTotalNumberOfPhotos; row++) { + NSInteger existingNumberOfPhotos = newTotalNumberOfPhotos - newPhotos.count; + for (NSInteger row = existingNumberOfPhotos; row < newTotalNumberOfPhotos; row++) { NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:section]; [indexPaths addObject:path]; } diff --git a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m index 90d88fbe27..fe9b30bb33 100644 --- a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m +++ b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m @@ -1,20 +1,18 @@ // // ViewController.m -// Sample -// -// Created by Garrett Moon on 3/22/16. +// Texture // // Copyright (c) 2014-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. +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. 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 "ViewController.h" @@ -32,7 +30,9 @@ // Do any additional setup after loading the view, typically from a nib. ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init]; - imageNode.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"]; + imageNode.URL = [NSURL URLWithString:@"https://i.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"]; + // Uncomment to see animated webp support + // imageNode.URL = [NSURL URLWithString:@"https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp"]; imageNode.frame = self.view.bounds; imageNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; imageNode.contentMode = UIViewContentModeScaleAspectFit; diff --git a/examples/AnimatedGIF/Podfile b/examples/AnimatedGIF/Podfile index 922ff50ec1..e784c52d14 100644 --- a/examples/AnimatedGIF/Podfile +++ b/examples/AnimatedGIF/Podfile @@ -2,5 +2,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' target 'Sample' do pod 'Texture', :path => '../..' + pod 'PINRemoteImage/WebP' end diff --git a/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj b/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj index 1ecfa3fb97..6969496c87 100755 --- a/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj +++ b/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj @@ -121,7 +121,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0810; - LastUpgradeCheck = 0810; + LastUpgradeCheck = 0910; ORGANIZATIONNAME = AsyncDisplayKit; TargetAttributes = { 5D823AD01DD3B7770075E14A = { @@ -183,9 +183,18 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -198,13 +207,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -244,7 +256,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -252,7 +266,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; @@ -276,7 +294,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -294,7 +312,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -302,7 +322,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; @@ -320,7 +344,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index 1c12aaa4d4..ec900dc24f 100644 --- a/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme +++ b/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -1,6 +1,6 @@ @@ -45,6 +46,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/examples/CustomCollectionView-Swift/Sample.xcworkspace/contents.xcworkspacedata b/examples/CustomCollectionView-Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj b/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj index 5572695204..d69d6e6d85 100644 --- a/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj +++ b/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj @@ -139,7 +139,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0810; + LastUpgradeCheck = 0910; ORGANIZATIONNAME = Facebook; TargetAttributes = { 050E7C6D19D22E19004363C2 = { @@ -184,9 +184,18 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -214,13 +223,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -262,14 +274,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -308,14 +326,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; diff --git a/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index 34c6101e3e..05f94265c3 100644 --- a/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme +++ b/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -1,6 +1,6 @@ @@ -45,6 +46,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/examples/LayoutSpecExamples-Swift/Sample.xcworkspace/contents.xcworkspacedata b/examples/LayoutSpecExamples-Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/LayoutSpecExamples-Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/LayoutSpecExamples-Swift/Sample/AppDelegate.swift b/examples/LayoutSpecExamples-Swift/Sample/AppDelegate.swift index eeeba4f0ae..6cce6558a8 100644 --- a/examples/LayoutSpecExamples-Swift/Sample/AppDelegate.swift +++ b/examples/LayoutSpecExamples-Swift/Sample/AppDelegate.swift @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let window = UIWindow(frame: UIScreen.main.bounds) window.backgroundColor = UIColor.white - window.rootViewController = UINavigationController(rootViewController: OverviewViewController()); + window.rootViewController = UINavigationController(rootViewController: OverviewViewController()) window.makeKeyAndVisible() self.window = window diff --git a/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode.swift b/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode.swift index 75531d9026..8be3356839 100644 --- a/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode.swift +++ b/examples/LayoutSpecExamples-Swift/Sample/LayoutExampleNode.swift @@ -143,3 +143,140 @@ class FlexibleSeparatorSurroundingContent : LayoutExampleNode { return "try rotating me!" } } + +class CornerLayoutSample : PhotoWithOutsetIconOverlay { + let photoNode1 = ASImageNode() + let photoNode2 = ASImageNode() + let dotNode = ASImageNode() + let badgeTextNode = ASTextNode() + let badgeImageNode = ASImageNode() + + struct ImageSize { + static let avatar = CGSize(width: 100, height: 100) + static let icon = CGSize(width: 26, height: 26) + } + + struct ImageColor { + static let avatar = UIColor.lightGray + static let icon = UIColor.red + } + + required init() { + super.init() + + let avatarImage = UIImage.draw(size: ImageSize.avatar, fillColor: ImageColor.avatar) { () -> UIBezierPath in + return UIBezierPath(roundedRect: CGRect(origin: CGPoint.zero, size: ImageSize.avatar), cornerRadius: ImageSize.avatar.width / 20) + } + + let iconImage = UIImage.draw(size: ImageSize.icon, fillColor: ImageColor.icon) { () -> UIBezierPath in + return UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: ImageSize.icon)) + } + + photoNode1.image = avatarImage + photoNode2.image = avatarImage + dotNode.image = iconImage + + badgeTextNode.attributedText = NSAttributedString.attributedString(string: " 999+ ", fontSize: 20, color: .white) + + badgeImageNode.image = UIImage.as_resizableRoundedImage(withCornerRadius: 12, cornerColor: .clear, fill: .red) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + photoNode.style.preferredSize = ImageSize.avatar + iconNode.style.preferredSize = ImageSize.icon + + let badgeSpec = ASBackgroundLayoutSpec(child: badgeTextNode, background: badgeImageNode) + let cornerSpec1 = ASCornerLayoutSpec(child: photoNode1, corner: dotNode, location: .topRight) + let cornerSpec2 = ASCornerLayoutSpec(child: photoNode2, corner: badgeSpec, location: .topRight) + let cornerSpec3 = ASCornerLayoutSpec(child: photoNode, corner: iconNode, location: .topRight) + + cornerSpec1.offset = CGPoint(x: -3, y: 3) + + let stackSpec = ASStackLayoutSpec.vertical() + stackSpec.spacing = 40 + stackSpec.children = [cornerSpec1, cornerSpec2, cornerSpec3] + + return stackSpec + } + + override class func title() -> String { + return "Declarative way for Corner image Layout" + } + + override class func descriptionTitle() -> String? { + return nil + } +} + +class UserProfileSample : LayoutExampleNode { + + let badgeNode = ASImageNode() + let avatarNode = ASImageNode() + let usernameNode = ASTextNode() + let subtitleNode = ASTextNode() + + struct ImageSize { + static let avatar = CGSize(width: 44, height: 44) + static let badge = CGSize(width: 15, height: 15) + } + + struct ImageColor { + static let avatar = UIColor.lightGray + static let badge = UIColor.red + } + + required init() { + super.init() + + avatarNode.image = UIImage.draw(size: ImageSize.avatar, fillColor: ImageColor.avatar) { () -> UIBezierPath in + return UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: ImageSize.avatar)) + } + + badgeNode.image = UIImage.draw(size: ImageSize.badge, fillColor: ImageColor.badge) { () -> UIBezierPath in + return UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: ImageSize.badge)) + } + + makeSingleLine(for: usernameNode, with: "Hello world", fontSize: 17, textColor: .black) + makeSingleLine(for: subtitleNode, with: "This is a long long subtitle, with a long long appended string.", fontSize: 14, textColor: .lightGray) + } + + private func makeSingleLine(for node: ASTextNode, with text: String, fontSize: CGFloat, textColor: UIColor) { + node.attributedText = NSAttributedString.attributedString(string: text, fontSize: fontSize, color: textColor) + node.maximumNumberOfLines = 1 + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let avatarBox = ASCornerLayoutSpec(child: avatarNode, corner: badgeNode, location: .bottomRight) + avatarBox.offset = CGPoint(x: -6, y: -6) + + let textBox = ASStackLayoutSpec.vertical() + textBox.justifyContent = .spaceAround + textBox.children = [usernameNode, subtitleNode] + + let profileBox = ASStackLayoutSpec.horizontal() + profileBox.spacing = 10 + profileBox.children = [avatarBox, textBox] + + // Apply text truncation + let elems: [ASLayoutElement] = [usernameNode, subtitleNode, textBox, profileBox] + for elem in elems { + elem.style.flexShrink = 1 + } + + let insetBox = ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 120, left: 20, bottom: CGFloat.infinity, right: 20), + child: profileBox + ) + + return insetBox + } + + override class func title() -> String { + return "Common user profile layout." + } + + override class func descriptionTitle() -> String? { + return "For corner image layout and text truncation." + } + +} diff --git a/examples/LayoutSpecExamples-Swift/Sample/OverviewViewController.swift b/examples/LayoutSpecExamples-Swift/Sample/OverviewViewController.swift index 6e5240c7d2..e4b6f5d5a4 100644 --- a/examples/LayoutSpecExamples-Swift/Sample/OverviewViewController.swift +++ b/examples/LayoutSpecExamples-Swift/Sample/OverviewViewController.swift @@ -26,7 +26,9 @@ class OverviewViewController: ASViewController { HeaderWithRightAndLeftItems.self, PhotoWithInsetTextOverlay.self, PhotoWithOutsetIconOverlay.self, - FlexibleSeparatorSurroundingContent.self + FlexibleSeparatorSurroundingContent.self, + CornerLayoutSample.self, + UserProfileSample.self ] super.init(node: tableNode) diff --git a/examples/LayoutSpecExamples-Swift/Sample/Utilities.swift b/examples/LayoutSpecExamples-Swift/Sample/Utilities.swift index 683130618b..1f1bad89ce 100644 --- a/examples/LayoutSpecExamples-Swift/Sample/Utilities.swift +++ b/examples/LayoutSpecExamples-Swift/Sample/Utilities.swift @@ -74,7 +74,21 @@ extension UIImage { return roundedImage ?? self } - + + class func draw(size: CGSize, fillColor: UIColor, shapeClosure: () -> UIBezierPath) -> UIImage { + UIGraphicsBeginImageContext(size) + + let path = shapeClosure() + path.addClip() + + fillColor.setFill() + path.fill() + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image! + } } extension NSAttributedString { diff --git a/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj b/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj index 8027131811..3c9041a2b3 100644 --- a/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj +++ b/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj @@ -220,13 +220,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata b/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.h b/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.h index 7355285d3c..65241146ba 100644 --- a/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.h +++ b/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.h @@ -33,3 +33,9 @@ @interface FlexibleSeparatorSurroundingContent : LayoutExampleNode @end + +@interface CornerLayoutExample : PhotoWithOutsetIconOverlay +@end + +@interface UserProfileSample : LayoutExampleNode +@end diff --git a/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m b/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m index e7f9fbe6f0..9b84cb16e4 100644 --- a/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m +++ b/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m @@ -260,6 +260,188 @@ @end +@interface CornerLayoutExample () +@property (nonatomic, strong) ASImageNode *dotNode; +@property (nonatomic, strong) ASImageNode *photoNode1; +@property (nonatomic, strong) ASTextNode *badgeTextNode; +@property (nonatomic, strong) ASImageNode *badgeImageNode; +@property (nonatomic, strong) ASImageNode *photoNode2; +@end + +@implementation CornerLayoutExample + +static CGFloat const kSampleAvatarSize = 100; +static CGFloat const kSampleIconSize = 26; +static CGFloat const kSampleBadgeCornerRadius = 12; + ++ (NSString *)title +{ + return @"Declarative way for Corner image Layout"; +} + ++ (NSString *)descriptionTitle +{ + return nil; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + UIImage *avatarImage = [self avatarImageWithSize:CGSizeMake(kSampleAvatarSize, kSampleAvatarSize)]; + UIImage *cornerImage = [self cornerImageWithSize:CGSizeMake(kSampleIconSize, kSampleIconSize)]; + + NSAttributedString *numberText = [NSAttributedString attributedStringWithString:@" 999+ " fontSize:20 color:UIColor.whiteColor]; + + _dotNode = [ASImageNode new]; + _dotNode.image = cornerImage; + + _photoNode1 = [ASImageNode new]; + _photoNode1.image = avatarImage; + + _badgeTextNode = [ASTextNode new]; + _badgeTextNode.attributedText = numberText; + + _badgeImageNode = [ASImageNode new]; + _badgeImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:kSampleBadgeCornerRadius + cornerColor:UIColor.clearColor + fillColor:UIColor.redColor]; + + _photoNode2 = [ASImageNode new]; + _photoNode2.image = avatarImage; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + + ASBackgroundLayoutSpec *badgeSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:_badgeTextNode + background:_badgeImageNode]; + + ASCornerLayoutSpec *cornerSpec1 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:_photoNode1 corner:_dotNode location:ASCornerLayoutLocationTopRight]; + cornerSpec1.offset = CGPointMake(-3, 3); + + ASCornerLayoutSpec *cornerSpec2 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:_photoNode2 corner:badgeSpec location:ASCornerLayoutLocationTopRight]; + + self.photoNode.style.preferredSize = CGSizeMake(kSampleAvatarSize, kSampleAvatarSize); + self.iconNode.style.preferredSize = CGSizeMake(kSampleIconSize, kSampleIconSize); + + ASCornerLayoutSpec *cornerSpec3 = [ASCornerLayoutSpec cornerLayoutSpecWithChild:self.photoNode corner:self.iconNode location:ASCornerLayoutLocationTopRight]; + + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec verticalStackLayoutSpec]; + stackSpec.spacing = 40; + stackSpec.children = @[cornerSpec1, cornerSpec2, cornerSpec3]; + + return stackSpec; +} + +- (UIImage *)avatarImageWithSize:(CGSize)size +{ + return [UIImage imageWithSize:size fillColor:UIColor.lightGrayColor shapeBlock:^UIBezierPath *{ + CGRect rect = (CGRect){ CGPointZero, size }; + return [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:MIN(size.width, size.height) / 20]; + }]; +} + +- (UIImage *)cornerImageWithSize:(CGSize)size +{ + return [UIImage imageWithSize:size fillColor:UIColor.redColor shapeBlock:^UIBezierPath *{ + return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, size }]; + }]; +} + +@end + + +@interface UserProfileSample () +@property (nonatomic, strong) ASImageNode *badgeNode; +@property (nonatomic, strong) ASImageNode *avatarNode; +@property (nonatomic, strong) ASTextNode *usernameNode; +@property (nonatomic, strong) ASTextNode *subtitleNode; +@property (nonatomic, assign) CGFloat photoSizeValue; +@property (nonatomic, assign) CGFloat iconSizeValue; +@end + +@implementation UserProfileSample + ++ (NSString *)title +{ + return @"Common user profile layout."; +} + ++ (NSString *)descriptionTitle +{ + return @"For corner image layout and text truncation."; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _photoSizeValue = 44; + _iconSizeValue = 15; + + CGSize iconSize = CGSizeMake(_iconSizeValue, _iconSizeValue); + CGSize photoSize = CGSizeMake(_photoSizeValue, _photoSizeValue); + + _badgeNode = [ASImageNode new]; + _badgeNode.style.preferredSize = iconSize; + _badgeNode.image = [UIImage imageWithSize:iconSize fillColor:UIColor.redColor shapeBlock:^UIBezierPath *{ + return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, iconSize }]; + }]; + + _avatarNode = [ASImageNode new]; + _avatarNode.style.preferredSize = photoSize; + _avatarNode.image = [UIImage imageWithSize:photoSize fillColor:UIColor.lightGrayColor shapeBlock:^UIBezierPath *{ + return [UIBezierPath bezierPathWithOvalInRect:(CGRect){ CGPointZero, photoSize }]; + }]; + + _usernameNode = [ASTextNode new]; + _usernameNode.attributedText = [NSAttributedString attributedStringWithString:@"Hello World" fontSize:17 color:UIColor.blackColor]; + _usernameNode.maximumNumberOfLines = 1; + + _subtitleNode = [ASTextNode new]; + _subtitleNode.attributedText = [NSAttributedString attributedStringWithString:@"This is a long long subtitle, with a long long appended string." fontSize:14 color:UIColor.lightGrayColor]; + _subtitleNode.maximumNumberOfLines = 1; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + // Apply avatar with badge + // Normally, avatar's box size is the only photo size and it will not include the badge size. + // Otherwise, use includeCornerForSizeCalculation property to increase the box's size if needed. + ASCornerLayoutSpec *avatarBox = [ASCornerLayoutSpec new]; + avatarBox.child = _avatarNode; + avatarBox.corner = _badgeNode; + avatarBox.cornerLocation = ASCornerLayoutLocationBottomRight; + avatarBox.offset = CGPointMake(-6, -6); + + ASStackLayoutSpec *textBox = [ASStackLayoutSpec verticalStackLayoutSpec]; + textBox.justifyContent = ASStackLayoutJustifyContentSpaceAround; + textBox.children = @[_usernameNode, _subtitleNode]; + + ASStackLayoutSpec *profileBox = [ASStackLayoutSpec horizontalStackLayoutSpec]; + profileBox.spacing = 10; + profileBox.children = @[avatarBox, textBox]; + + // Apply text truncation. + NSArray *elems = @[_usernameNode, _subtitleNode, textBox, profileBox]; + for (id elem in elems) { + elem.style.flexShrink = 1; + } + + ASInsetLayoutSpec *profileInsetBox = [ASInsetLayoutSpec new]; + profileInsetBox.insets = UIEdgeInsetsMake(120, 20, INFINITY, 20); + profileInsetBox.child = profileBox; + + return profileInsetBox; +} + +@end + @implementation LayoutExampleNode + (NSString *)title diff --git a/examples/LayoutSpecExamples/Sample/OverviewViewController.m b/examples/LayoutSpecExamples/Sample/OverviewViewController.m index e3f44dfb06..c3cd7f9b6b 100644 --- a/examples/LayoutSpecExamples/Sample/OverviewViewController.m +++ b/examples/LayoutSpecExamples/Sample/OverviewViewController.m @@ -1,11 +1,18 @@ // // OverviewViewController.m -// Sample +// Texture // // Copyright (c) 2014-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. +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. 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 "OverviewViewController.h" @@ -37,7 +44,10 @@ _layoutExamples = @[[HeaderWithRightAndLeftItems class], [PhotoWithInsetTextOverlay class], [PhotoWithOutsetIconOverlay class], - [FlexibleSeparatorSurroundingContent class]]; + [FlexibleSeparatorSurroundingContent class], + [CornerLayoutExample class], + [UserProfileSample class] + ]; } return self; diff --git a/examples/LayoutSpecExamples/Sample/Utilities.h b/examples/LayoutSpecExamples/Sample/Utilities.h index 0fcb0ece91..b4bf2f824a 100644 --- a/examples/LayoutSpecExamples/Sample/Utilities.h +++ b/examples/LayoutSpecExamples/Sample/Utilities.h @@ -1,11 +1,18 @@ // // Utilities.h -// Sample +// Texture // // Copyright (c) 2014-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. +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. 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 @@ -18,6 +25,7 @@ @interface UIImage (Additions) - (UIImage *)makeCircularImageWithSize:(CGSize)size withBorderWidth:(CGFloat)width; ++ (UIImage *)imageWithSize:(CGSize)size fillColor:(UIColor *)fillColor shapeBlock:(UIBezierPath *(^)(void))shapeBlock; @end @interface NSAttributedString (Additions) diff --git a/examples/LayoutSpecExamples/Sample/Utilities.m b/examples/LayoutSpecExamples/Sample/Utilities.m index 74b4ae87a6..92e5c4bda7 100644 --- a/examples/LayoutSpecExamples/Sample/Utilities.m +++ b/examples/LayoutSpecExamples/Sample/Utilities.m @@ -1,11 +1,18 @@ // // Utilities.m -// Sample +// Texture // // Copyright (c) 2014-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. +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. 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 "Utilities.h" @@ -64,6 +71,21 @@ return roundedImage; } ++ (UIImage *)imageWithSize:(CGSize)size fillColor:(UIColor *)fillColor shapeBlock:(UIBezierPath *(^)(void))shapeBlock +{ + UIGraphicsBeginImageContext(size); + [fillColor setFill]; + + UIBezierPath *path = shapeBlock(); + [path addClip]; + [path fill]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + @end @implementation NSAttributedString (Additions) diff --git a/examples/PagerNode/Sample/PageNode.m b/examples/PagerNode/Sample/PageNode.m index bda108a1bf..0efd831219 100644 --- a/examples/PagerNode/Sample/PageNode.m +++ b/examples/PagerNode/Sample/PageNode.m @@ -1,20 +1,18 @@ // // PageNode.m -// Sample -// -// Created by McCallum, Levi on 12/7/15. +// Texture // // Copyright (c) 2014-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. +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. 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 "PageNode.h" @@ -26,10 +24,10 @@ return constrainedSize; } -- (void)fetchData +- (void)didEnterPreloadState { - [super fetchData]; - NSLog(@"Fetching data for node: %@", self); + [super didEnterPreloadState]; + NSLog(@"didEnterPreloadState for node: %@", self); } @end diff --git a/examples/README.md b/examples/README.md index e56667c0ce..76c71a75d0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -203,6 +203,20 @@ Featuring: - ASTableView - ASCellNode +### LayoutSpecExamples [ObjC] + +![Layout Spec Example App Screenshot](https://github.com/AsyncDisplayKit/Documentation/raw/master/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png) + +Featuring: +- ASStackLayoutSpec +- ASInsetLayoutSpec +- ASOverlayLayoutSpec +- ASAbsoluteLayoutSpec +- ASBackgroundLayoutSpec +- ASCornerLayoutSpec + +There is an associated swift version app: LayoutSpecExamples-Swift with same logic implementation. + ## License This file provided by Facebook is for non-commercial testing and evaluation @@ -214,3 +228,5 @@ Featuring: FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + diff --git a/examples/Swift/Sample.xcodeproj/project.pbxproj b/examples/Swift/Sample.xcodeproj/project.pbxproj index 081a5a2d6a..a377337a3a 100644 --- a/examples/Swift/Sample.xcodeproj/project.pbxproj +++ b/examples/Swift/Sample.xcodeproj/project.pbxproj @@ -133,7 +133,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 0910; ORGANIZATIONNAME = Facebook; TargetAttributes = { 050E7C6D19D22E19004363C2 = { @@ -180,9 +180,18 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -210,13 +219,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -243,13 +255,21 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -258,6 +278,7 @@ ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -286,13 +307,21 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -300,6 +329,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -309,6 +339,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index f7f575e824..05f94265c3 100644 --- a/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme +++ b/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -1,6 +1,6 @@ @@ -45,6 +46,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata b/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/Swift/Sample/AppDelegate.swift b/examples/Swift/Sample/AppDelegate.swift index e560672966..c40f1be4ed 100644 --- a/examples/Swift/Sample/AppDelegate.swift +++ b/examples/Swift/Sample/AppDelegate.swift @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let window = UIWindow(frame: UIScreen.main.bounds) window.backgroundColor = UIColor.white - window.rootViewController = UINavigationController(rootViewController: ViewController()); + window.rootViewController = UINavigationController(rootViewController: ViewController()) window.makeKeyAndVisible() self.window = window return true diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj index 5927a1d960..23ef18b40e 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj @@ -230,7 +230,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 0910; ORGANIZATIONNAME = "Calum Harris"; TargetAttributes = { 3AB33F591E1F94520039F711 = { @@ -293,9 +293,18 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", + "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", + "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -321,13 +330,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ASDKgram-Swift-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -379,7 +391,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -387,7 +401,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -410,7 +428,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -429,7 +447,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -437,7 +457,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -454,7 +478,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/contents.xcworkspacedata b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..102e5c6013 --- /dev/null +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift index e8a0dce3ce..08c1bc3273 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift @@ -42,7 +42,7 @@ class PhotoFeedTableNodeController: ASViewController { node.view.separatorStyle = .none node.dataSource = self node.delegate = self - node.view.leadingScreensForBatching = 2.5 + node.leadingScreensForBatching = 2.5 navigationController?.hidesBarsOnSwipe = true } @@ -64,7 +64,10 @@ class PhotoFeedTableNodeController: ASViewController { }() func fetchNewBatchWithContext(_ context: ASBatchContext?) { - activityIndicator.startAnimating() + DispatchQueue.main.async { + self.activityIndicator.startAnimating() + } + photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in switch connectionStatus { case .connected: diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift index e2196208d2..830d78eb0e 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift @@ -25,12 +25,13 @@ final class WebService { URLSession.shared.dataTask(with: resource.url) { data, response, error in // Check for errors in responses. let result = self.checkForNetworkErrors(data, response, error) - - switch result { - case .success(let data): - completion(resource.parse(data)) - case .failure(let error): - completion(.failure(error)) + DispatchQueue.main.async { + switch result { + case .success(let data): + completion(resource.parse(data)) + case .failure(let error): + completion(.failure(error)) + } } }.resume() } @@ -40,15 +41,16 @@ extension WebService { fileprivate func checkForNetworkErrors(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Result { // Check for errors in responses. - guard error == nil else { - if (error as! NSError).domain == NSURLErrorDomain && ((error as! NSError).code == NSURLErrorNotConnectedToInternet || (error as! NSError).code == NSURLErrorTimedOut) { + if let error = error { + let nsError = error as NSError + if nsError.domain == NSURLErrorDomain && (nsError.code == NSURLErrorNotConnectedToInternet || nsError.code == NSURLErrorTimedOut) { return .failure(.noInternetConnection) } else { - return .failure(.returnedError(error!)) + return .failure(.returnedError(error)) } } - guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else { + if let response = response as? HTTPURLResponse, response.statusCode <= 200 && response.statusCode >= 299 { return .failure((.invalidStatusCode("Request returned status code other than 2xx \(response)"))) } diff --git a/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj b/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj index 2dac7da9fe..9439f16227 100644 --- a/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj +++ b/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj @@ -171,7 +171,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 0910; ORGANIZATIONNAME = "Marvin Nazari"; TargetAttributes = { 427F7FC51E58519300D3E11B = { @@ -217,9 +217,14 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-RepoSearcher/Pods-RepoSearcher-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework", + "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -247,13 +252,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RepoSearcher-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -285,7 +293,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -293,7 +303,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -316,7 +330,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -335,7 +349,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -343,7 +359,11 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -360,7 +380,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/examples_extra/RepoSearcher/RepoSearcher/IGListCollectionContext+ASDK.swift b/examples_extra/RepoSearcher/RepoSearcher/IGListCollectionContext+ASDK.swift index b1c623b651..df923205c8 100644 --- a/examples_extra/RepoSearcher/RepoSearcher/IGListCollectionContext+ASDK.swift +++ b/examples_extra/RepoSearcher/RepoSearcher/IGListCollectionContext+ASDK.swift @@ -10,8 +10,8 @@ import Foundation import IGListKit import AsyncDisplayKit -extension IGListCollectionContext { - func nodeForItem(at index: Int, sectionController: IGListSectionController) -> ASCellNode? { +extension ListCollectionContext { + func nodeForItem(at index: Int, sectionController: ListSectionController) -> ASCellNode? { return (cellForItem(at: index, sectionController: sectionController) as? _ASCollectionViewCell)?.node } } diff --git a/examples_extra/RepoSearcher/RepoSearcher/LabelSectionController.swift b/examples_extra/RepoSearcher/RepoSearcher/LabelSectionController.swift index 040634c934..fe65d2dfa1 100644 --- a/examples_extra/RepoSearcher/RepoSearcher/LabelSectionController.swift +++ b/examples_extra/RepoSearcher/RepoSearcher/LabelSectionController.swift @@ -10,7 +10,7 @@ import Foundation import AsyncDisplayKit import IGListKit -final class LabelSectionController: IGListSectionController, IGListSectionType, ASSectionController { +final class LabelSectionController: ListSectionController, ASSectionController { var object: String? func nodeBlockForItem(at index: Int) -> ASCellNodeBlock { @@ -22,22 +22,22 @@ final class LabelSectionController: IGListSectionController, IGListSectionType, } } - func numberOfItems() -> Int { + override func numberOfItems() -> Int { return 1 } - func didUpdate(to object: Any) { + override func didUpdate(to object: Any) { self.object = String(describing: object) } - func didSelectItem(at index: Int) {} + override func didSelectItem(at index: Int) {} //ASDK Replacement - func sizeForItem(at index: Int) -> CGSize { + override func sizeForItem(at index: Int) -> CGSize { return ASIGListSectionControllerMethods.sizeForItem(at: index) } - func cellForItem(at index: Int) -> UICollectionViewCell { + override func cellForItem(at index: Int) -> UICollectionViewCell { return ASIGListSectionControllerMethods.cellForItem(at: index, sectionController: self) } } diff --git a/examples_extra/RepoSearcher/RepoSearcher/NSObject+IGListDiffable.swift b/examples_extra/RepoSearcher/RepoSearcher/NSObject+IGListDiffable.swift index 176b484300..f6b95a193e 100644 --- a/examples_extra/RepoSearcher/RepoSearcher/NSObject+IGListDiffable.swift +++ b/examples_extra/RepoSearcher/RepoSearcher/NSObject+IGListDiffable.swift @@ -8,11 +8,11 @@ import IGListKit -extension NSObject: IGListDiffable { +extension NSObject: ListDiffable { public func diffIdentifier() -> NSObjectProtocol { return self } - public func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { + public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return isEqual(object) } } diff --git a/examples_extra/RepoSearcher/RepoSearcher/SearchNode.swift b/examples_extra/RepoSearcher/RepoSearcher/SearchNode.swift index 440cc3752e..57bb1eebea 100644 --- a/examples_extra/RepoSearcher/RepoSearcher/SearchNode.swift +++ b/examples_extra/RepoSearcher/RepoSearcher/SearchNode.swift @@ -29,9 +29,11 @@ final class SearchBarNode: ASDisplayNode { init(delegate: UISearchBarDelegate?) { self.delegate = delegate - super.init(viewBlock: { + super.init() + setViewBlock { UISearchBar() - }, didLoad: nil) + } + style.preferredSize = CGSize(width: UIScreen.main.bounds.width, height: 44) } diff --git a/examples_extra/RepoSearcher/RepoSearcher/SearchSectionController.swift b/examples_extra/RepoSearcher/RepoSearcher/SearchSectionController.swift index 75738a0c99..1619b55984 100644 --- a/examples_extra/RepoSearcher/RepoSearcher/SearchSectionController.swift +++ b/examples_extra/RepoSearcher/RepoSearcher/SearchSectionController.swift @@ -13,7 +13,7 @@ protocol SearchSectionControllerDelegate: class { func searchSectionController(_ sectionController: SearchSectionController, didChangeText text: String) } -final class SearchSectionController: IGListSectionController, IGListSectionType, ASSectionController { +final class SearchSectionController: ListSectionController, ASSectionController { weak var delegate: SearchSectionControllerDelegate? @@ -28,25 +28,25 @@ final class SearchSectionController: IGListSectionController, IGListSectionType, } } - func numberOfItems() -> Int { + override func numberOfItems() -> Int { return 1 } - func didUpdate(to object: Any) {} - func didSelectItem(at index: Int) {} + override func didUpdate(to object: Any) {} + override func didSelectItem(at index: Int) {} //ASDK Replacement - func sizeForItem(at index: Int) -> CGSize { + override func sizeForItem(at index: Int) -> CGSize { return ASIGListSectionControllerMethods.sizeForItem(at: index) } - func cellForItem(at index: Int) -> UICollectionViewCell { + override func cellForItem(at index: Int) -> UICollectionViewCell { return ASIGListSectionControllerMethods.cellForItem(at: index, sectionController: self) } } -extension SearchSectionController: IGListScrollDelegate { - func listAdapter(_ listAdapter: IGListAdapter, didScroll sectionController: IGListSectionController) { +extension SearchSectionController: ListScrollDelegate { + func listAdapter(_ listAdapter: ListAdapter, didScroll sectionController: ListSectionController) { guard let searchNode = collectionContext?.nodeForItem(at: 0, sectionController: self) as? SearchNode else { return } let searchBar = searchNode.searchBarNode.searchBar @@ -54,8 +54,8 @@ extension SearchSectionController: IGListScrollDelegate { searchBar.resignFirstResponder() } - func listAdapter(_ listAdapter: IGListAdapter!, willBeginDragging sectionController: IGListSectionController!) {} - func listAdapter(_ listAdapter: IGListAdapter!, didEndDragging sectionController: IGListSectionController!, willDecelerate decelerate: Bool) {} + func listAdapter(_ listAdapter: ListAdapter, willBeginDragging sectionController: ListSectionController) {} + func listAdapter(_ listAdapter: ListAdapter, didEndDragging sectionController: ListSectionController, willDecelerate decelerate: Bool) {} } diff --git a/examples_extra/RepoSearcher/RepoSearcher/SearchViewController.swift b/examples_extra/RepoSearcher/RepoSearcher/SearchViewController.swift index 35f1c39134..dd00d9aa74 100644 --- a/examples_extra/RepoSearcher/RepoSearcher/SearchViewController.swift +++ b/examples_extra/RepoSearcher/RepoSearcher/SearchViewController.swift @@ -14,8 +14,8 @@ class SearchToken: NSObject {} final class SearchViewController: ASViewController { - lazy var adapter: IGListAdapter = { - return IGListAdapter(updater: IGListAdapterUpdater(), viewController: self, workingRangeSize: 0) + lazy var adapter: ListAdapter = { + return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0) }() let words = ["first", "second", "third", "more", "hi", "others"] @@ -36,8 +36,8 @@ final class SearchViewController: ASViewController { } } -extension SearchViewController: IGListAdapterDataSource { - func listAdapter(_ listAdapter: IGListAdapter, sectionControllerFor object: Any) -> IGListSectionController { +extension SearchViewController: ListAdapterDataSource { + func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { if object is SearchToken { let section = SearchSectionController() section.delegate = self @@ -46,14 +46,14 @@ extension SearchViewController: IGListAdapterDataSource { return LabelSectionController() } - func emptyView(for listAdapter: IGListAdapter) -> UIView? { + func emptyView(for listAdapter: ListAdapter) -> UIView? { // emptyView dosent work in this secenario, there is always one section (searchbar) present in collection return nil } - func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] { - guard filterString != "" else { return [searchToken] + words.map { $0 as IGListDiffable } } - return [searchToken] + words.filter { $0.lowercased().contains(filterString.lowercased()) }.map { $0 as IGListDiffable } + func objects(for listAdapter: ListAdapter) -> [ListDiffable] { + guard filterString != "" else { return [searchToken] + words.map { $0 as ListDiffable } } + return [searchToken] + words.filter { $0.lowercased().contains(filterString.lowercased()) }.map { $0 as ListDiffable } } } diff --git a/examples_extra/Shop/Shop.xcodeproj/project.pbxproj b/examples_extra/Shop/Shop.xcodeproj/project.pbxproj index cdbdc81693..836c0e7716 100644 --- a/examples_extra/Shop/Shop.xcodeproj/project.pbxproj +++ b/examples_extra/Shop/Shop.xcodeproj/project.pbxproj @@ -268,7 +268,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0810; - LastUpgradeCheck = 0810; + LastUpgradeCheck = 0910; ORGANIZATIONNAME = Dimitri; TargetAttributes = { 278BFA1D1DD4A7B80065BACA = { @@ -330,13 +330,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ShopTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 4E2E9451B168505B69D5EA0F /* [CP] Embed Pods Frameworks */ = { @@ -360,13 +363,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Shop-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 7A9094E163FF7B834F7D5B76 /* [CP] Embed Pods Frameworks */ = { @@ -479,7 +485,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -487,7 +495,12 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -510,7 +523,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.1; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -528,7 +541,9 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -536,7 +551,12 @@ CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -553,7 +573,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.1; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/examples_extra/Shop/Shop/Scenes/Product/ProductNode.swift b/examples_extra/Shop/Shop/Scenes/Product/ProductNode.swift index 97bcec3aec..0efaf45d3b 100644 --- a/examples_extra/Shop/Shop/Scenes/Product/ProductNode.swift +++ b/examples_extra/Shop/Shop/Scenes/Product/ProductNode.swift @@ -50,7 +50,7 @@ class ProductNode: ASDisplayNode { private func setupImageNode() { self.imageNode.url = URL(string: self.product.imageURL) - self.imageNode.preferredFrameSize = CGSize(width: UIScreen.main.bounds.width, height: 300) + self.imageNode.style.preferredSize = CGSize(width: UIScreen.main.bounds.width, height: 300) } private func setupTitleNode() { @@ -103,13 +103,13 @@ class ProductNode: ASDisplayNode { override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let spacer = ASLayoutSpec() - spacer.flexGrow = true - self.titleNode.flexShrink = true + spacer.style.flexGrow = 1 + self.titleNode.style.flexShrink = 1 let titlePriceSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 2.0, justifyContent: .start, alignItems: .center, children: [self.titleNode, spacer, self.priceNode]) - titlePriceSpec.alignSelf = .stretch + titlePriceSpec.style.alignSelf = .stretch let starRatingReviewsSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 25.0, justifyContent: .start, alignItems: .center, children: [self.starRatingNode, self.reviewsNode]) let contentSpec = ASStackLayoutSpec(direction: .vertical, spacing: 8.0, justifyContent: .start, alignItems: .stretch, children: [titlePriceSpec, starRatingReviewsSpec, self.descriptionNode]) - contentSpec.flexShrink = true + contentSpec.style.flexShrink = 1 let insetSpec = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(12.0, 12.0, 12.0, 12.0), child: contentSpec) let finalSpec = ASStackLayoutSpec(direction: .vertical, spacing: 5.0, justifyContent: .start, alignItems: .center, children: [self.imageNode, insetSpec]) return finalSpec diff --git a/examples_extra/Shop/Shop/Scenes/Product/StarRatingNode.swift b/examples_extra/Shop/Shop/Scenes/Product/StarRatingNode.swift index 8556251721..83a37564ff 100644 --- a/examples_extra/Shop/Shop/Scenes/Product/StarRatingNode.swift +++ b/examples_extra/Shop/Shop/Scenes/Product/StarRatingNode.swift @@ -36,7 +36,7 @@ class StarRatingNode: ASDisplayNode { for i in 0..<5 { let imageNode = ASImageNode() imageNode.image = i <= self.rating ? UIImage(named: "filled_star") : UIImage(named: "unfilled_star") - imageNode.preferredFrameSize = self.starSize + imageNode.style.preferredSize = self.starSize self.starImageNodes.append(imageNode) } } diff --git a/examples_extra/Shop/Shop/Scenes/Products/ProductTableNode.swift b/examples_extra/Shop/Shop/Scenes/Products/ProductTableNode.swift index 45c36e656e..7cb0a2b9f8 100644 --- a/examples_extra/Shop/Shop/Scenes/Products/ProductTableNode.swift +++ b/examples_extra/Shop/Shop/Scenes/Products/ProductTableNode.swift @@ -54,7 +54,7 @@ class ProductTableNode: ASCellNode { private func setupImageNode() { self.imageNode.url = URL(string: self.product.imageURL) - self.imageNode.preferredFrameSize = self.imageSize + self.imageNode.style.preferredSize = self.imageSize } private func setupTitleNode() { @@ -110,12 +110,12 @@ class ProductTableNode: ASCellNode { override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { let spacer = ASLayoutSpec() - spacer.flexGrow = true - self.titleNode.flexShrink = true + spacer.style.flexGrow = 1 + self.titleNode.style.flexShrink = 1 let titlePriceSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 2.0, justifyContent: .start, alignItems: .center, children: [self.titleNode, spacer, self.priceNode]) - titlePriceSpec.alignSelf = .stretch + titlePriceSpec.style.alignSelf = .stretch let contentSpec = ASStackLayoutSpec(direction: .vertical, spacing: 4.0, justifyContent: .start, alignItems: .stretch, children: [titlePriceSpec, self.subtitleNode, self.starRatingNode]) - contentSpec.flexShrink = true + contentSpec.style.flexShrink = 1 let finalSpec = ASStackLayoutSpec(direction: .horizontal, spacing: 10.0, justifyContent: .start, alignItems: .start, children: [self.imageNode, contentSpec]) return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0), child: finalSpec) } diff --git a/examples_extra/Shop/Shop/Scenes/Products/ProductsTableViewController.swift b/examples_extra/Shop/Shop/Scenes/Products/ProductsTableViewController.swift index af716bf62c..ab33726d96 100644 --- a/examples_extra/Shop/Shop/Scenes/Products/ProductsTableViewController.swift +++ b/examples_extra/Shop/Shop/Scenes/Products/ProductsTableViewController.swift @@ -42,7 +42,7 @@ class ProductsTableViewController: ASViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let indexPath = self.tableNode.view.indexPathForSelectedRow { + if let indexPath = self.tableNode.indexPathForSelectedRow { self.tableNode.view.deselectRow(at: indexPath, animated: true) } } diff --git a/examples_extra/Shop/Shop/Scenes/Shop/ShopViewController.swift b/examples_extra/Shop/Shop/Scenes/Shop/ShopViewController.swift index ad531709ef..c32ce3f0a4 100644 --- a/examples_extra/Shop/Shop/Scenes/Shop/ShopViewController.swift +++ b/examples_extra/Shop/Shop/Scenes/Shop/ShopViewController.swift @@ -77,7 +77,7 @@ extension ShopViewController: ASTableDataSource, ASTableDelegate { func tableView(_ tableView: ASTableView, constrainedSizeForRowAt indexPath: IndexPath) -> ASSizeRange { let width = UIScreen.main.bounds.width - return ASSizeRangeMakeExactSize(CGSize(width: width, height: 175)) + return ASSizeRangeMake(CGSize(width: width, height: 175)) } } diff --git a/examples_extra/TextStressTest/Podfile b/examples_extra/TextStressTest/Podfile index 922ff50ec1..6670022698 100644 --- a/examples_extra/TextStressTest/Podfile +++ b/examples_extra/TextStressTest/Podfile @@ -1,6 +1,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' target 'Sample' do - pod 'Texture', :path => '../..' + pod 'Texture/Yoga', :path => '../..' end diff --git a/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj b/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj index 3453463d27..86a3f35f70 100644 --- a/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/TextStressTest/Sample.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; 92F1263CECFE3FFCC7A5F936 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8EC8300ABAAEA079224272A /* libPods-Sample.a */; }; + C081EE8D1F85AEEC00F0B5F1 /* TabBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */; }; + C081EE921F85AFB800F0B5F1 /* CollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */; }; + C081EE931F85AFB800F0B5F1 /* TextCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = C081EE911F85AFB800F0B5F1 /* TextCellNode.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -29,6 +32,12 @@ 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; A950870A2154F92D5DC91F1A /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; + C081EE8B1F85AEEC00F0B5F1 /* TabBarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TabBarController.h; sourceTree = ""; }; + C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TabBarController.m; sourceTree = ""; }; + C081EE8E1F85AFB800F0B5F1 /* CollectionViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CollectionViewController.h; sourceTree = ""; }; + C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CollectionViewController.m; sourceTree = ""; }; + C081EE901F85AFB800F0B5F1 /* TextCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextCellNode.h; sourceTree = ""; }; + C081EE911F85AFB800F0B5F1 /* TextCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextCellNode.m; sourceTree = ""; }; E8EC8300ABAAEA079224272A /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -68,11 +77,17 @@ 05E2128319D4DB510098F589 /* Sample */ = { isa = PBXGroup; children = ( + C081EE8E1F85AFB800F0B5F1 /* CollectionViewController.h */, + C081EE8F1F85AFB800F0B5F1 /* CollectionViewController.m */, + C081EE901F85AFB800F0B5F1 /* TextCellNode.h */, + C081EE911F85AFB800F0B5F1 /* TextCellNode.m */, 05E2128819D4DB510098F589 /* AppDelegate.h */, 05E2128919D4DB510098F589 /* AppDelegate.m */, 05E2128B19D4DB510098F589 /* ViewController.h */, 05E2128C19D4DB510098F589 /* ViewController.m */, 05E2128419D4DB510098F589 /* Supporting Files */, + C081EE8B1F85AEEC00F0B5F1 /* TabBarController.h */, + C081EE8C1F85AEEC00F0B5F1 /* TabBarController.m */, ); path = Sample; sourceTree = ""; @@ -181,13 +196,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Sample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 96436DA0C1AFF84D8041B522 /* [CP] Embed Pods Frameworks */ = { @@ -227,8 +245,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C081EE931F85AFB800F0B5F1 /* TextCellNode.m in Sources */, 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + C081EE8D1F85AEEC00F0B5F1 /* TabBarController.m in Sources */, 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + C081EE921F85AFB800F0B5F1 /* CollectionViewController.m in Sources */, 05E2128719D4DB510098F589 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/examples_extra/TextStressTest/Sample/AppDelegate.m b/examples_extra/TextStressTest/Sample/AppDelegate.m index a8e5594780..5be641c029 100644 --- a/examples_extra/TextStressTest/Sample/AppDelegate.m +++ b/examples_extra/TextStressTest/Sample/AppDelegate.m @@ -1,25 +1,39 @@ -/* This file provided by Facebook is for non-commercial testing and evaluation - * purposes only. Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ +// +// AppDelegate.m +// Texture +// +// Copyright (c) 2017-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 "AppDelegate.h" +#import "TabBarController.h" +#import "CollectionViewController.h" #import "ViewController.h" + @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; - self.window.rootViewController = [[ViewController alloc] init]; + + ViewController *viewController = [[ViewController alloc] init]; + viewController.tabBarItem.title = @"TextStress"; + + CollectionViewController *cvc = [[CollectionViewController alloc] init]; + cvc.tabBarItem.title = @"Flexbox"; + + TabBarController *tabBarController = [[TabBarController alloc] init]; + tabBarController.viewControllers = @[cvc, viewController]; + + self.window.rootViewController = tabBarController; [self.window makeKeyAndVisible]; return YES; } diff --git a/examples_extra/TextStressTest/Sample/CollectionViewController.h b/examples_extra/TextStressTest/Sample/CollectionViewController.h new file mode 100644 index 0000000000..159b2fa1c9 --- /dev/null +++ b/examples_extra/TextStressTest/Sample/CollectionViewController.h @@ -0,0 +1,17 @@ +// +// CollectionViewController.h +// Texture +// +// Copyright (c) 2017-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 CollectionViewController : ASViewController +@end diff --git a/examples_extra/TextStressTest/Sample/CollectionViewController.m b/examples_extra/TextStressTest/Sample/CollectionViewController.m new file mode 100644 index 0000000000..d93005236b --- /dev/null +++ b/examples_extra/TextStressTest/Sample/CollectionViewController.m @@ -0,0 +1,67 @@ +// +// CollectionViewController.m +// Texture +// +// Copyright (c) 2017-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 "CollectionViewController.h" +#import "TextCellNode.h" + +@interface CollectionViewController() +{ + ASCollectionNode *_collectionNode; + NSArray *_labels; + TextCellNode *_cellNode; +} + +@end + +@implementation CollectionViewController + +- (instancetype)init +{ + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:flowLayout]; + CGRect rect = [[UIApplication sharedApplication] statusBarFrame]; + _collectionNode.contentInset = UIEdgeInsetsMake(rect.size.height, 0, 0, 0); + self = [super initWithNode:_collectionNode]; + if (self) { + _collectionNode.delegate = self; + _collectionNode.dataSource = self; + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + _collectionNode.backgroundColor = [UIColor whiteColor]; + _labels = @[@"Fight of the Living Dead: Experiment Fight of the Living Dead: Experiment", @"S1 • E1"]; +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return 1; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return ^{ + _cellNode = [[TextCellNode alloc] initWithText1:_labels[0] text2:_labels[1]]; + return _cellNode; + }; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGFloat width = collectionNode.view.bounds.size.width; + return ASSizeRangeMake(CGSizeMake(width, 0.0f), CGSizeMake(width, CGFLOAT_MAX)); +} + +@end diff --git a/examples_extra/TextStressTest/Sample/TabBarController.h b/examples_extra/TextStressTest/Sample/TabBarController.h new file mode 100644 index 0000000000..5a25bb04ac --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TabBarController.h @@ -0,0 +1,16 @@ +// +// TabBarController.h +// Texture +// +// Copyright (c) 2017-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 + +@interface TabBarController : ASTabBarController +@end diff --git a/examples_extra/TextStressTest/Sample/TabBarController.m b/examples_extra/TextStressTest/Sample/TabBarController.m new file mode 100644 index 0000000000..a7905cb235 --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TabBarController.m @@ -0,0 +1,19 @@ +// +// TabBarController.m +// Texture +// +// Copyright (c) 2017-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 "TabBarController.h" + +@interface TabBarController () +@end + +@implementation TabBarController +@end diff --git a/examples_extra/TextStressTest/Sample/TextCellNode.h b/examples_extra/TextStressTest/Sample/TextCellNode.h new file mode 100644 index 0000000000..c4b35cfaf4 --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TextCellNode.h @@ -0,0 +1,17 @@ +// +// TextCellNode.h +// Texture +// +// Copyright (c) 2017-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 + +@interface TextCellNode : ASCellNode +- (instancetype)initWithText1:(NSString *)text1 text2:(NSString *)text2; +@end diff --git a/examples_extra/TextStressTest/Sample/TextCellNode.m b/examples_extra/TextStressTest/Sample/TextCellNode.m new file mode 100644 index 0000000000..13bb7694cb --- /dev/null +++ b/examples_extra/TextStressTest/Sample/TextCellNode.m @@ -0,0 +1,100 @@ +// +// TextCellNode.m +// Texture +// +// Copyright (c) 2017-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 "TextCellNode.h" +#import +#import + +#ifndef USE_ASTEXTNODE_2 +#define USE_ASTEXTNODE_2 1 +#endif + +@interface TextCellNode() +{ +#if USE_ASTEXTNODE_2 + ASTextNode2 *_label1; + ASTextNode2 *_label2; +#else + ASTextNode *_label1; + ASTextNode *_label2; +#endif +} +@end + +@implementation TextCellNode + +- (instancetype)initWithText1:(NSString *)text1 text2:(NSString *)text2 +{ + self = [super init]; + if (self) { + self.automaticallyManagesSubnodes = YES; + self.clipsToBounds = YES; +#if USE_ASTEXTNODE_2 + _label1 = [[ASTextNode2 alloc] init]; + _label2 = [[ASTextNode2 alloc] init]; +#else + _label1 = [[ASTextNode alloc] init]; + _label2 = [[ASTextNode alloc] init]; +#endif + + _label1.attributedText = [[NSAttributedString alloc] initWithString:text1]; + _label2.attributedText = [[NSAttributedString alloc] initWithString:text2]; + + _label1.maximumNumberOfLines = 1; + _label1.truncationMode = NSLineBreakByTruncatingTail; + _label2.maximumNumberOfLines = 1; + _label2.truncationMode = NSLineBreakByTruncatingTail; + + [self simpleSetupYogaLayout]; + } + return self; +} + +/** + This is to text a row with two labels, the first should be truncated with "...". + Layout is like: [l1Container[_label1], label2]. + This shows a bug of ASTextNode2. + */ +- (void)simpleSetupYogaLayout +{ + [self.style yogaNodeCreateIfNeeded]; + [_label1.style yogaNodeCreateIfNeeded]; + [_label2.style yogaNodeCreateIfNeeded]; + + _label1.style.flexGrow = 0; + _label1.style.flexShrink = 1; + _label1.backgroundColor = [UIColor lightGrayColor]; + + _label2.style.flexGrow = 0; + _label2.style.flexShrink = 0; + _label2.backgroundColor = [UIColor greenColor]; + + ASDisplayNode *l1Container = [ASDisplayNode yogaVerticalStack]; + + // TODO(fix ASTextNode2): next two line will show the bug of TextNode2 + // which works for ASTextNode though + // see discussion here: https://github.com/TextureGroup/Texture/pull/553 + l1Container.style.alignItems = ASStackLayoutAlignItemsCenter; + _label1.style.alignSelf = ASStackLayoutAlignSelfStart; + + l1Container.style.flexGrow = 0; + l1Container.style.flexShrink = 1; + + l1Container.yogaChildren = @[_label1]; + + self.style.justifyContent = ASStackLayoutJustifyContentSpaceBetween; + self.style.alignItems = ASStackLayoutAlignItemsStart; + self.style.flexDirection = ASStackLayoutDirectionHorizontal; + self.yogaChildren = @[l1Container, _label2]; +} + +@end diff --git a/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj b/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj index f6094669cc..b4eeb8cc2c 100644 --- a/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj +++ b/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7319D22E19004363C2 /* AppDelegate.swift */; }; 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7519D22E19004363C2 /* ViewController.swift */; }; 05DDD8DB19D2336300013C30 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */; }; - 34566CAA1BC1204100715E6B /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */; }; - 34566CAB1BC1204100715E6B /* AsyncDisplayKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6C5053DB19EE266A00E385DE /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053D919EE266A00E385DE /* Default-667h@2x.png */; }; 6C5053DC19EE266A00E385DE /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */; }; /* End PBXBuildFile section */ @@ -38,13 +36,6 @@ remoteGlobalIDString = 057D02BF1AC0A66700C7AC3C; remoteInfo = AsyncDisplayKitTestHost; }; - 34566CA81BC1202A00715E6B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = B35061DA1B010EDF0018CF92; - remoteInfo = "AsyncDisplayKit-iOS"; - }; 34566CAC1BC1204100715E6B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 34566C9B1BC1202A00715E6B /* AsyncDisplayKit.xcodeproj */; @@ -61,7 +52,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 34566CAB1BC1204100715E6B /* AsyncDisplayKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -86,7 +76,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 34566CAA1BC1204100715E6B /* AsyncDisplayKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -150,7 +139,6 @@ 34566CA31BC1202A00715E6B /* libAsyncDisplayKit.a */, 34566CA51BC1202A00715E6B /* AsyncDisplayKitTests.xctest */, 34566CA71BC1202A00715E6B /* AsyncDisplayKitTestHost.app */, - 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */, ); name = Products; sourceTree = ""; @@ -189,7 +177,7 @@ TargetAttributes = { 050E7C6D19D22E19004363C2 = { CreatedOnToolsVersion = 6.0.1; - LastSwiftMigration = 0800; + LastSwiftMigration = 0830; }; }; }; @@ -220,8 +208,8 @@ /* Begin PBXReferenceProxy section */ 34566CA31BC1202A00715E6B /* libAsyncDisplayKit.a */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libAsyncDisplayKit.a; + fileType = wrapper.framework; + path = AsyncDisplayKit.framework; remoteRef = 34566CA21BC1202A00715E6B /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -239,13 +227,6 @@ remoteRef = 34566CA61BC1202A00715E6B /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 34566CA91BC1202A00715E6B /* AsyncDisplayKit.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = AsyncDisplayKit.framework; - remoteRef = 34566CA81BC1202A00715E6B /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -369,7 +350,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -382,7 +363,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/smoke-tests/Framework/Sample/AppDelegate.swift b/smoke-tests/Framework/Sample/AppDelegate.swift index 5c30133002..843523a436 100644 --- a/smoke-tests/Framework/Sample/AppDelegate.swift +++ b/smoke-tests/Framework/Sample/AppDelegate.swift @@ -22,9 +22,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - let window = UIWindow(frame: UIScreen.mainScreen().bounds) - window.backgroundColor = UIColor.whiteColor() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + let window = UIWindow(frame: UIScreen.main.bounds) + window.backgroundColor = UIColor.white window.rootViewController = ViewController(nibName: nil, bundle: nil) window.makeKeyAndVisible() self.window = window diff --git a/smoke-tests/Framework/Sample/ViewController.swift b/smoke-tests/Framework/Sample/ViewController.swift index 24fa79de75..544850d42d 100644 --- a/smoke-tests/Framework/Sample/ViewController.swift +++ b/smoke-tests/Framework/Sample/ViewController.swift @@ -25,7 +25,7 @@ class ViewController: UIViewController, ASTableDataSource, ASTableDelegate { // MARK: UIViewController. - override required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + override required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { self.tableNode = ASTableNode() super.init(nibName: nil, bundle: nil) @@ -50,7 +50,7 @@ class ViewController: UIViewController, ASTableDataSource, ASTableDelegate { // MARK: ASTableView data source and delegate. - func tableNode(tableNode: ASTableNode, nodeForRowAtIndexPath indexPath: NSIndexPath) -> ASCellNode { + func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { let patter = NSString(format: "[%ld.%ld] says hello!", indexPath.section, indexPath.row) let node = ASTextCellNode() node.text = patter as String @@ -58,11 +58,11 @@ class ViewController: UIViewController, ASTableDataSource, ASTableDelegate { return node } - func numberOfSectionsInTableNode(tableNode: ASTableNode) -> Int { + func numberOfSections(in tableNode: ASTableNode) -> Int { return 1 } - func tableNode(tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { + func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { return 20 }