diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 068938a4d..c9f5cba82 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 */; }; + 4496D0731FA9EA6B001CC8D5 /* ASTraitCollectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4496D0721FA9EA6B001CC8D5 /* ASTraitCollectionTests.m */; }; 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 */; }; @@ -130,9 +134,7 @@ 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, ); }; }; 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 */; }; - 690ED5991E36D118000627C0 /* ASImageNode+tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 690ED5941E36D118000627C0 /* ASImageNode+tvOS.h */; settings = {ATTRIBUTES = (Private, ); }; }; 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.m in Sources */ = {isa = PBXBuildFile; fileRef = 690ED5951E36D118000627C0 /* ASImageNode+tvOS.m */; }; 692510141E74FB44003F2DD0 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 692510131E74FB44003F2DD0 /* Default-568h@2x.png */; }; 692BE8D71E36B65B00C86D87 /* ASLayoutSpecPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 692BE8D61E36B65B00C86D87 /* ASLayoutSpecPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -262,7 +264,7 @@ B35062131B010EFD0018CF92 /* ASBasicImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */; settings = {ATTRIBUTES = (Public, ); }; }; B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */ = {isa = PBXBuildFile; fileRef = 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */; }; B35062151B010EFD0018CF92 /* ASBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 299DA1A71A828D2900162D41 /* ASBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.mm */; }; + B35062161B010EFD0018CF92 /* ASBatchContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.m */; }; B35062171B010EFD0018CF92 /* ASDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 464052191A3F83C40061C0BA /* ASDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521A1A3F83C40061C0BA /* ASDataController.mm */; }; B350621B1B010EFD0018CF92 /* ASTableLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -307,10 +309,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 */; }; @@ -324,6 +327,8 @@ CC0F886C1E4286FA00576FED /* ReferenceImages_64 in Resources */ = {isa = PBXBuildFile; fileRef = CC0F88691E4286FA00576FED /* ReferenceImages_64 */; }; CC0F886D1E4286FA00576FED /* ReferenceImages_iOS_10 in Resources */ = {isa = PBXBuildFile; fileRef = CC0F886A1E4286FA00576FED /* ReferenceImages_iOS_10 */; }; CC11F97A1DB181180024D77B /* ASNetworkImageNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */; }; + CC18248C200D49C800875940 /* ASTextNodeCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = CC18248B200D49C800875940 /* ASTextNodeCommon.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC224E962066CA6D00BBA57F /* configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = CC224E952066CA6D00BBA57F /* configuration.json */; }; CC2F65EE1E5FFB1600DA57C9 /* ASMutableElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */; }; CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */; }; CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20811C3F76D600798563 /* ASPendingStateController.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -352,8 +357,12 @@ CC58AA4B1E398E1D002C8CB4 /* ASBlockTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC6AA2DA1E9F03B900978E87 /* ASDisplayNode+Ancestry.h in Headers */ = {isa = PBXBuildFile; fileRef = CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC6AA2DB1E9F03B900978E87 /* ASDisplayNode+Ancestry.m in Sources */ = {isa = PBXBuildFile; fileRef = CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.m */; }; + CC7AF196200D9BD500A21BDE /* ASExperimentalFeatures.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7AF195200D9BD500A21BDE /* ASExperimentalFeatures.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC7AF198200DAB2200A21BDE /* ASExperimentalFeatures.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7AF197200D9E8400A21BDE /* ASExperimentalFeatures.m */; }; CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; }; CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC84C7F220474C5300A3851B /* ASCGImageBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = CC84C7F020474C5300A3851B /* ASCGImageBuffer.h */; }; + CC84C7F320474C5300A3851B /* ASCGImageBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = CC84C7F120474C5300A3851B /* ASCGImageBuffer.m */; }; CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */; settings = {ATTRIBUTES = (Private, ); }; }; CC8B05D61D73836400F54286 /* ASPerformanceTestContext.m in Sources */ = {isa = PBXBuildFile; fileRef = CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */; }; CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */; }; @@ -376,6 +385,9 @@ CCA282D01E9EBF6C0037E8B7 /* ASTipsWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */; }; CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */; }; CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62D1EECC2A80060C137 /* ASAssert.m */; }; + CCAA0B7F206ADBF30057B336 /* ASRecursiveUnfairLock.h in Headers */ = {isa = PBXBuildFile; fileRef = CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCAA0B80206ADBF30057B336 /* ASRecursiveUnfairLock.m in Sources */ = {isa = PBXBuildFile; fileRef = CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.m */; }; + CCAA0B82206ADECB0057B336 /* ASRecursiveUnfairLockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCAA0B81206ADECB0057B336 /* ASRecursiveUnfairLockTests.m */; }; CCB1F95A1EFB60A5009C7475 /* ASLog.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB1F9591EFB60A5009C7475 /* ASLog.m */; }; CCB1F95C1EFB6350009C7475 /* ASSignpost.h in Headers */ = {isa = PBXBuildFile; fileRef = CCB1F95B1EFB6316009C7475 /* ASSignpost.h */; }; CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */; }; @@ -398,8 +410,22 @@ CCCCCCE41EC3EF060087FE10 /* NSParagraphStyle+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.m */; }; CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */; }; CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */; }; + CCDC9B4D200991D10063C1F8 /* ASGraphicsContext.h in Headers */ = {isa = PBXBuildFile; fileRef = CCDC9B4B200991D10063C1F8 /* ASGraphicsContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCDC9B4E200991D10063C1F8 /* ASGraphicsContext.m in Sources */ = {isa = PBXBuildFile; fileRef = CCDC9B4C200991D10063C1F8 /* ASGraphicsContext.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 */; }; + CCED5E3E2020D36800395C40 /* ASNetworkImageLoadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = CCED5E3C2020D36800395C40 /* ASNetworkImageLoadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCED5E3F2020D36800395C40 /* ASNetworkImageLoadInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = CCED5E3D2020D36800395C40 /* ASNetworkImageLoadInfo.m */; }; + CCED5E412020D49D00395C40 /* ASNetworkImageLoadInfo+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = CCED5E402020D41600395C40 /* ASNetworkImageLoadInfo+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CCEDDDCA200C2AC300FFCD0A /* ASConfigurationInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = CCEDDDC8200C2AC300FFCD0A /* ASConfigurationInternal.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCEDDDCB200C2AC300FFCD0A /* ASConfigurationInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDC9200C2AC300FFCD0A /* ASConfigurationInternal.m */; }; + CCEDDDCD200C2CB900FFCD0A /* ASConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = CCEDDDCC200C2CB900FFCD0A /* ASConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCEDDDCF200C42A200FFCD0A /* ASConfigurationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = CCEDDDCE200C42A200FFCD0A /* ASConfigurationDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCEDDDD1200C488000FFCD0A /* ASConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDD0200C488000FFCD0A /* ASConfiguration.m */; }; + CCEDDDD9200C518800FFCD0A /* ASConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.m */; }; 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 */; }; @@ -411,7 +437,6 @@ DBDB83971C6E879900D0098C /* ASPagerFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */; }; DE4843DC1C93EAC100A1F33B /* ASLayoutTransition.h in Headers */ = {isa = PBXBuildFile; fileRef = E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */; settings = {ATTRIBUTES = (Private, ); }; }; DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; - DE7EF4F81DFF77720082B84A /* ASDisplayNode+FrameworkSubclasses.h in Headers */ = {isa = PBXBuildFile; fileRef = DE7EF4F71DFF77720082B84A /* ASDisplayNode+FrameworkSubclasses.h */; settings = {ATTRIBUTES = (Private, ); }; }; DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE84918E1C8FFF9F003D89E9 /* ASRunLoopQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */; }; DE8BEAC21C2DF3FC00D57C12 /* ASDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -423,6 +448,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 */; }; @@ -438,6 +466,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, ); }; }; @@ -445,8 +474,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, ); }; }; @@ -460,6 +487,7 @@ E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */; }; F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */; }; + FA4FAF15200A850200E735BD /* ASControlNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = FA4FAF14200A850200E735BD /* ASControlNode+Private.h */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -570,6 +598,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 = ""; }; @@ -615,11 +646,12 @@ 296A0A311A951715005ACEAA /* ASScrollDirection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASScrollDirection.h; path = Source/Details/ASScrollDirection.h; sourceTree = SOURCE_ROOT; }; 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASBatchFetchingTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 299DA1A71A828D2900162D41 /* ASBatchContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchContext.h; sourceTree = ""; }; - 299DA1A81A828D2900162D41 /* ASBatchContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBatchContext.mm; sourceTree = ""; }; + 299DA1A81A828D2900162D41 /* ASBatchContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBatchContext.m; sourceTree = ""; }; 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASBasicImageDownloaderContextTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionReusableView.h; sourceTree = ""; }; 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _ASCollectionReusableView.m; sourceTree = ""; }; 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTableViewTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 4496D0721FA9EA6B001CC8D5 /* ASTraitCollectionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASTraitCollectionTests.m; sourceTree = ""; }; 464052191A3F83C40061C0BA /* ASDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASDataController.h; sourceTree = ""; }; 4640521A1A3F83C40061C0BA /* ASDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDataController.mm; sourceTree = ""; }; 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableLayoutController.h; sourceTree = ""; }; @@ -651,9 +683,7 @@ 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 = ""; }; 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 = ""; }; - 690ED5941E36D118000627C0 /* ASImageNode+tvOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+tvOS.h"; sourceTree = ""; }; 690ED5951E36D118000627C0 /* ASImageNode+tvOS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASImageNode+tvOS.m"; sourceTree = ""; }; 692510131E74FB44003F2DD0 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 692BE8D61E36B65B00C86D87 /* ASLayoutSpecPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutSpecPrivate.h; sourceTree = ""; }; @@ -783,10 +813,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 = ""; }; @@ -798,6 +829,8 @@ CC0F88691E4286FA00576FED /* ReferenceImages_64 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ReferenceImages_64; sourceTree = ""; }; CC0F886A1E4286FA00576FED /* ReferenceImages_iOS_10 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ReferenceImages_iOS_10; sourceTree = ""; }; CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASNetworkImageNodeTests.m; sourceTree = ""; }; + CC18248B200D49C800875940 /* ASTextNodeCommon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASTextNodeCommon.h; sourceTree = ""; }; + CC224E952066CA6D00BBA57F /* configuration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = configuration.json; sourceTree = ""; }; CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionView+Undeprecated.h"; sourceTree = ""; }; CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMutableElementMap.h; sourceTree = ""; }; CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMutableElementMap.m; sourceTree = ""; }; @@ -833,9 +866,13 @@ CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBlockTypes.h; sourceTree = ""; }; CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ASDisplayNode+Ancestry.h"; path = "Base/ASDisplayNode+Ancestry.h"; sourceTree = ""; }; CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "ASDisplayNode+Ancestry.m"; path = "Base/ASDisplayNode+Ancestry.m"; sourceTree = ""; }; + CC7AF195200D9BD500A21BDE /* ASExperimentalFeatures.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASExperimentalFeatures.h; sourceTree = ""; }; + CC7AF197200D9E8400A21BDE /* ASExperimentalFeatures.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASExperimentalFeatures.m; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = ""; }; CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = ""; }; + CC84C7F020474C5300A3851B /* ASCGImageBuffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASCGImageBuffer.h; sourceTree = ""; }; + CC84C7F120474C5300A3851B /* ASCGImageBuffer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASCGImageBuffer.m; sourceTree = ""; }; CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCellNode+Internal.h"; sourceTree = ""; }; CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPerformanceTestContext.h; sourceTree = ""; }; CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPerformanceTestContext.m; sourceTree = ""; }; @@ -858,6 +895,9 @@ CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipsWindow.h; sourceTree = ""; }; CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTipsWindow.m; sourceTree = ""; }; CCA5F62D1EECC2A80060C137 /* ASAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASAssert.m; sourceTree = ""; }; + CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRecursiveUnfairLock.h; sourceTree = ""; }; + CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASRecursiveUnfairLock.m; sourceTree = ""; }; + CCAA0B81206ADECB0057B336 /* ASRecursiveUnfairLockTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASRecursiveUnfairLockTests.m; sourceTree = ""; }; CCB1F9591EFB60A5009C7475 /* ASLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLog.m; sourceTree = ""; }; CCB1F95B1EFB6316009C7475 /* ASSignpost.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASSignpost.h; sourceTree = ""; }; CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeSnapshotTests.m; sourceTree = ""; }; @@ -882,12 +922,29 @@ CCCCCCD41EC3EF060087FE10 /* NSParagraphStyle+ASText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSParagraphStyle+ASText.m"; sourceTree = ""; }; CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+ASText.h"; sourceTree = ""; }; CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+ASText.m"; sourceTree = ""; }; + CCDC9B4B200991D10063C1F8 /* ASGraphicsContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASGraphicsContext.h; sourceTree = ""; }; + CCDC9B4C200991D10063C1F8 /* ASGraphicsContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASGraphicsContext.m; sourceTree = ""; }; CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionModernDataSourceTests.m; sourceTree = ""; }; CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSectionController.h; sourceTree = ""; }; CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+AsyncDisplayKit.h"; sourceTree = ""; }; 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 = ""; }; + CCED5E3C2020D36800395C40 /* ASNetworkImageLoadInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASNetworkImageLoadInfo.h; sourceTree = ""; }; + CCED5E3D2020D36800395C40 /* ASNetworkImageLoadInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASNetworkImageLoadInfo.m; sourceTree = ""; }; + CCED5E402020D41600395C40 /* ASNetworkImageLoadInfo+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASNetworkImageLoadInfo+Private.h"; sourceTree = ""; }; + CCEDDDC8200C2AC300FFCD0A /* ASConfigurationInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASConfigurationInternal.h; sourceTree = ""; }; + CCEDDDC9200C2AC300FFCD0A /* ASConfigurationInternal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASConfigurationInternal.m; sourceTree = ""; }; + CCEDDDCC200C2CB900FFCD0A /* ASConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASConfiguration.h; sourceTree = ""; }; + CCEDDDCE200C42A200FFCD0A /* ASConfigurationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASConfigurationDelegate.h; sourceTree = ""; }; + CCEDDDD0200C488000FFCD0A /* ASConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASConfiguration.m; sourceTree = ""; }; + CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASConfigurationTests.m; 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 = ""; }; @@ -901,7 +958,6 @@ DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPagerFlowLayout.h; sourceTree = ""; }; DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPagerFlowLayout.m; sourceTree = ""; }; DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+FrameworkPrivate.h"; sourceTree = ""; }; - DE7EF4F71DFF77720082B84A /* ASDisplayNode+FrameworkSubclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+FrameworkSubclasses.h"; sourceTree = ""; }; DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDelegateProxy.h; sourceTree = ""; }; DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDelegateProxy.m; sourceTree = ""; }; DEC146B41C37A16A004A0EE7 /* ASCollectionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASCollectionInternal.h; path = Details/ASCollectionInternal.h; sourceTree = ""; }; @@ -911,6 +967,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 = ""; }; @@ -926,6 +985,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 = ""; }; @@ -933,8 +993,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 = ""; }; @@ -949,6 +1007,7 @@ E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeExtrasTests.m; sourceTree = ""; }; + FA4FAF14200A850200E735BD /* ASControlNode+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASControlNode+Private.h"; sourceTree = ""; }; FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1010,6 +1069,7 @@ isa = PBXGroup; children = ( 058D09B1195D04C000B7D73C /* Source */, + CC224E942066CA6D00BBA57F /* Schemas */, 058D09C5195D04C000B7D73C /* Tests */, 058D09AE195D04C000B7D73C /* Frameworks */, 058D09AD195D04C000B7D73C /* Products */, @@ -1047,68 +1107,82 @@ 058D09B1195D04C000B7D73C /* Source */ = { isa = PBXGroup; children = ( + 058D0A42195D058D00B7D73C /* Base */, CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */, + DE89C1691DCEB9CC00D49D74 /* Debug */, + 058D09E1195D050800B7D73C /* Details */, + AC6456051B0A333200CF11B8 /* Layout */, + 058D0A01195D050800B7D73C /* Private */, + 058D09B2195D04C000B7D73C /* Supporting Files */, + 257754661BED245B00737CA5 /* TextKit */, + 690ED5911E36D118000627C0 /* tvOS */, CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */, - DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */, - DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */, - 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */, - 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */, - AEEC47DF1C20C2DD00EC1693 /* ASVideoNode.h */, - AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */, - 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */, - 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */, + DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */, + DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */, + CCEDDDCC200C2CB900FFCD0A /* ASConfiguration.h */, + CCEDDDD0200C488000FFCD0A /* ASConfiguration.m */, + CCEDDDC8200C2AC300FFCD0A /* ASConfigurationInternal.h */, + CCEDDDC9200C2AC300FFCD0A /* ASConfigurationInternal.m */, + CCEDDDCE200C42A200FFCD0A /* ASConfigurationDelegate.h */, 055F1A3A19ABD43F004DAFF1 /* ASCellNode.h */, AC6456071B0A335000CF11B8 /* ASCellNode.mm */, + CC84C7F020474C5300A3851B /* ASCGImageBuffer.h */, + CC84C7F120474C5300A3851B /* ASCGImageBuffer.m */, + DEC146B41C37A16A004A0EE7 /* ASCollectionInternal.h */, + DEC146B51C37A16A004A0EE7 /* ASCollectionInternal.m */, 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */, 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */, B13CA0FF1C52004900E031AB /* ASCollectionNode+Beta.h */, AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */, AC3C4A501A1139C100143C57 /* ASCollectionView.mm */, - AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */, B13CA0F61C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h */, - DEC146B41C37A16A004A0EE7 /* ASCollectionInternal.h */, - DEC146B51C37A16A004A0EE7 /* ASCollectionInternal.m */, + AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */, + DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */, 058D09D5195D050800B7D73C /* ASControlNode.h */, 058D09D6195D050800B7D73C /* ASControlNode.mm */, - DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */, - DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */, 058D09D7195D050800B7D73C /* ASControlNode+Subclasses.h */, 058D09D8195D050800B7D73C /* ASDisplayNode.h */, 058D09D9195D050800B7D73C /* ASDisplayNode.mm */, + CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */, + CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.m */, 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */, - 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */, - 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */, CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */, CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */, 69BCE3D71EC6513B007DCCAD /* ASDisplayNode+Layout.mm */, + 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */, + 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */, 058D09DB195D050800B7D73C /* ASDisplayNodeExtras.h */, 058D09DC195D050800B7D73C /* ASDisplayNodeExtras.mm */, 0587F9BB1A7309ED00AFF0BA /* ASEditableTextNode.h */, 0587F9BC1A7309ED00AFF0BA /* ASEditableTextNode.mm */, + CC7AF195200D9BD500A21BDE /* ASExperimentalFeatures.h */, + CC7AF197200D9E8400A21BDE /* ASExperimentalFeatures.m */, 058D09DD195D050800B7D73C /* ASImageNode.h */, 058D09DE195D050800B7D73C /* ASImageNode.mm */, 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */, - 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */, - 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.m */, + 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */, + 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */, 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */, 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */, 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */, 68FC85DD1CE29AB700EDD713 /* ASNavigationController.m */, - CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */, - CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.m */, + CCED5E3C2020D36800395C40 /* ASNetworkImageLoadInfo.h */, + CCED5E3D2020D36800395C40 /* ASNetworkImageLoadInfo.m */, 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */, 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */, 698371D91E4379CD00437585 /* ASNodeController+Beta.h */, 698371DA1E4379CD00437585 /* ASNodeController+Beta.m */, + DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */, + DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */, 25E327541C16819500A2170C /* ASPagerNode.h */, 25E327551C16819500A2170C /* ASPagerNode.m */, E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */, A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */, A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */, CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */, - ACE87A2B1D73696800D7FF06 /* ASSectionContext.h */, D785F6601A74327E00291744 /* ASScrollNode.h */, D785F6611A74327E00291744 /* ASScrollNode.mm */, + ACE87A2B1D73696800D7FF06 /* ASSectionContext.h */, 68FC85E01CE29B7E00EDD713 /* ASTabBarController.h */, 68FC85E11CE29B7E00EDD713 /* ASTabBarController.m */, B0F880581BEAEC7500D17647 /* ASTableNode.h */, @@ -1118,27 +1192,25 @@ 055F1A3319ABD3E3004DAFF1 /* ASTableView.mm */, AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */, 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */, + CC18248B200D49C800875940 /* ASTextNodeCommon.h */, 058D09DF195D050800B7D73C /* ASTextNode.h */, - A373200E1C571B050011FC94 /* ASTextNode+Beta.h */, 058D09E0195D050800B7D73C /* ASTextNode.mm */, + A373200E1C571B050011FC94 /* ASTextNode+Beta.h */, 909C4C731F09C98B00D6B76F /* ASTextNode2.h */, 909C4C741F09C98B00D6B76F /* ASTextNode2.mm */, + AEEC47DF1C20C2DD00EC1693 /* ASVideoNode.h */, + AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */, + 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */, + 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */, ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */, 9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */, - 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */, - DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */, 68FC85E71CE29C7D00EDD713 /* ASVisibilityProtocols.h */, 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.m */, + 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */, + 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */, + 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.m */, CC55A70B1E529FA200594372 /* UIResponder+AsyncDisplayKit.h */, CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.m */, - 058D0A42195D058D00B7D73C /* Base */, - DE89C1691DCEB9CC00D49D74 /* Debug */, - 058D09E1195D050800B7D73C /* Details */, - 058D0A01195D050800B7D73C /* Private */, - AC6456051B0A333200CF11B8 /* Layout */, - 257754661BED245B00737CA5 /* TextKit */, - 690ED5911E36D118000627C0 /* tvOS */, - 058D09B2195D04C000B7D73C /* Supporting Files */, ); path = Source; sourceTree = ""; @@ -1146,8 +1218,8 @@ 058D09B2195D04C000B7D73C /* Supporting Files */ = { isa = PBXGroup; children = ( - CC57EAF91E394EA40034C595 /* Info.plist */, 044285011BAA3CC700D16268 /* AsyncDisplayKit.modulemap */, + CC57EAF91E394EA40034C595 /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; @@ -1155,73 +1227,85 @@ 058D09C5195D04C000B7D73C /* Tests */ = { isa = PBXGroup; children = ( - CC583ABF1EF9BAB400134156 /* Common */, - CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, - CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, - CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, - CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, - CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */, - CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */, - CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */, - CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */, - CC54A81D1D7008B300296A24 /* ASDispatchTests.m */, - CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.m */, - CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.m */, - CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */, - 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.m */, - DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */, DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */, - CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */, - CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */, - 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, - 056D21501ABCEDA1001107EF /* ASSnapshotTestCase.h */, - 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */, - 056D21541ABCEF50001107EF /* ASImageNodeSnapshotTests.m */, - ACF6ED531B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm */, - 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */, - ACF6ED551B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm */, - ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */, - 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */, - ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */, - ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */, AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.m */, - 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.m */, - ACF6ED571B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.h */, - ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m */, - 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */, + 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */, 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */, - CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */, + 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */, 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */, + CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */, + CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, + ACF6ED531B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm */, + CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, + 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */, 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */, - AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.m */, + CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.m */, 2911485B1A77147A005D0878 /* ASControlNodeTests.m */, + 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */, ACF6ED541B178DC700DA7C62 /* ASDimensionTests.mm */, + CC54A81D1D7008B300296A24 /* ASDispatchTests.m */, 058D0A2D195D057000B7D73C /* ASDisplayLayerTests.m */, 058D0A2E195D057000B7D73C /* ASDisplayNodeAppearanceTests.m */, - 058D0A2F195D057000B7D73C /* ASDisplayNodeTests.mm */, + F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */, + DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */, 69B225661D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm */, + CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */, + 058D0A2F195D057000B7D73C /* ASDisplayNodeTests.mm */, 058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */, 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.m */, 697B31591CFE4B410049936F /* ASEditableTextNodeTests.m */, + 056D21541ABCEF50001107EF /* ASImageNodeSnapshotTests.m */, + ACF6ED551B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm */, + CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */, + 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.m */, + CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */, E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */, + ACF6ED571B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.h */, + ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m */, + 699B83501E3C1BA500433FA4 /* ASLayoutSpecTests.m */, + CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */, + CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */, 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, + BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.m */, + CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, + ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */, + AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.m */, + CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */, + CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */, + CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */, + ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */, + E52AC9BE1FEA915D00AA4040 /* ASRectMapTests.m */, + CCAA0B81206ADECB0057B336 /* ASRecursiveUnfairLockTests.m */, + 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */, + 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */, + E586F96B1F9F9E2900ECE00E /* ASScrollNodeTests.m */, + 056D21501ABCEDA1001107EF /* ASSnapshotTestCase.h */, + 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */, + ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */, + BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */, CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */, 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */, - 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */, 254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */, + 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */, + CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */, + 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.m */, 058D0A36195D057000B7D73C /* ASTextNodeTests.m */, 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */, + CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */, + CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */, + 4496D0721FA9EA6B001CC8D5 /* ASTraitCollectionTests.m */, + CC0AEEA31D66316E005D1C78 /* ASUICollectionViewTests.m */, AEEC47E31C21D3D200EC1693 /* ASVideoNodeTests.m */, - F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */, + CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.m */, + 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.m */, + CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */, + 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */, + 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, + CC583ABF1EF9BAB400134156 /* Common */, 058D09C6195D04C000B7D73C /* Supporting Files */, 052EE06A1A15A0D8002C6279 /* TestResources */, - 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */, - 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.m */, - 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */, - 699B83501E3C1BA500433FA4 /* ASLayoutSpecTests.m */, - 4E9127681F64157600499623 /* ASRunLoopQueueTests.m */, ); path = Tests; sourceTree = ""; @@ -1240,12 +1324,13 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( - CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */, - CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */, - CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */, - CC0F885D1E4280B800576FED /* _ASCollectionViewCell.m */, + E5B077EB1E6843AF00C24B5B /* Collection Layout */, + 25B171EA1C12242700508A7A /* Data Controller */, + 058D09F7195D050800B7D73C /* Transactions */, 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */, 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.m */, + CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */, + CC0F885D1E4280B800576FED /* _ASCollectionViewCell.m */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -1257,23 +1342,25 @@ 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */, 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */, 299DA1A71A828D2900162D41 /* ASBatchContext.h */, - 299DA1A81A828D2900162D41 /* ASBatchContext.mm */, + 299DA1A81A828D2900162D41 /* ASBatchContext.m */, E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */, - 68C215561DE10D330019C4BC /* ASCollectionViewLayoutInspector.h */, - 68C215571DE10D330019C4BC /* ASCollectionViewLayoutInspector.m */, 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */, 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.m */, + 68C215561DE10D330019C4BC /* ASCollectionViewLayoutInspector.h */, + 68C215571DE10D330019C4BC /* ASCollectionViewLayoutInspector.m */, 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */, 696F01EB1DD2AF450049FBD5 /* ASEventLog.mm */, + CCDC9B4B200991D10063C1F8 /* ASGraphicsContext.h */, + CCDC9B4C200991D10063C1F8 /* ASGraphicsContext.m */, E5B225271F1790B5001E1431 /* ASHashing.h */, E5B225261F1790B5001E1431 /* ASHashing.m */, - 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */, - 4640521C1A3F83C40061C0BA /* ASTableLayoutController.m */, 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */, 058D09E7195D050800B7D73C /* ASHighlightOverlayLayer.mm */, 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */, 68355B381CB57A5A001D4E68 /* ASImageContainerProtocolCategories.m */, 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */, + CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */, + CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */, 4640521D1A3F83C40061C0BA /* ASLayoutController.h */, 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */, 68EE0DBB1C1B4ED300BA1B99 /* ASMainSerialQueue.h */, @@ -1289,10 +1376,14 @@ 055F1A3619ABD413004DAFF1 /* ASRangeController.h */, 055F1A3719ABD413004DAFF1 /* ASRangeController.mm */, 69F10C851C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h */, + CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */, + CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.m */, 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */, 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */, 296A0A311A951715005ACEAA /* ASScrollDirection.h */, 205F0E111B371BD7007741D0 /* ASScrollDirection.m */, + 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */, + 4640521C1A3F83C40061C0BA /* ASTableLayoutController.m */, 058D0A12195D050800B7D73C /* ASThread.h */, CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */, CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.m */, @@ -1304,15 +1395,12 @@ CC3B20881C3F7A5400798563 /* ASWeakSet.m */, 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */, 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.m */, - 25B171EA1C12242700508A7A /* Data Controller */, - E5B077EB1E6843AF00C24B5B /* Collection Layout */, DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */, DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */, CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */, CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m */, 058D09F5195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.h */, 058D09F6195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.m */, - 058D09F7195D050800B7D73C /* Transactions */, 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */, 205F0E0E1B371875007741D0 /* UICollectionViewLayout+ASConvenience.m */, 058D09FF195D050800B7D73C /* UIView+ASConvenience.h */, @@ -1325,9 +1413,9 @@ children = ( 058D09F8195D050800B7D73C /* _ASAsyncTransaction.h */, 058D09F9195D050800B7D73C /* _ASAsyncTransaction.mm */, - 058D09FA195D050800B7D73C /* _ASAsyncTransactionContainer+Private.h */, 058D09FB195D050800B7D73C /* _ASAsyncTransactionContainer.h */, 058D09FC195D050800B7D73C /* _ASAsyncTransactionContainer.m */, + 058D09FA195D050800B7D73C /* _ASAsyncTransactionContainer+Private.h */, 058D09FD195D050800B7D73C /* _ASAsyncTransactionGroup.h */, 058D09FE195D050800B7D73C /* _ASAsyncTransactionGroup.m */, ); @@ -1337,29 +1425,10 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( - CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */, - CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */, - CCA282C61E9EB64B0037E8B7 /* ASDisplayNodeTipState.h */, - CCA282C71E9EB64B0037E8B7 /* ASDisplayNodeTipState.m */, - CCA282CA1E9EB73E0037E8B7 /* ASTipNode.h */, - CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */, - CCA282BE1E9EAE010037E8B7 /* ASTip.h */, - CCA282BF1E9EAE010037E8B7 /* ASTip.m */, - CCA282C21E9EAE630037E8B7 /* ASLayerBackingTipProvider.h */, - CCA282C31E9EAE630037E8B7 /* ASLayerBackingTipProvider.m */, - CCA282BA1E9EABDD0037E8B7 /* ASTipProvider.h */, - CCA282BB1E9EABDD0037E8B7 /* ASTipProvider.m */, - CCA282B21E9EA7310037E8B7 /* ASTipsController.h */, - CCA282B31E9EA7310037E8B7 /* ASTipsController.m */, - CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */, - CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */, - E5ABAC791E8564EE007AC15C /* ASRectTable.h */, - E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */, - CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */, - CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, - 6947B0BB1E36B4E30007C478 /* Layout */, CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */, E52F8AEE1EAE659600B5A912 /* Collection Layout */, + 6947B0BB1E36B4E30007C478 /* Layout */, + CCCCCCC11EC3EF060087FE10 /* TextExperiment */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, @@ -1376,6 +1445,7 @@ CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */, CC0F885A1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h */, CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.m */, + FA4FAF14200A850200E735BD /* ASControlNode+Private.h */, 9F98C0231DBDF2A300476D92 /* ASControlTargetAction.h */, 9F98C0241DBDF2A300476D92 /* ASControlTargetAction.m */, 8B0768B11CE752EC002E1453 /* ASDefaultPlaybackButton.h */, @@ -1388,27 +1458,46 @@ 058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */, 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */, DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */, - DE7EF4F71DFF77720082B84A /* ASDisplayNode+FrameworkSubclasses.h */, 058D0A0B195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm */, 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */, 6959433C1D70815300B0EE1F /* ASDisplayNodeLayout.mm */, - 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */, + CCA282C61E9EB64B0037E8B7 /* ASDisplayNodeTipState.h */, + CCA282C71E9EB64B0037E8B7 /* ASDisplayNodeTipState.m */, 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */, + 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */, ACF6ED431B17847A00DA7C62 /* ASInternalHelpers.h */, ACF6ED441B17847A00DA7C62 /* ASInternalHelpers.m */, + CCA282C21E9EAE630037E8B7 /* ASLayerBackingTipProvider.h */, + CCA282C31E9EAE630037E8B7 /* ASLayerBackingTipProvider.m */, E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */, E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */, - 0442850B1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h */, - 0442850C1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.m */, + CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */, + CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */, + CCED5E402020D41600395C40 /* ASNetworkImageLoadInfo+Private.h */, CC3B20811C3F76D600798563 /* ASPendingStateController.h */, CC3B20821C3F76D600798563 /* ASPendingStateController.mm */, + E52AC9B91FEA90EB00AA4040 /* ASRectMap.h */, + E52AC9B81FEA90EB00AA4040 /* ASRectMap.mm */, + CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */, + CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */, + CCA282BE1E9EAE010037E8B7 /* ASTip.h */, + CCA282BF1E9EAE010037E8B7 /* ASTip.m */, + CCA282CA1E9EB73E0037E8B7 /* ASTipNode.h */, + CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */, + CCA282BA1E9EABDD0037E8B7 /* ASTipProvider.h */, + CCA282BB1E9EABDD0037E8B7 /* ASTipProvider.m */, + CCA282B21E9EA7310037E8B7 /* ASTipsController.h */, + CCA282B31E9EA7310037E8B7 /* ASTipsController.m */, + CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */, + CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */, + 0442850B1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h */, + 0442850C1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.m */, 83A7D9581D44542100BF333E /* ASWeakMap.h */, 83A7D9591D44542100BF333E /* ASWeakMap.m */, - CCCCCCC11EC3EF060087FE10 /* TextExperiment */, ); path = Private; sourceTree = ""; @@ -1433,19 +1522,18 @@ children = ( B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */, B30BF6511C5964B0004FCD53 /* ASLayoutManager.m */, + 257754951BEE44CD00737CA5 /* ASTextKitAttributes.h */, + 257754941BEE44CD00737CA5 /* ASTextKitAttributes.mm */, 257754BA1BEE458E00737CA5 /* ASTextKitComponents.h */, 257754B71BEE458D00737CA5 /* ASTextKitComponents.mm */, - 257754BB1BEE458E00737CA5 /* ASTextKitCoreTextAdditions.h */, - 257754B81BEE458E00737CA5 /* ASTextKitCoreTextAdditions.m */, - 257754B91BEE458E00737CA5 /* ASTextNodeWordKerner.h */, - 257754BC1BEE458E00737CA5 /* ASTextNodeTypes.h */, - 257754BD1BEE458E00737CA5 /* ASTextNodeWordKerner.m */, - 257754941BEE44CD00737CA5 /* ASTextKitAttributes.mm */, - 257754951BEE44CD00737CA5 /* ASTextKitAttributes.h */, 257754961BEE44CD00737CA5 /* ASTextKitContext.h */, 257754971BEE44CD00737CA5 /* ASTextKitContext.mm */, + 257754BB1BEE458E00737CA5 /* ASTextKitCoreTextAdditions.h */, + 257754B81BEE458E00737CA5 /* ASTextKitCoreTextAdditions.m */, 257754981BEE44CD00737CA5 /* ASTextKitEntityAttribute.h */, 257754991BEE44CD00737CA5 /* ASTextKitEntityAttribute.m */, + A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */, + 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */, 257754931BEE44CD00737CA5 /* ASTextKitRenderer.h */, 2577549A1BEE44CD00737CA5 /* ASTextKitRenderer.mm */, 2577549B1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.h */, @@ -1456,9 +1544,10 @@ 257754A01BEE44CD00737CA5 /* ASTextKitShadower.mm */, 257754A11BEE44CD00737CA5 /* ASTextKitTailTruncater.h */, 257754A21BEE44CD00737CA5 /* ASTextKitTailTruncater.mm */, - A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */, - 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */, 257754A31BEE44CD00737CA5 /* ASTextKitTruncating.h */, + 257754BC1BEE458E00737CA5 /* ASTextNodeTypes.h */, + 257754B91BEE458E00737CA5 /* ASTextNodeWordKerner.h */, + 257754BD1BEE458E00737CA5 /* ASTextNodeWordKerner.m */, ); name = TextKit; sourceTree = ""; @@ -1466,12 +1555,12 @@ 25B171EA1C12242700508A7A /* Data Controller */ = { isa = PBXGroup; children = ( - DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */, - DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.m */, - 464052191A3F83C40061C0BA /* ASDataController.h */, - 4640521A1A3F83C40061C0BA /* ASDataController.mm */, E5711A2A1C840C81009619D4 /* ASCollectionElement.h */, E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */, + 464052191A3F83C40061C0BA /* ASDataController.h */, + 4640521A1A3F83C40061C0BA /* ASDataController.mm */, + DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */, + DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.m */, E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */, E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */, AC6145401D8AFAE8003D62A2 /* ASSection.h */, @@ -1483,9 +1572,7 @@ 690ED5911E36D118000627C0 /* tvOS */ = { isa = PBXGroup; children = ( - 690ED5921E36D118000627C0 /* ASControlNode+tvOS.h */, 690ED5931E36D118000627C0 /* ASControlNode+tvOS.m */, - 690ED5941E36D118000627C0 /* ASImageNode+tvOS.h */, 690ED5951E36D118000627C0 /* ASImageNode+tvOS.m */, ); path = tvOS; @@ -1518,6 +1605,8 @@ ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */, ACF6ED031B17843500DA7C62 /* ASCenterLayoutSpec.h */, ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */, + 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */, + 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */, ACF6ED071B17843500DA7C62 /* ASDimension.h */, ACF6ED081B17843500DA7C62 /* ASDimension.mm */, 690C35631E055C7B00069B91 /* ASDimensionInternal.h */, @@ -1552,6 +1641,14 @@ path = Layout; sourceTree = ""; }; + CC224E942066CA6D00BBA57F /* Schemas */ = { + isa = PBXGroup; + children = ( + CC224E952066CA6D00BBA57F /* configuration.json */, + ); + path = Schemas; + sourceTree = ""; + }; CC583ABF1EF9BAB400134156 /* Common */ = { isa = PBXGroup; children = ( @@ -1559,6 +1656,7 @@ CC583AC11EF9BAB400134156 /* ASTestCase.h */, CC583AC21EF9BAB400134156 /* ASTestCase.m */, CC583AC31EF9BAB400134156 /* ASXCTExtensions.h */, + CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */, CC583AC41EF9BAB400134156 /* NSInvocation+ASTestHelpers.h */, CC583AC51EF9BAB400134156 /* NSInvocation+ASTestHelpers.m */, CC583AC61EF9BAB400134156 /* OCMockObject+ASAdditions.h */, @@ -1619,9 +1717,9 @@ CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */ = { isa = PBXGroup; children = ( + CCF92DCE1E315FC50019E9C6 /* IGListKit Support */, CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */, CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */, - CCF92DCE1E315FC50019E9C6 /* IGListKit Support */, ); name = "Collection Data Adapter"; sourceTree = ""; @@ -1649,10 +1747,10 @@ DE89C1691DCEB9CC00D49D74 /* Debug */ = { isa = PBXGroup; children = ( - CCA282B61E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h */, - CCA282B71E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.m */, 764D83D21C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h */, 764D83D31C8EA515009B4FB8 /* AsyncDisplayKit+Debug.m */, + CCA282B61E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h */, + CCA282B71E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.m */, ); path = Debug; sourceTree = ""; @@ -1660,6 +1758,10 @@ E52F8AEE1EAE659600B5A912 /* Collection Layout */ = { isa = PBXGroup; children = ( + E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */, + E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */, + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */, @@ -1668,10 +1770,6 @@ E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */, - E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */, - E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */, - E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, - E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, ); name = "Collection Layout"; sourceTree = ""; @@ -1679,15 +1777,15 @@ E5B077EB1E6843AF00C24B5B /* Collection Layout */ = { isa = PBXGroup; children = ( - E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */, - E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */, - E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */, - E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */, - E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */, + E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */, + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */, + E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, + E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */, + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); @@ -1711,12 +1809,14 @@ 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 */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */, E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */, + CCAA0B7F206ADBF30057B336 /* ASRecursiveUnfairLock.h in Headers */, E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */, CCCCCCE31EC3EF060087FE10 /* NSParagraphStyle+ASText.h in Headers */, @@ -1725,6 +1825,7 @@ 696F01EC1DD2AF450049FBD5 /* ASEventLog.h in Headers */, 690C35641E055C7B00069B91 /* ASDimensionInternal.h in Headers */, 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */, + CC18248C200D49C800875940 /* ASTextNodeCommon.h in Headers */, 698371DB1E4379CD00437585 /* ASNodeController+Beta.h in Headers */, 6907C2581DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h in Headers */, 69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */, @@ -1777,6 +1878,7 @@ CC6AA2DA1E9F03B900978E87 /* ASDisplayNode+Ancestry.h in Headers */, 8021EC1D1D2B00B100799119 /* UIImage+ASConvenience.h in Headers */, B35061FD1B010EFD0018CF92 /* ASDisplayNode+Subclasses.h in Headers */, + CCEDDDCD200C2CB900FFCD0A /* ASConfiguration.h in Headers */, B35061FB1B010EFD0018CF92 /* ASDisplayNode.h in Headers */, B35061FE1B010EFD0018CF92 /* ASDisplayNodeExtras.h in Headers */, CC0F88601E4280B800576FED /* _ASCollectionViewCell.h in Headers */, @@ -1789,12 +1891,12 @@ B35062021B010EFD0018CF92 /* ASImageNode.h in Headers */, B350621F1B010EFD0018CF92 /* ASImageProtocols.h in Headers */, 34EFC75F1B701C8600AD841F /* ASInsetLayoutSpec.h in Headers */, + CCEDDDCA200C2AC300FFCD0A /* ASConfigurationInternal.h in Headers */, 34EFC7671B701CD900AD841F /* ASLayout.h in Headers */, DBDB83951C6E879900D0098C /* ASPagerFlowLayout.h in Headers */, 34EFC7691B701CE100AD841F /* ASLayoutElement.h in Headers */, 698DFF471E36B7E9002891F1 /* ASLayoutSpecUtilities.h in Headers */, 9C70F20D1CDBE9CB007D6C76 /* ASDefaultPlayButton.h in Headers */, - DE7EF4F81DFF77720082B84A /* ASDisplayNode+FrameworkSubclasses.h in Headers */, CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */, CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */, 254C6B7E1BF94DF4003EC431 /* ASTextKitTailTruncater.h in Headers */, @@ -1802,6 +1904,7 @@ 68EE0DBE1C1B4ED300BA1B99 /* ASMainSerialQueue.h in Headers */, CCCCCCE11EC3EF060087FE10 /* ASTextUtilities.h in Headers */, B350624B1B010EFD0018CF92 /* _ASPendingState.h in Headers */, + CCDC9B4D200991D10063C1F8 /* ASGraphicsContext.h in Headers */, E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */, CC54A81C1D70079800296A24 /* ASDispatch.h in Headers */, B350624D1B010EFD0018CF92 /* _ASScopeTimer.h in Headers */, @@ -1823,6 +1926,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 */, @@ -1833,6 +1937,7 @@ 9C70F20F1CDBE9FF007D6C76 /* ASLayoutManager.h in Headers */, 6947B0C31E36B5040007C478 /* ASStackPositionedLayout.h in Headers */, DBABFAFC1C6A8D2F0039EA4A /* _ASTransitionContext.h in Headers */, + FA4FAF15200A850200E735BD /* ASControlNode+Private.h in Headers */, B350624F1B010EFD0018CF92 /* ASDisplayNode+DebugTiming.h in Headers */, CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */, B35062521B010EFD0018CF92 /* ASDisplayNodeInternal.h in Headers */, @@ -1848,14 +1953,13 @@ DEC146B71C37A16A004A0EE7 /* ASCollectionInternal.h in Headers */, 68B8A4E21CBDB958007E4543 /* ASWeakProxy.h in Headers */, 9F98C0271DBE29FC00476D92 /* ASControlTargetAction.h in Headers */, - 690ED5961E36D118000627C0 /* ASControlNode+tvOS.h in Headers */, + CC84C7F220474C5300A3851B /* ASCGImageBuffer.h in Headers */, 695943401D70815300B0EE1F /* ASDisplayNodeLayout.h in Headers */, 0442850E1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.h in Headers */, DE8BEAC21C2DF3FC00D57C12 /* ASDelegateProxy.h in Headers */, B350623E1B010EFD0018CF92 /* _ASAsyncTransactionContainer+Private.h in Headers */, AC6145411D8AFAE8003D62A2 /* ASSection.h in Headers */, 8BBBAB8C1CEBAF1700107FC6 /* ASDefaultPlaybackButton.h in Headers */, - 690ED5991E36D118000627C0 /* ASImageNode+tvOS.h in Headers */, 254C6B741BF94DF4003EC431 /* ASTextNodeWordKerner.h in Headers */, 698DFF441E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h in Headers */, CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */, @@ -1868,6 +1972,7 @@ DE4843DC1C93EAC100A1F33B /* ASLayoutTransition.h in Headers */, CC57EAF81E3939450034C595 /* ASTableView+Undeprecated.h in Headers */, 254C6B781BF94DF4003EC431 /* ASTextKitContext.h in Headers */, + CCED5E412020D49D00395C40 /* ASNetworkImageLoadInfo+Private.h in Headers */, 9CDC18CD1B910E12004965E2 /* ASLayoutElementPrivate.h in Headers */, B35062201B010EFD0018CF92 /* ASLayoutController.h in Headers */, B35062211B010EFD0018CF92 /* ASLayoutRangeType.h in Headers */, @@ -1884,11 +1989,11 @@ 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 */, CCA282C41E9EAE630037E8B7 /* ASLayerBackingTipProvider.h in Headers */, + CCEDDDCF200C42A200FFCD0A /* ASConfigurationDelegate.h in Headers */, E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */, 6900C5F41E8072DA00BCD75C /* ASImageNode+Private.h in Headers */, 68B0277B1C1A79D60041016B /* ASDisplayNode+Beta.h in Headers */, @@ -1900,6 +2005,7 @@ CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */, 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */, CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */, + CC7AF196200D9BD500A21BDE /* ASExperimentalFeatures.h in Headers */, CCCCCCDF1EC3EF060087FE10 /* ASTextRunDelegate.h in Headers */, 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */, 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */, @@ -1920,6 +2026,7 @@ B35062391B010EFD0018CF92 /* ASThread.h in Headers */, 2C107F5B1BA9F54500F13DE5 /* AsyncDisplayKit.h in Headers */, 509E68651B3AEDC5009B9150 /* CoreGraphics+ASConvenience.h in Headers */, + CCED5E3E2020D36800395C40 /* ASNetworkImageLoadInfo.h in Headers */, B350623A1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.h in Headers */, 044284FF1BAA3BD600D16268 /* UICollectionViewLayout+ASConvenience.h in Headers */, B35062431B010EFD0018CF92 /* UIView+ASConvenience.h in Headers */, @@ -1994,7 +2101,7 @@ attributes = { CLASSPREFIX = AS; LastUpgradeCheck = 0820; - ORGANIZATIONNAME = Facebook; + ORGANIZATIONNAME = Pinterest; TargetAttributes = { 057D02BE1AC0A66700C7AC3C = { CreatedOnToolsVersion = 6.2; @@ -2051,6 +2158,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + CC224E962066CA6D00BBA57F /* configuration.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2063,13 +2171,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 */ = { @@ -2118,7 +2229,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CCEDDDD9200C518800FFCD0A /* ASConfigurationTests.m in Sources */, E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */, + 4496D0731FA9EA6B001CC8D5 /* ASTraitCollectionTests.m in Sources */, 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m in Sources */, CC583AD71EF9BDC100134156 /* NSInvocation+ASTestHelpers.m in Sources */, CC051F1F1D7A286A006434CB /* ASCALayerTests.m in Sources */, @@ -2128,9 +2241,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 */, @@ -2156,25 +2270,32 @@ 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 */, + CCAA0B82206ADECB0057B336 /* ASRecursiveUnfairLockTests.m 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 */, @@ -2195,6 +2316,7 @@ 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 */, 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, @@ -2223,11 +2345,12 @@ E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */, 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.m in Sources */, + CCDC9B4E200991D10063C1F8 /* ASGraphicsContext.m in Sources */, CCCCCCD81EC3EF060087FE10 /* ASTextInput.m in Sources */, 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, DE8BEAC41C2DF3FC00D57C12 /* ASDelegateProxy.m in Sources */, B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, - B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */, + B35062161B010EFD0018CF92 /* ASBatchContext.m in Sources */, AC47D9421B3B891B00AAEE9D /* ASCellNode.mm in Sources */, E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.m in Sources */, 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */, @@ -2235,6 +2358,7 @@ E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */, 68FC85EC1CE29C7D00EDD713 /* ASVisibilityProtocols.m in Sources */, CC55A7121E52A0F200594372 /* ASResponderChainEnumerator.m in Sources */, + CCED5E3F2020D36800395C40 /* ASNetworkImageLoadInfo.m in Sources */, 68B8A4E41CBDB958007E4543 /* ASWeakProxy.m in Sources */, E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */, 9C70F20A1CDBE949007D6C76 /* ASTableNode.mm in Sources */, @@ -2244,9 +2368,11 @@ 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.m in Sources */, B35061F91B010EFD0018CF92 /* ASControlNode.mm in Sources */, 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.m in Sources */, + CCAA0B80206ADBF30057B336 /* ASRecursiveUnfairLock.m in Sources */, B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */, CCB1F95A1EFB60A5009C7475 /* ASLog.m in Sources */, 767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.m in Sources */, + CCEDDDCB200C2AC300FFCD0A /* ASConfigurationInternal.m in Sources */, CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.m in Sources */, 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, @@ -2285,6 +2411,7 @@ DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */, CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */, CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */, + CCEDDDD1200C488000FFCD0A /* ASConfiguration.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, @@ -2307,7 +2434,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 */, @@ -2316,6 +2442,7 @@ E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */, 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */, + CC7AF198200DAB2200A21BDE /* ASExperimentalFeatures.m in Sources */, E5B2252E1F17E521001E1431 /* ASDispatch.m in Sources */, 696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */, 9C70F2051CDA4F06007D6C76 /* ASTraitCollection.m in Sources */, @@ -2326,6 +2453,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 */, @@ -2346,6 +2474,7 @@ 697796611D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm in Sources */, B350623B1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.m in Sources */, CCA282CD1E9EB73E0037E8B7 /* ASTipNode.m in Sources */, + CC84C7F320474C5300A3851B /* ASCGImageBuffer.m in Sources */, 044284FD1BAA365100D16268 /* UICollectionViewLayout+ASConvenience.m in Sources */, CC0F885B1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.m in Sources */, 690ED5981E36D118000627C0 /* ASControlNode+tvOS.m in Sources */, @@ -2420,14 +2549,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; @@ -2450,7 +2585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2465,14 +2600,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; @@ -2488,7 +2629,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -2608,14 +2749,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; @@ -2631,7 +2778,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; diff --git a/AsyncDisplayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AsyncDisplayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/AsyncDisplayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb407e1b..32708c063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,70 @@ ## master - * Add your own contributions to the next release on the line below this with your name. +- User FLT_EPSILON in ASCeilPixelValue and ASFloorPixelValue to help with floating point precision errors when computing layouts for 3x devices. [Ricky Cancro](https://github.com/rcancro) [#838](https://github.com/TextureGroup/Texture/pull/864) +- Disable interface colescing and match to pre-colescing interface update behavior [Max Wang](https://github.com/wsdwsd0829) [#862](https://github.com/TextureGroup/Texture/pull/862) +- [ASDisplayNode] Add safeAreaInsets, layoutMargins and related properties to ASDisplayNode, with full support for older OS versions [Yevgen Pogribnyi](https://github.com/ypogribnyi) [#685](https://github.com/TextureGroup/Texture/pull/685) +- [ASPINRemoteImageDownloader] Allow cache to provide animated image. [Max Wang](https://github.com/wsdwsd0829) [#850](https://github.com/TextureGroup/Texture/pull/850) +- [tvOS] Fixes errors when building against tvOS SDK [Alex Hill](https://github.com/alexhillc) [#728](https://github.com/TextureGroup/Texture/pull/728) +- [ASDisplayNode] Add unit tests for layout z-order changes (with an open issue to fix). +- [ASWrapperCellNode] Introduce a new class allowing more control of UIKit passthrough cells. +- [ASDisplayNode] Consolidate main thread initialization and allow apps to invoke it manually instead of +load. +- [ASRunloopQueue] Introduce new runloop queue(ASCATransactionQueue) to coalesce Interface state update calls for view controller transitions. +- [ASRangeController] Fix stability of "minimum" rangeMode if the app has more than one layout before scrolling. +- **Important** ASDisplayNode's cornerRadius is a new thread-safe bridged property that should be preferred over CALayer's. Use the latter at your own risk! [Huy Nguyen](https://github.com/nguyenhuy) [#749](https://github.com/TextureGroup/Texture/pull/749). +- [ASCellNode] Adds mapping for UITableViewCell focusStyle [Alex Hill](https://github.com/alexhillc) [#727](https://github.com/TextureGroup/Texture/pull/727) +- [ASNetworkImageNode] Fix capturing self in the block while loading image in ASNetworkImageNode. [Denis Mororozov](https://github.com/morozkin) [#777](https://github.com/TextureGroup/Texture/pull/777) +- [ASTraitCollection] Add new properties of UITraitCollection to ASTraitCollection. [Yevgen Pogribnyi](https://github.com/ypogribnyi) +- [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) +- [ASCornerLayoutSpec] New layout spec class for declarative corner element layout. [#657](https://github.com/TextureGroup/Texture/pull/657) [huangkun](https://github.com/huang-kun) +- [ASDisplayNode 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) +- [ASDisplayNode layout] Fix an issue that sometimes causes a node's pending layout to not be applied. [Huy Nguyen](https://github.com/nguyenhuy) [#792](https://github.com/TextureGroup/Texture/pull/792) +- [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 subnodes can start preloading right away. [Huy Nguyen](https://github.com/nguyenhuy) [#706](https://github.com/TextureGroup/Texture/pull/706) +- [ASCollectionNode] Added support for interactive item movement. [Adlai Holler](https://github.com/Adlai-Holler) +- Added an experimental "no-copy" rendering API. See ASGraphicsContext.h for info. [Adlai Holler](https://github.com/Adlai-Holler) +- Dropped support for iOS 8. [Adlai Holler](https://github.com/Adlai-Holler) +- Added a configuration API – a unified place to turn on/off experimental Texture features. See `ASConfiguration.h` for info. [Adlai Holler](https://github.com/Adlai-Holler) +- **Breaking** Changes to ASNetworkImageNode: [Adlai Holler](https://github.com/Adlai-Holler) + - Modified `ASImageDownloaderCompletion` to add an optional `id userInfo` field. Your custom downloader can pass `nil`. + - Modified the last argument to `-[ASNetworkImageNodeDelegate imageNode:didLoadImage:info:]` method from a struct to an object of new class `ASNetworkImageLoadInfo` which includes other metadata about the load operation. +- Removed +load static initializer from ASDisplayNode. [Adlai Holler](https://github.com/Adlai-Holler) +- Optimized ASNetworkImageNode loading and resolved edge cases where the image provided to the delegate was not the image that was loaded. [Adlai Holler](https://github.com/Adlai-Holler) [#778](https://github.com/TextureGroup/Texture/pull/778/) +- Make `ASCellNode` tint color apply to table view cell accessories. [Vladyslav Chapaev](https://github.com/ShogunPhyched) [#764](https://github.com/TextureGroup/Texture/pull/764) +- Fix ASTextNode2 is accessing backgroundColor off main while sizing / layout is happening. [Michael Schneider](https://github.com/maicki) [#794](https://github.com/TextureGroup/Texture/pull/778/) +- Pass scrollViewWillEndDragging delegation through in ASIGListAdapterDataSource for IGListKit integration. [#796](https://github.com/TextureGroup/Texture/pull/796) +- Fix UIResponder handling with view backing ASDisplayNode. [Michael Schneider](https://github.com/maicki) [#789](https://github.com/TextureGroup/Texture/pull/789/) +- Optimized thread-local storage by replacing pthread_specific with C11 thread-local variables. [Adlai Holler](https://github.com/Adlai-Holler) [#811](https://github.com/TextureGroup/Texture/pull/811/) +- Fixed a thread-sanitizer warning in ASTextNode. [Adlai Holler](https://github.com/Adlai-Holler) [#830](https://github.com/TextureGroup/Texture/pull/830/) +- Fix ASTextNode2 handling background color incorrectly. [Adlai Holler](https://github.com/Adlai-Holler) [#831](https://github.com/TextureGroup/Texture/pull/831/) +- [NoCopyRendering] Improved performance & fixed image memory not being tagged in Instruments. [Adlai Holler](https://github.com/Adlai-Holler) [#833](https://github.com/TextureGroup/Texture/pull/833/) +- Use `NS_RETURNS_RETAINED` macro to make our methods a tiny bit faster. [Adlai Holler](https://github.com/Adlai-Holler) [#843](https://github.com/TextureGroup/Texture/pull/843/) +- `ASDisplayNode, ASLayoutSpec, and ASLayoutElementStyle` now conform to `NSLocking`. They act as recursive locks. Useful locking macros have been added as `ASThread.h`. Subclasses / client code can lock these objects but should be careful as usual when dealing with locks. [Adlai Holler](https://github.com/Adlai-Holler) +- Introduces `ASRecursiveUnfairLock` as an experiment to improve locking performance. [Adlai Holler](https://github.com/Adlai-Holler) + +## 2.6 +- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) +- [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) + +## 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) -- [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) ## 2.5 diff --git a/Cartfile b/Cartfile index aebf9308e..f1a449b01 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ github "pinterest/PINRemoteImage" "3.0.0-beta.13" -github "pinterest/PINCache" +github "pinterest/PINCache" "3.0.1-beta.6" diff --git a/Dangerfile b/Dangerfile index edd455d53..edbbec21e 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,3 +1,5 @@ +require 'open-uri' + source_pattern = /(\.m|\.mm|\.h)$/ # Sometimes it's a README fix, or something like that - which isn't relevant for @@ -52,7 +54,7 @@ def check_file_header(files_to_check, licenses) correct_license = false licenses.each do |license| license_header = full_license(license, filename) - if data.start_with?(license_header) + if data.include? "Pinterest, Inc." correct_license = true end end @@ -67,7 +69,7 @@ end # Ensure new files have proper header new_source_license_header = <<-HEREDOC -// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -87,7 +89,7 @@ modified_source_license_header = <<-HEREDOC // 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, +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 diff --git a/Podfile b/Podfile index 0ebd8c98c..21e72482c 100644 --- a/Podfile +++ b/Podfile @@ -1,6 +1,6 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target :'AsyncDisplayKitTests' do pod 'OCMock', '~> 3.4' diff --git a/Schemas/configuration.json b/Schemas/configuration.json new file mode 100644 index 000000000..4cc58ecc7 --- /dev/null +++ b/Schemas/configuration.json @@ -0,0 +1,24 @@ +{ + "id": "configuration.json", + "title": "configuration", + "description" : "Schema definition of a Texture Configuration", + "$schema": "http://json-schema.org/schema#", + "type": "object", + "properties": { + "version" : { + "type" : "number" + }, + "experimental_features": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "exp_graphics_contexts", + "exp_text_node", + "exp_interface_state_coalesce", + "exp_unfair_lock" + ] + } + } + } +} diff --git a/Source/ASBlockTypes.h b/Source/ASBlockTypes.h index cf05a1937..f0e2875e1 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/ASButtonNode.mm b/Source/ASButtonNode.mm index 41e411caf..7f50dd91a 100644 --- a/Source/ASButtonNode.mm +++ b/Source/ASButtonNode.mm @@ -18,7 +18,7 @@ #import #import #import -#import +#import #import #import #import @@ -161,7 +161,7 @@ - (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously - (void)updateImage { - __instanceLock__.lock(); + [self lock]; UIImage *newImage; if (self.enabled == NO && _disabledImage) { @@ -178,18 +178,18 @@ - (void)updateImage if ((_imageNode != nil || newImage != nil) && newImage != self.imageNode.image) { _imageNode.image = newImage; - __instanceLock__.unlock(); + [self unlock]; [self setNeedsLayout]; return; } - __instanceLock__.unlock(); + [self unlock]; } - (void)updateTitle { - __instanceLock__.lock(); + [self lock]; NSAttributedString *newTitle; if (self.enabled == NO && _disabledAttributedTitle) { @@ -207,19 +207,19 @@ - (void)updateTitle // Calling self.titleNode is essential here because _titleNode is lazily created by the getter. if ((_titleNode != nil || newTitle.length > 0) && [self.titleNode.attributedText isEqualToAttributedString:newTitle] == NO) { _titleNode.attributedText = newTitle; - __instanceLock__.unlock(); + [self unlock]; self.accessibilityLabel = _titleNode.accessibilityLabel; [self setNeedsLayout]; return; } - __instanceLock__.unlock(); + [self unlock]; } - (void)updateBackgroundImage { - __instanceLock__.lock(); + [self lock]; UIImage *newImage; if (self.enabled == NO && _disabledBackgroundImage) { @@ -236,25 +236,25 @@ - (void)updateBackgroundImage if ((_backgroundImageNode != nil || newImage != nil) && newImage != self.backgroundImageNode.image) { _backgroundImageNode.image = newImage; - __instanceLock__.unlock(); + [self unlock]; [self setNeedsLayout]; return; } - __instanceLock__.unlock(); + [self unlock]; } - (CGFloat)contentSpacing { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _contentSpacing; } - (void)setContentSpacing:(CGFloat)contentSpacing { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (contentSpacing == _contentSpacing) { return; } @@ -267,14 +267,14 @@ - (void)setContentSpacing:(CGFloat)contentSpacing - (BOOL)laysOutHorizontally { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _laysOutHorizontally; } - (void)setLaysOutHorizontally:(BOOL)laysOutHorizontally { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (laysOutHorizontally == _laysOutHorizontally) { return; } @@ -287,49 +287,49 @@ - (void)setLaysOutHorizontally:(BOOL)laysOutHorizontally - (ASVerticalAlignment)contentVerticalAlignment { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _contentVerticalAlignment; } - (void)setContentVerticalAlignment:(ASVerticalAlignment)contentVerticalAlignment { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _contentVerticalAlignment = contentVerticalAlignment; } - (ASHorizontalAlignment)contentHorizontalAlignment { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _contentHorizontalAlignment; } - (void)setContentHorizontalAlignment:(ASHorizontalAlignment)contentHorizontalAlignment { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _contentHorizontalAlignment = contentHorizontalAlignment; } - (UIEdgeInsets)contentEdgeInsets { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _contentEdgeInsets; } - (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _contentEdgeInsets = contentEdgeInsets; } - (ASButtonNodeImageAlignment)imageAlignment { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _imageAlignment; } - (void)setImageAlignment:(ASButtonNodeImageAlignment)imageAlignment { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _imageAlignment = imageAlignment; } @@ -349,7 +349,7 @@ - (void)setTitle:(NSString *)title withFont:(UIFont *)font withColor:(UIColor *) - (NSAttributedString *)attributedTitleForState:(UIControlState)state { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); switch (state) { case UIControlStateNormal: return _normalAttributedTitle; @@ -374,7 +374,7 @@ - (NSAttributedString *)attributedTitleForState:(UIControlState)state - (void)setAttributedTitle:(NSAttributedString *)title forState:(UIControlState)state { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); switch (state) { case UIControlStateNormal: _normalAttributedTitle = [title copy]; @@ -406,7 +406,7 @@ - (void)setAttributedTitle:(NSAttributedString *)title forState:(UIControlState) - (UIImage *)imageForState:(UIControlState)state { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); switch (state) { case UIControlStateNormal: return _normalImage; @@ -431,7 +431,7 @@ - (UIImage *)imageForState:(UIControlState)state - (void)setImage:(UIImage *)image forState:(UIControlState)state { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); switch (state) { case UIControlStateNormal: _normalImage = image; @@ -463,7 +463,7 @@ - (void)setImage:(UIImage *)image forState:(UIControlState)state - (UIImage *)backgroundImageForState:(UIControlState)state { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); switch (state) { case UIControlStateNormal: return _normalBackgroundImage; @@ -488,7 +488,7 @@ - (UIImage *)backgroundImageForState:(UIControlState)state - (void)setBackgroundImage:(UIImage *)image forState:(UIControlState)state { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); switch (state) { case UIControlStateNormal: _normalBackgroundImage = image; @@ -525,7 +525,7 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize ASLayoutSpec *spec; ASStackLayoutSpec *stack = [[ASStackLayoutSpec alloc] init]; { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); stack.direction = _laysOutHorizontally ? ASStackLayoutDirectionHorizontal : ASStackLayoutDirectionVertical; stack.spacing = _contentSpacing; stack.horizontalAlignment = _contentHorizontalAlignment; diff --git a/Source/ASCGImageBuffer.h b/Source/ASCGImageBuffer.h new file mode 100644 index 000000000..88f1fd9c3 --- /dev/null +++ b/Source/ASCGImageBuffer.h @@ -0,0 +1,32 @@ +// +// ASCGImageBuffer.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASCGImageBuffer : NSObject + +/// Init a zero-filled buffer with the given length. +- (instancetype)initWithLength:(NSUInteger)length; + +@property (readonly) void *mutableBytes NS_RETURNS_INNER_POINTER; + +/// Don't do any drawing or call any methods after calling this. +- (CGDataProviderRef)createDataProviderAndInvalidate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ASCGImageBuffer.m b/Source/ASCGImageBuffer.m new file mode 100644 index 000000000..8b236f65c --- /dev/null +++ b/Source/ASCGImageBuffer.m @@ -0,0 +1,92 @@ +// +// ASCGImageBuffer.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASCGImageBuffer.h" + +#import +#import +#import +#import + +/** + * The behavior of this class is modeled on the private function + * _CGDataProviderCreateWithCopyOfData, which is the function used + * by CGBitmapContextCreateImage. + * + * If the buffer is larger than a page, we use mmap and mark it as + * read-only when they are finished drawing. Then we wrap the VM + * in an NSData + */ +@implementation ASCGImageBuffer { + BOOL _createdData; + BOOL _isVM; + NSUInteger _length; +} + +- (instancetype)initWithLength:(NSUInteger)length +{ + if (self = [super init]) { + _length = length; + _isVM = (length >= vm_page_size); + if (_isVM) { + _mutableBytes = mmap(NULL, length, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, VM_MAKE_TAG(VM_MEMORY_COREGRAPHICS_DATA), 0); + if (_mutableBytes == MAP_FAILED) { + NSAssert(NO, @"Failed to map for CG image data."); + _isVM = NO; + } + } + + // Check the VM flag again because we may have failed above. + if (!_isVM) { + _mutableBytes = calloc(1, length); + } + } + return self; +} + +- (void)dealloc +{ + if (!_createdData) { + [ASCGImageBuffer deallocateBuffer:_mutableBytes length:_length isVM:_isVM]; + } +} + +- (CGDataProviderRef)createDataProviderAndInvalidate +{ + NSAssert(!_createdData, @"Should not create data provider from buffer multiple times."); + _createdData = YES; + + // Mark the pages as read-only. + if (_isVM) { + __unused kern_return_t result = vm_protect(mach_task_self(), (vm_address_t)_mutableBytes, _length, true, VM_PROT_READ); + NSAssert(result == noErr, @"Error marking buffer as read-only: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]); + } + + // Wrap in an NSData + BOOL isVM = _isVM; + NSData *d = [[NSData alloc] initWithBytesNoCopy:_mutableBytes length:_length deallocator:^(void * _Nonnull bytes, NSUInteger length) { + [ASCGImageBuffer deallocateBuffer:bytes length:length isVM:isVM]; + }]; + return CGDataProviderCreateWithCFData((__bridge CFDataRef)d); +} + ++ (void)deallocateBuffer:(void *)buf length:(NSUInteger)length isVM:(BOOL)isVM +{ + if (isVM) { + __unused kern_return_t result = vm_deallocate(mach_task_self(), (vm_address_t)buf, length); + NSAssert(result == noErr, @"Failed to unmap cg image buffer: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]); + } else { + free(buf); + } +} + +@end diff --git a/Source/ASCellNode.h b/Source/ASCellNode.h index 2c911f17a..87f72cf5c 100644 --- a/Source/ASCellNode.h +++ b/Source/ASCellNode.h @@ -190,6 +190,12 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { */ @property (nonatomic) UITableViewCellSelectionStyle selectionStyle; +/* @abstract The focus style when a cell is focused + * @default UITableViewCellFocusStyleDefault + * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. + */ +@property (nonatomic) UITableViewCellFocusStyle focusStyle; + /* @abstract The view used as the background of the cell when it is selected. * ASTableView uses these properties when configuring UITableViewCells that host ASCellNodes. * ASCollectionView uses these properties when configuring UICollectionViewCells that host ASCellNodes. @@ -215,7 +221,7 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { - (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock __unavailable; -- (void)setLayerBacked:(BOOL)layerBacked AS_UNAVAILABLE("ASCellNode does not support layer-backing"); +- (void)setLayerBacked:(BOOL)layerBacked AS_UNAVAILABLE("ASCellNode does not support layer-backing, although subnodes may be layer-backed."); @end diff --git a/Source/ASCellNode.mm b/Source/ASCellNode.mm index 400be0796..c93da2db6 100644 --- a/Source/ASCellNode.mm +++ b/Source/ASCellNode.mm @@ -58,6 +58,7 @@ - (instancetype)init // Use UITableViewCell defaults _selectionStyle = UITableViewCellSelectionStyleDefault; + _focusStyle = UITableViewCellFocusStyleDefault; self.clipsToBounds = YES; return self; @@ -87,7 +88,7 @@ - (void)didLoad if ([_viewController isKindOfClass:[ASViewController class]]) { ASViewController *asViewController = (ASViewController *)_viewController; _viewControllerNode = asViewController.node; - [_viewController view]; + [_viewController loadViewIfNeeded]; } else { // Careful to avoid retain cycle UIViewController *viewController = _viewController; @@ -351,6 +352,25 @@ - (BOOL)supportsLayerBacking return NO; } +- (BOOL)shouldUseUIKitCell +{ + return NO; +} + +@end + + +#pragma mark - +#pragma mark ASWrapperCellNode + +// TODO: Consider if other calls, such as willDisplayCell, should be bridged to this class. +@implementation ASWrapperCellNode : ASCellNode + +- (BOOL)shouldUseUIKitCell +{ + return YES; +} + @end diff --git a/Source/ASCollectionNode+Beta.h b/Source/ASCollectionNode+Beta.h index 952227896..2c8163b71 100644 --- a/Source/ASCollectionNode+Beta.h +++ b/Source/ASCollectionNode+Beta.h @@ -57,6 +57,19 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL usesSynchronousDataLoading; +/** + * Returns YES if the ASCollectionNode contents are completely synchronized with the underlying collection-view layout. + */ +@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized; + +/** + * Schedules a block to be performed (on the main thread) as soon as the completion block is called + * on performBatchUpdates:. + * + * When isSynchronized == YES, the block is run block immediately (before the method returns). + */ +- (void)onDidFinishSynchronizing:(void (^)(void))didFinishSynchronizing; + - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(nullable id)layoutFacilitator; - (instancetype)initWithLayoutDelegate:(id)layoutDelegate layoutFacilitator:(nullable id)layoutFacilitator; diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index d60fd57ef..b25372efc 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -109,6 +109,30 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL allowsMultipleSelection; +/** + * A Boolean value that determines whether bouncing always occurs when vertical scrolling reaches the end of the content. + * The default value of this property is NO. + */ +@property (nonatomic, assign) BOOL alwaysBounceVertical; + +/** + * A Boolean value that determines whether bouncing always occurs when horizontal scrolling reaches the end of the content view. + * The default value of this property is NO. + */ +@property (nonatomic, assign) BOOL alwaysBounceHorizontal; + +/** + * A Boolean value that controls whether the vertical scroll indicator is visible. + * The default value of this property is YES. + */ +@property (nonatomic, assign) BOOL showsVerticalScrollIndicator; + +/** + * A Boolean value that controls whether the horizontal scroll indicator is visible. + * The default value of this property is NO. + */ +@property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; + /** * The layout used to organize the node's items. * @@ -244,7 +268,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. @@ -255,7 +279,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:. @@ -284,7 +308,7 @@ NS_ASSUME_NONNULL_BEGIN * * Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks. */ -- (void)onDidFinishProcessingUpdates:(nullable void (^)())didFinishProcessingUpdates; +- (void)onDidFinishProcessingUpdates:(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. @@ -382,7 +406,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; /** @@ -630,6 +654,32 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSArray *)collectionNode:(ASCollectionNode *)collectionNode supplementaryElementKindsInSection:(NSInteger)section; +/** + * Asks the data source if it's possible to move the specified item interactively. + * + * See @p -[UICollectionViewDataSource collectionView:canMoveItemAtIndexPath:] @c. + * + * @param collectionNode The sender. + * @param node The display node for the item that may be moved. + * + * @return Whether the item represented by @p node may be moved. + */ +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode canMoveItemWithNode:(ASCellNode *)node; + +/** + * Called when the user has interactively moved an item. The data source + * should update its internal data store to reflect the move. Note that you + * should not call [collectionNode moveItemAtIndexPath:toIndexPath:] – the + * collection node's internal state will be updated automatically. + * + * * See @p -[UICollectionViewDataSource collectionView:moveItemAtIndexPath:toIndexPath:] @c. + * + * @param collectionNode The sender. + * @param sourceIndexPath The original item index path. + * @param destinationIndexPath The new item index path. + */ +- (void)collectionNode:(ASCollectionNode *)collectionNode moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; + /** * Similar to -collectionView:cellForItemAtIndexPath:. * diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 2449377fd..a7f9e9bf7 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -49,9 +49,13 @@ @interface _ASCollectionPendingState : NSObject @property (nonatomic, assign) BOOL usesSynchronousDataLoading; @property (nonatomic, assign) CGFloat leadingScreensForBatching; @property (weak, nonatomic) id layoutInspector; +@property (nonatomic, assign) BOOL alwaysBounceVertical; +@property (nonatomic, assign) BOOL alwaysBounceHorizontal; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) CGPoint contentOffset; @property (nonatomic, assign) BOOL animatesContentOffset; +@property (nonatomic, assign) BOOL showsVerticalScrollIndicator; +@property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; @end @implementation _ASCollectionPendingState @@ -172,6 +176,18 @@ - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionVi return self; } +#if ASDISPLAYNODE_ASSERTIONS_ENABLED +- (void)dealloc +{ + if (self.nodeLoaded) { + __weak UIView *view = self.view; + ASPerformBlockOnMainThread(^{ + ASDisplayNodeCAssertNil(view.superview, @"Node's view should be removed from hierarchy."); + }); + } +} +#endif + #pragma mark ASDisplayNode - (void)didLoad @@ -191,13 +207,28 @@ - (void)didLoad view.allowsMultipleSelection = pendingState.allowsMultipleSelection; view.usesSynchronousDataLoading = pendingState.usesSynchronousDataLoading; view.layoutInspector = pendingState.layoutInspector; - view.contentInset = pendingState.contentInset; - + + // Only apply these flags if they're enabled; the view might come with them turned on. + if (pendingState.alwaysBounceVertical) { + view.alwaysBounceVertical = YES; + } + if (pendingState.alwaysBounceHorizontal) { + view.alwaysBounceHorizontal = YES; + } + + UIEdgeInsets contentInset = pendingState.contentInset; + if (!UIEdgeInsetsEqualToEdgeInsets(contentInset, UIEdgeInsetsZero)) { + view.contentInset = contentInset; + } + + CGPoint contentOffset = pendingState.contentOffset; + if (!CGPointEqualToPoint(contentOffset, CGPointZero)) { + [view setContentOffset:contentOffset animated:pendingState.animatesContentOffset]; + } + if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; } - - [view setContentOffset:pendingState.contentOffset animated:pendingState.animatesContentOffset]; // Don't need to set collectionViewLayout to the view as the layout was already used to init the view in view block. } @@ -223,10 +254,11 @@ - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfac - (void)didEnterPreloadState { [super didEnterPreloadState]; + // ASCollectionNode is often nested inside of other collections. In this case, ASHierarchyState's RangeManaged bit will be set. // Intentionally allocate the view here and trigger a layout pass on it, which in turn will trigger the intial data load. // We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view. // TODO (ASCL) If this node supports async layout, kick off the initial data load without allocating the view - if (CGRectEqualToRect(self.bounds, CGRectZero) == NO) { + if (ASHierarchyStateIncludesRangeManaged(self.hierarchyState) && CGRectEqualToRect(self.bounds, CGRectZero) == NO) { [[self view] layoutIfNeeded]; } } @@ -423,6 +455,82 @@ - (BOOL)allowsMultipleSelection } } +- (void)setAlwaysBounceVertical:(BOOL)alwaysBounceVertical +{ + if ([self pendingState]) { + _pendingState.alwaysBounceVertical = alwaysBounceVertical; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.alwaysBounceVertical = alwaysBounceVertical; + } +} + +- (BOOL)alwaysBounceVertical +{ + if ([self pendingState]) { + return _pendingState.alwaysBounceVertical; + } else { + return self.view.alwaysBounceVertical; + } +} + +- (void)setAlwaysBounceHorizontal:(BOOL)alwaysBounceHorizontal +{ + if ([self pendingState]) { + _pendingState.alwaysBounceHorizontal = alwaysBounceHorizontal; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.alwaysBounceHorizontal = alwaysBounceHorizontal; + } +} + +- (BOOL)alwaysBounceHorizontal +{ + if ([self pendingState]) { + return _pendingState.alwaysBounceHorizontal; + } else { + return self.view.alwaysBounceHorizontal; + } +} + +- (void)setShowsVerticalScrollIndicator:(BOOL)showsVerticalScrollIndicator +{ + if ([self pendingState]) { + _pendingState.showsVerticalScrollIndicator = showsVerticalScrollIndicator; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.showsVerticalScrollIndicator = showsVerticalScrollIndicator; + } +} + +- (BOOL)showsVerticalScrollIndicator +{ + if ([self pendingState]) { + return _pendingState.showsVerticalScrollIndicator; + } else { + return self.view.showsVerticalScrollIndicator; + } +} + +- (void)setShowsHorizontalScrollIndicator:(BOOL)showsHorizontalScrollIndicator +{ + if ([self pendingState]) { + _pendingState.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator; + } else { + ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist"); + self.view.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator; + } +} + +- (BOOL)showsHorizontalScrollIndicator +{ + if ([self pendingState]) { + return _pendingState.showsHorizontalScrollIndicator; + } else { + return self.view.showsHorizontalScrollIndicator; + } +} + - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout { if ([self pendingState]) { @@ -733,8 +841,11 @@ - (BOOL)isProcessingUpdates return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO); } -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +- (void)onDidFinishProcessingUpdates:(void (^)())completion { + if (!completion) { + return; + } if (!self.nodeLoaded) { completion(); } else { @@ -742,6 +853,23 @@ - (void)onDidFinishProcessingUpdates:(nullable void (^)())completion } } +- (BOOL)isSynchronized +{ + return (self.nodeLoaded ? [self.view isSynchronized] : YES); +} + +- (void)onDidFinishSynchronizing:(void (^)())completion +{ + if (!completion) { + return; + } + if (!self.nodeLoaded) { + completion(); + } else { + [self.view onDidFinishSynchronizing:completion]; + } +} + - (void)waitUntilAllUpdatesAreProcessed { ASDisplayNodeAssertMainThread(); diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index ba9dda296..594d88888 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -256,7 +256,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. @@ -267,7 +267,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. @@ -276,7 +276,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. @@ -296,9 +296,15 @@ 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:(void (^)(void))completion; - (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASCollectionNode waitUntilAllUpdatesAreProcessed] instead."); +/** + * See ASCollectionNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized; +- (void)onDidFinishSynchronizing:(void (^)(void))completion; + /** * Registers the given kind of supplementary node for use in creating node-backed supplementary views. * diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 7c20dbddf..44a6ca6e0 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -64,8 +64,6 @@ return __val; \ } -#define ASIndexPathForSection(section) [NSIndexPath indexPathForItem:0 inSection:section] - #define ASFlowLayoutDefault(layout, property, default) \ ({ \ UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ @@ -103,6 +101,10 @@ @interface ASCollectionView () *_cellsForLayoutUpdates; id _layoutFacilitator; CGFloat _leadingScreensForBatching; + + // When we update our data controller in response to an interactive move, + // we don't want to tell the collection view about the change (it knows!) + BOOL _updatingInResponseToInteractiveMove; BOOL _inverted; NSUInteger _superBatchUpdateCount; @@ -123,15 +125,6 @@ @interface ASCollectionView () = 9. - */ - CALayer *_retainedLayer; - /** * If YES, the `UICollectionView` will reload its data on next layout pass so we should not forward any updates to it. @@ -158,7 +151,12 @@ @interface ASCollectionView () )dataSource { // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. We also allow this when we're doing interop. @@ -453,6 +462,8 @@ - (void)setAsyncDataSource:(id)asyncDataSource _asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForSupplementaryElementOfKind:atIndexPath:)]; _asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:supplementaryElementKindsInSection:)]; _asyncDataSourceFlags.nodeModelForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeModelForItemAtIndexPath:)]; + _asyncDataSourceFlags.collectionNodeCanMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:canMoveItemWithNode:)]; + _asyncDataSourceFlags.collectionNodeMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:moveItemAtIndexPath:toIndexPath:)]; _asyncDataSourceFlags.interop = [_asyncDataSource conformsToProtocol:@protocol(ASCollectionDataSourceInterop)]; if (_asyncDataSourceFlags.interop) { @@ -476,6 +487,7 @@ - (void)setAsyncDataSource:(id)asyncDataSource if (_layoutInspectorFlags.didChangeCollectionViewDataSource) { [layoutInspector didChangeCollectionViewDataSource:asyncDataSource]; } + [self _asyncDelegateOrDataSourceDidChange]; } - (id)asyncDelegate @@ -538,6 +550,8 @@ - (void)setAsyncDelegate:(id)asyncDelegate _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; @@ -555,6 +569,15 @@ - (void)setAsyncDelegate:(id)asyncDelegate if (_layoutInspectorFlags.didChangeCollectionViewDelegate) { [layoutInspector didChangeCollectionViewDelegate:asyncDelegate]; } + [self _asyncDelegateOrDataSourceDidChange]; +} + +- (void)_asyncDelegateOrDataSourceDidChange +{ + ASDisplayNodeAssertMainThread(); + if (_asyncDataSource == nil && _asyncDelegate == nil) { + [_dataController clearData]; + } } - (void)setCollectionViewLayout:(nonnull UICollectionViewLayout *)collectionViewLayout @@ -641,18 +664,21 @@ - (BOOL)zeroContentInsets - (CGSize)sizeForElement:(ASCollectionElement *)element { ASDisplayNodeAssertMainThread(); - if (element == nil) { + ASCellNode *node = element.node; + if (element == nil || node == nil) { return CGSizeZero; } - ASCellNode *node = element.node; BOOL useUIKitCell = node.shouldUseUIKitCell; if (useUIKitCell) { - // In this case, we should use the exact value that was stashed earlier by calling sizeForItem:, referenceSizeFor*, etc. - // Although the node would use the preferredSize in layoutThatFits, we can skip this because there's no constrainedSize. - ASDisplayNodeAssert([node.superclass isSubclassOfClass:[ASCellNode class]] == NO, - @"Placeholder cells for UIKit passthrough should be generic ASCellNodes: %@", node); - return node.style.preferredSize; + ASWrapperCellNode *wrapperNode = (ASWrapperCellNode *)node; + if (wrapperNode.sizeForItemBlock) { + return wrapperNode.sizeForItemBlock(wrapperNode, element.constrainedSize.max); + } else { + // In this case, we should use the exact value that was stashed earlier by calling sizeForItem:, referenceSizeFor*, etc. + // Although the node would use the preferredSize in layoutThatFits, we can skip this because there's no constrainedSize. + return wrapperNode.style.preferredSize; + } } else { return [node layoutThatFits:element.constrainedSize].size; } @@ -676,19 +702,13 @@ - (NSIndexPath *)convertIndexPathFromCollectionNode:(NSIndexPath *)indexPath wai 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; } /** @@ -722,13 +742,7 @@ - (NSIndexPath *)convertIndexPathToCollectionNode:(NSIndexPath *)indexPath 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 @@ -790,7 +804,11 @@ - (void)invalidateFlowLayoutDelegateMetrics { // 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]; + ASWrapperCellNode *wrapperNode = (ASWrapperCellNode *)node; + if (wrapperNode.sizeForItemBlock) { + continue; + } + NSIndexPath *indexPath = [_dataController.pendingMap indexPathForElement:element]; NSString *kind = [element supplementaryElementKind]; CGSize previousSize = node.style.preferredSize; CGSize size = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath]; @@ -827,7 +845,7 @@ - (CGSize)_sizeForUIKitCellWithKind:(NSString *)kind atIndexPath:(NSIndexPath *) 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]) { + if (indexPath && [_asyncDelegate respondsToSelector:sizeForItem]) { size = [(id)_asyncDelegate collectionView:self layout:l sizeForItemAtIndexPath:indexPath]; } else { size = ASFlowLayoutDefault(l, itemSize, CGSizeZero); @@ -835,7 +853,7 @@ - (CGSize)_sizeForUIKitCellWithKind:(NSString *)kind atIndexPath:(NSIndexPath *) } 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]) { + if (indexPath && [_asyncDelegate respondsToSelector:sizeForHeader]) { size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForHeaderInSection:indexPath.section]; } else { size = ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); @@ -843,7 +861,7 @@ - (CGSize)_sizeForUIKitCellWithKind:(NSString *)kind atIndexPath:(NSIndexPath *) } 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]) { + if (indexPath && [_asyncDelegate respondsToSelector:sizeForFooter]) { size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForFooterInSection:indexPath.section]; } else { size = ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); @@ -1047,7 +1065,7 @@ - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout * 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); } @@ -1057,29 +1075,28 @@ - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout * 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 { - 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); } @@ -1087,10 +1104,10 @@ - (UIEdgeInsets)collectionView:(UICollectionView *)cv layout:(UICollectionViewLa - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l 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 } @@ -1098,10 +1115,10 @@ - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l 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 } @@ -1115,9 +1132,12 @@ - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView UICollectionReusableView *view = nil; ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:kind atIndexPath:indexPath]; ASCellNode *node = element.node; + ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil); + BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interopViewForSupplementaryElement && wrapperNode); - BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopViewForSupplementaryElement && (_asyncDataSourceFlags.interopAlwaysDequeue || node.shouldUseUIKitCell); - if (shouldDequeueExternally) { + if (wrapperNode.viewForSupplementaryBlock) { + view = wrapperNode.viewForSupplementaryBlock(wrapperNode); + } else if (shouldDequeueExternally) { // This codepath is used for both IGListKit mode, and app-level UICollectionView interop. view = [(id)_asyncDataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } else { @@ -1141,15 +1161,19 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell UICollectionViewCell *cell = nil; ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; ASCellNode *node = element.node; + ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil); + BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interop && wrapperNode); - BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interop && node.shouldUseUIKitCell); - if (shouldDequeueExternally) { + if (wrapperNode.cellForItemBlock) { + cell = wrapperNode.cellForItemBlock(wrapperNode); + } else if (shouldDequeueExternally) { cell = [(id)_asyncDataSource collectionView:collectionView cellForItemAtIndexPath:indexPath]; } else { cell = [self dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; } ASDisplayNodeAssert(element != nil, @"Element should exist. indexPath = %@, collectionDataSource = %@", indexPath, self); + ASDisplayNodeAssert(cell != nil, @"UICollectionViewCell must not be nil. indexPath = %@, collectionDataSource = %@", indexPath, self); if (_ASCollectionViewCell *asCell = ASDynamicCastStrict(cell, _ASCollectionViewCell)) { asCell.element = element; @@ -1163,9 +1187,8 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICol { if (_asyncDelegateFlags.interopWillDisplayCell) { ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:modelIndexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; } } @@ -1187,7 +1210,7 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICol 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; @@ -1224,9 +1247,8 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( { if (_asyncDelegateFlags.interopDidEndDisplayingCell) { ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:modelIndexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; } } @@ -1269,10 +1291,9 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)rawView forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopWillDisplaySupplementaryView) { - ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:modelIndexPath]; + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:indexPath]; } } @@ -1310,10 +1331,9 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementa - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)rawView forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopdidEndDisplayingSupplementaryView) { - ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; - NSIndexPath *modelIndexPath = [self indexPathForNode:node]; - if (modelIndexPath && node.shouldUseUIKitCell) { - [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:modelIndexPath]; + ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; + if (node.shouldUseUIKitCell) { + [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:indexPath]; } } @@ -1506,16 +1526,72 @@ - (void)collectionView:(UICollectionView *)collectionView performAction:(nonnull } } +- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath +{ + // Mimic UIKit's gating logic. + // If the data source doesn't support moving, then all bets are off. + if (!_asyncDataSourceFlags.collectionNodeMoveItem) { + return NO; + } + + // Currently we do not support interactive moves when using async layout. The reason is, we do not have a mechanism + // to propagate the "presentation data" element map (containing the speculative in-progress moves) to the layout delegate, + // and this can cause exceptions to be thrown from UICV. For example, if you drag an item out of a section, + // the element map will still contain N items in that section, even though there's only N-1 shown, and UICV will + // throw an exception that you specified an element that doesn't exist. + // + // In iOS >= 11, this is made much easier by the UIDataSourceTranslating API. In previous versions of iOS our best bet + // would be to capture the invalidation contexts that are sent during interactive moves and make our own data source translator. + if ([self.collectionViewLayout isKindOfClass:[ASCollectionLayout class]]) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + as_log_debug(ASCollectionLog(), "Collection node item interactive movement is not supported when using a layout delegate. This message will only be logged once. Node: %@", ASObjectDescriptionMakeTiny(self)); + }); + return NO; + } + + // If the data source implements canMoveItem, let them decide. + if (_asyncDataSourceFlags.collectionNodeCanMoveItem) { + if (auto cellNode = [self nodeForItemAtIndexPath:indexPath]) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + return [_asyncDataSource collectionNode:collectionNode canMoveItemWithNode:cellNode]; + } + } + + // Otherwise allow the move for all items. + return YES; +} + +- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath +{ + ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeMoveItem, @"Should not allow interactive collection item movement if data source does not support it."); + + // Inform the data source first, in case they call nodeForItemAtIndexPath:. + // We want to make sure we return them the node for the item they have in mind. + if (auto collectionNode = self.collectionNode) { + [_asyncDataSource collectionNode:collectionNode moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + } + + // Now we update our data controller's store. + // Get up to date + [self waitUntilAllUpdatesAreCommitted]; + // Set our flag to suppress informing super about the change. + ASDisplayNodeAssertFalse(_updatingInResponseToInteractiveMove); + _updatingInResponseToInteractiveMove = YES; + // Submit the move + [self moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + // Wait for it to finish – should be fast! + [self waitUntilAllUpdatesAreCommitted]; + // Clear the flag + _updatingInResponseToInteractiveMove = NO; +} + - (void)scrollViewDidScroll:(UIScrollView *)scrollView { - // If a scroll happenes the current range mode needs to go to full ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; if (ASInterfaceStateIncludesVisible(interfaceState)) { - _rangeController.contentOffsetHasChanged = YES; - [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; [self _checkForBatchFetching]; } - for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { // _cellsForVisibilityUpdates only includes cells for ASCellNode subclasses with overrides of the visibility method. [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView]; @@ -1554,6 +1630,10 @@ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + // If a scroll happens the current range mode needs to go to full + _rangeController.contentHasBeenScrolled = YES; + [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; + for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging inScrollView:scrollView]; } @@ -1586,7 +1666,12 @@ - (void)setInverted:(BOOL)inverted - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { - _leadingScreensForBatching = leadingScreensForBatching; + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + ASPerformBlockOnMainThread(^{ + [self _checkForBatchFetching]; + }); + } } - (CGFloat)leadingScreensForBatching @@ -1777,39 +1862,32 @@ - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAt if (_asyncDataSourceFlags.collectionNodeNodeBlockForItem) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); block = [_asyncDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]; - } else if (_asyncDataSourceFlags.collectionNodeNodeForItem) { + } + if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForItem) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); cell = [_asyncDataSource collectionNode:collectionNode nodeForItemAtIndexPath:indexPath]; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - } else if (_asyncDataSourceFlags.collectionViewNodeBlockForItem) { + if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeBlockForItem) { block = [_asyncDataSource collectionView:self nodeBlockForItemAtIndexPath:indexPath]; - } else if (_asyncDataSourceFlags.collectionViewNodeForItem) { + } + if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForItem) { cell = [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath]; } #pragma clang diagnostic pop - // Handle nil node block or cell - if (cell && [cell isKindOfClass:[ASCellNode class]]) { - block = ^{ - return cell; - }; - } - if (block == nil) { - if (_asyncDataSourceFlags.interop) { - CGSize preferredSize = [self _sizeForUIKitCellWithKind:nil atIndexPath:indexPath]; - block = ^{ - ASCellNode *node = [[ASCellNode alloc] init]; - node.shouldUseUIKitCell = YES; - node.style.preferredSize = preferredSize; - return node; - }; - } else { - ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the protocol!", indexPath, cell, block); - block = ^{ - return [[ASCellNode alloc] init]; - }; + if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) { + // In this case, either the client is expecting a UIKit passthrough cell to be created automatically, + // or it is an error. + if (_asyncDataSourceFlags.interop) { + cell = [[ASWrapperCellNode alloc] init]; + cell.style.preferredSize = [self _sizeForUIKitCellWithKind:nil atIndexPath:indexPath]; + } else { + ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the protocol!", indexPath, cell, block); + cell = [[ASCellNode alloc] init]; + } } } @@ -1817,17 +1895,18 @@ - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAt __weak __typeof__(self) weakSelf = self; return ^{ __typeof__(self) strongSelf = weakSelf; - ASCellNode *node = (block != nil ? block() : [[ASCellNode alloc] init]); + ASCellNode *node = (block ? block() : cell); + ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"ASCollectionNode provided a non-ASCellNode! %@, %@", node, strongSelf); [node enterHierarchyState:ASHierarchyStateRangeManaged]; + if (node.interactionDelegate == nil) { node.interactionDelegate = strongSelf; } if (strongSelf.inverted) { - node.transform = CATransform3DMakeScale(1, -1, 1) ; + node.transform = CATransform3DMakeScale(1, -1, 1); } return node; }; - return block; } - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section @@ -1872,45 +1951,50 @@ - (BOOL)dataController:(ASDataController *)dataController presentedSizeForElemen #pragma mark - ASDataControllerSource optional methods -- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout { ASDisplayNodeAssertMainThread(); - ASCellNodeBlock nodeBlock = nil; - ASCellNode *node = nil; + ASCellNodeBlock block = nil; + ASCellNode *cell = nil; if (_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); - nodeBlock = [_asyncDataSource collectionNode:collectionNode nodeBlockForSupplementaryElementOfKind:kind atIndexPath:indexPath]; - } else if (_asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) { + block = [_asyncDataSource collectionNode:collectionNode nodeBlockForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + } + if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); - node = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; - } else if (_asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) { + cell = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + } + if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - node = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + cell = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; #pragma clang diagnostic pop } - if (nodeBlock == nil) { - if (node) { - nodeBlock = ^{ return node; }; - } else { + if (block == nil) { + if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) { // 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; + // If the UIKit method is implemented, then we should use a passthrough cell. + // Otherwise the CGSizeZero default will cause UIKit to not show it (so this isn't an error like the cellForItem case). + BOOL useUIKitCell = _asyncDataSourceFlags.interopViewForSupplementaryElement; if (useUIKitCell) { - preferredSize = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath]; + cell = [[ASWrapperCellNode alloc] init]; + cell.style.preferredSize = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath]; + } else { + cell = [[ASCellNode alloc] init]; } - nodeBlock = ^{ - ASCellNode *node = [[ASCellNode alloc] init]; - node.shouldUseUIKitCell = useUIKitCell; - node.style.preferredSize = preferredSize; - return node; - }; } + + // This condition is intended to run for either cells received from the datasource, or created just above. + if (cell.shouldUseUIKitCell) { + *shouldAsyncLayout = NO; + } + + block = ^{ return cell; }; } - return nodeBlock; + return block; } - (NSArray *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections @@ -1981,28 +2065,6 @@ - (ASRangeController *)rangeController return _rangeController; } -/// The UIKit version of this method is only available on iOS >= 9 -- (NSArray *)asdk_indexPathsForVisibleSupplementaryElementsOfKind:(NSString *)kind -{ - if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_9_0) { - return [self indexPathsForVisibleSupplementaryElementsOfKind:kind]; - } - - // iOS 8 workaround - // We cannot use willDisplaySupplementaryView/didEndDisplayingSupplementaryView - // because those methods send index paths for _deleted items_ (invalid index paths) - [self layoutIfNeeded]; - NSArray *visibleAttributes = [self.collectionViewLayout layoutAttributesForElementsInRect:self.bounds]; - NSMutableArray *result = [NSMutableArray array]; - for (UICollectionViewLayoutAttributes *attributes in visibleAttributes) { - if (attributes.representedElementCategory == UICollectionElementCategorySupplementaryView - && [attributes.representedElementKind isEqualToString:kind]) { - [result addObject:attributes.indexPath]; - } - } - return result; -} - - (NSHashTable *)visibleElementsForRangeController:(ASRangeController *)rangeController { return ASPointerTableByFlatMapping(_visibleElements, id element, element); @@ -2033,7 +2095,7 @@ - (NSString *)nameForRangeControllerDataSource - (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates { ASDisplayNodeAssertMainThread(); - if (!self.asyncDataSource || _superIsPendingDataLoad) { + if (!self.asyncDataSource || _superIsPendingDataLoad || _updatingInResponseToInteractiveMove) { updates(); [changeSet executeCompletionHandlerWithFinished:NO]; return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes @@ -2068,6 +2130,15 @@ - (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet [_layoutFacilitator collectionViewWillPerformBatchUpdates]; __block NSUInteger numberOfUpdates = 0; + id completion = ^(BOOL finished){ + as_activity_scope(as_activity_create("Handle collection update completion", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_verbose(ASCollectionLog(), "Update animation finished %{public}@", self.collectionNode); + // Flush any range changes that happened as part of the update animations ending. + [_rangeController updateIfNeeded]; + [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates]; + [changeSet executeCompletionHandlerWithFinished:finished]; + }; + [self _superPerformBatchUpdates:^{ updates(); @@ -2100,14 +2171,8 @@ - (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet [super insertItemsAtIndexPaths:change.indexPaths]; numberOfUpdates++; } - } completion:^(BOOL finished){ - as_activity_scope(as_activity_create("Handle collection update completion", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); - as_log_verbose(ASCollectionLog(), "Update animation finished %{public}@", self.collectionNode); - // Flush any range changes that happened as part of the update animations ending. - [_rangeController updateIfNeeded]; - [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates]; - [changeSet executeCompletionHandlerWithFinished:finished]; - }]; + } completion:completion]; + as_log_debug(ASCollectionLog(), "Completed batch update %{public}@", self.collectionNode); // Flush any range changes that happened as part of submitting the update. @@ -2208,15 +2273,23 @@ - (void)didMoveToWindow { BOOL visible = (self.window != nil); ASDisplayNode *node = self.collectionNode; + BOOL rangeControllerUpdated = NO; + if (!visible && node.inHierarchy) { + if (![node supportsRangeManagedInterfaceState]) { + rangeControllerUpdated = YES; + // Exit CellNodes first before Collection to match UIKit behaviors (tear down bottom up). + // Although we have not yet cleared the interfaceState's Visible bit (this happens in __exitHierarchy), + // the ASRangeController will get the correct value from -interfaceStateForRangeController:. + [_rangeController updateRanges]; + } [node __exitHierarchy]; } // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their // their update in the layout pass - if (![node supportsRangeManagedInterfaceState]) { - [_rangeController setNeedsUpdate]; - [_rangeController updateIfNeeded]; + if (![node supportsRangeManagedInterfaceState] && !rangeControllerUpdated) { + [_rangeController updateRanges]; } // When we aren't visible, we will only fetch up to the visible area. Now that we are visible, @@ -2226,6 +2299,20 @@ - (void)didMoveToWindow } } +- (void)willMoveToSuperview:(UIView *)newSuperview +{ + if (self.superview == nil && newSuperview != nil) { + _keepalive_node = self.collectionNode; + } +} + +- (void)didMoveToSuperview +{ + if (self.superview == nil) { + _keepalive_node = nil; + } +} + #pragma mark ASCALayerExtendedDelegate /** @@ -2247,18 +2334,17 @@ - (void)didMoveToWindow */ - (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) { + CGSize newSize = newBounds.size; + CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes; + if (CGSizeEqualToSize(lastUsedSize, newSize)) { return; } - CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes; - if (CGSizeEqualToSize(lastUsedSize, 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 = newBounds.size; + + _lastBoundsSizeUsedForMeasuringNodes = newSize; // Laying out all nodes is expensive. // We only need to do this if the bounds changed in the non-scrollable direction. @@ -2266,16 +2352,14 @@ - (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds new // 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]; } } @@ -2291,21 +2375,4 @@ - (void)setPrefetchingEnabled:(BOOL)prefetchingEnabled return; } -#if ASDISPLAYNODE_ASSERTIONS_ENABLED // Remove implementations entirely for efficiency if not asserting. - -// intercepted due to not being supported by ASCollectionView (prevent bugs caused by usage) - -- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0) -{ - ASDisplayNodeAssert(![self.asyncDataSource respondsToSelector:_cmd], @"%@ is not supported by ASCollectionView - please remove or disable this data source method.", NSStringFromSelector(_cmd)); - return NO; -} - -- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath NS_AVAILABLE_IOS(9_0) -{ - ASDisplayNodeAssert(![self.asyncDataSource respondsToSelector:_cmd], @"%@ is not supported by ASCollectionView - please remove or disable this data source method.", NSStringFromSelector(_cmd)); -} - -#endif - @end diff --git a/Source/ASConfiguration.h b/Source/ASConfiguration.h new file mode 100644 index 000000000..b024bb7e9 --- /dev/null +++ b/Source/ASConfiguration.h @@ -0,0 +1,62 @@ +// +// ASConfiguration.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@protocol ASConfigurationDelegate; + +NS_ASSUME_NONNULL_BEGIN + +static NSInteger const ASConfigurationSchemaCurrentVersion = 1; + +AS_SUBCLASSING_RESTRICTED +@interface ASConfiguration : NSObject + +/** + * Initialize this configuration with the provided dictionary, + * or nil to create an empty configuration. + * + * The schema is located in `schemas/configuration.json`. + */ +- (instancetype)initWithDictionary:(nullable NSDictionary *)dictionary; + +/** + * The delegate for configuration-related events. + * Delegate methods are called from a serial queue. + */ +@property (nonatomic, strong, nullable) id delegate; + +/** + * The experimental features to enable in Texture. + * See ASExperimentalFeatures for functions to convert to/from a string array. + */ +@property (nonatomic) ASExperimentalFeatures experimentalFeatures; + +@end + +/** + * Implement this method in a category to make your + * configuration available to Texture. It will be read + * only once and copied. + * + * NOTE: To specify your configuration at compile-time, you can + * define AS_FIXED_CONFIG_JSON as a C-string of JSON. This method + * will then be implemented to parse that string and generate + * a configuration. + */ +@interface ASConfiguration (UserProvided) ++ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED; +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ASConfiguration.m b/Source/ASConfiguration.m new file mode 100644 index 000000000..93c8c8fb0 --- /dev/null +++ b/Source/ASConfiguration.m @@ -0,0 +1,67 @@ +// +// ASConfiguration.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +/// Not too performance-sensitive here. + +/// Get this from C++, without the extra exception handling. +#define autotype __auto_type + +@implementation ASConfiguration + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ + if (self = [super init]) { + autotype featureStrings = ASDynamicCast(dictionary[@"experimental_features"], NSArray); + autotype version = ASDynamicCast(dictionary[@"version"], NSNumber).integerValue; + if (version != ASConfigurationSchemaCurrentVersion) { + NSLog(@"Texture warning: configuration schema is old version (%zd vs %zd)", version, ASConfigurationSchemaCurrentVersion); + } + self.experimentalFeatures = ASExperimentalFeaturesFromArray(featureStrings); + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = self.experimentalFeatures; + config.delegate = self.delegate; + return config; +} + +@end + +//#define AS_FIXED_CONFIG_JSON "{ \"version\" : 1, \"experimental_features\": [ \"exp_text_node\" ] }" + +#ifdef AS_FIXED_CONFIG_JSON + +@implementation ASConfiguration (UserProvided) + ++ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED +{ + NSData *data = [@AS_FIXED_CONFIG_JSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSDictionary *d = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; + if (!d) { + NSAssert(NO, @"Error parsing fixed config string '%s': %@", AS_FIXED_CONFIG_JSON, error); + return nil; + } else { + return [[ASConfiguration alloc] initWithDictionary:d]; + } +} + +@end + +#endif // AS_FIXED_CONFIG_JSON diff --git a/Source/ASConfigurationDelegate.h b/Source/ASConfigurationDelegate.h new file mode 100644 index 000000000..88055f037 --- /dev/null +++ b/Source/ASConfigurationDelegate.h @@ -0,0 +1,31 @@ +// +// ASConfigurationDelegate.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Used to communicate configuration-related events to the client. + */ +@protocol ASConfigurationDelegate + +/** + * Texture performed its first behavior related to the feature(s). + * This can be useful for tracking the impact of the behavior (A/B testing). + */ +- (void)textureDidActivateExperimentalFeatures:(ASExperimentalFeatures)features; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ASConfigurationInternal.h b/Source/ASConfigurationInternal.h new file mode 100644 index 000000000..08cd0580d --- /dev/null +++ b/Source/ASConfigurationInternal.h @@ -0,0 +1,42 @@ +// +// ASConfigurationInternal.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +/// Note this has to be public because it's imported by public header ASThread.h =/ +/// It will be private again after exp_unfair_lock ends. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN +ASDISPLAYNODE_EXTERN_C_BEGIN + +/** + * Quickly check if an experiment is enabled and notify the delegate + * that it's been activated. + * + * The delegate will be notified asynchronously. + */ +BOOL ASActivateExperimentalFeature(ASExperimentalFeatures option); + +AS_SUBCLASSING_RESTRICTED +@interface ASConfigurationManager : NSObject + + +/** + * No API for now. + * Just use ASActivateExperimentalFeature to access this efficiently. + */ + +@end + +NS_ASSUME_NONNULL_END +ASDISPLAYNODE_EXTERN_C_END diff --git a/Source/ASConfigurationInternal.m b/Source/ASConfigurationInternal.m new file mode 100644 index 000000000..a11e24229 --- /dev/null +++ b/Source/ASConfigurationInternal.m @@ -0,0 +1,95 @@ +// +// ASConfigurationInternal.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASConfigurationInternal.h" +#import +#import +#import + +#define ASGetSharedConfigMgr() (__bridge ASConfigurationManager *)ASConfigurationManager.sharedInstance + +@implementation ASConfigurationManager { + ASConfiguration *_config; + dispatch_queue_t _delegateQueue; + _Atomic(ASExperimentalFeatures) _activatedExperiments; +} + +/// Return CFTypeRef to avoid retain/release on this singleton. ++ (CFTypeRef)sharedInstance +{ + static CFTypeRef inst; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + inst = (__bridge_retained CFTypeRef)[[ASConfigurationManager alloc] init]; + }); + return inst; +} + ++ (ASConfiguration *)defaultConfiguration NS_RETURNS_RETAINED +{ + ASConfiguration *config = [[ASConfiguration alloc] init]; + // TODO(wsdwsd0829): Fix #788 before enabling it. + // config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing; + return config; +} + +- (instancetype)init +{ + if (self = [super init]) { + _delegateQueue = dispatch_queue_create("org.TextureGroup.Texture.ConfigNotifyQueue", DISPATCH_QUEUE_SERIAL); + if ([ASConfiguration respondsToSelector:@selector(textureConfiguration)]) { + _config = [[ASConfiguration textureConfiguration] copy]; + } else { + _config = [ASConfigurationManager defaultConfiguration]; + } + } + return self; +} + +- (BOOL)activateExperimentalFeature:(ASExperimentalFeatures)requested +{ + if (_config == nil) { + return NO; + } + + NSAssert(__builtin_popcount(requested) == 1, @"Cannot activate multiple features at once with this method."); + + // If they're disabled, ignore them. + ASExperimentalFeatures enabled = requested & _config.experimentalFeatures; + ASExperimentalFeatures prevTriggered = atomic_fetch_or(&_activatedExperiments, enabled); + ASExperimentalFeatures newlyTriggered = enabled & ~prevTriggered; + + // Notify delegate if needed. + if (newlyTriggered != 0) { + __unsafe_unretained id del = _config.delegate; + dispatch_async(_delegateQueue, ^{ + [del textureDidActivateExperimentalFeatures:newlyTriggered]; + }); + } + + return (enabled != 0); +} + +// Define this even when !DEBUG, since we may run our tests in release mode. ++ (void)test_resetWithConfiguration:(ASConfiguration *)configuration +{ + ASConfigurationManager *inst = ASGetSharedConfigMgr(); + inst->_config = configuration ?: [self defaultConfiguration]; + atomic_store(&inst->_activatedExperiments, 0); +} + +@end + +BOOL ASActivateExperimentalFeature(ASExperimentalFeatures feature) +{ + return [ASGetSharedConfigMgr() activateExperimentalFeature:feature]; +} diff --git a/Source/ASControlNode.h b/Source/ASControlNode.h index e08f0650f..7896b93eb 100644 --- a/Source/ASControlNode.h +++ b/Source/ASControlNode.h @@ -141,12 +141,17 @@ static UIControlState const ASControlStateSelected ASDISPLAYNODE_DEPRECATED_MSG( @param event The event which triggered these control actions. May be nil. */ - (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)event; +@end + #if TARGET_OS_TV +@interface ASControlNode (tvOS) + /** @abstract How the node looks when it isn't focused. Exposed here so that subclasses can override. */ - (void)setDefaultFocusAppearance; -#endif + @end +#endif NS_ASSUME_NONNULL_END diff --git a/Source/ASControlNode.mm b/Source/ASControlNode.mm index fdd1155e7..697abf8d0 100644 --- a/Source/ASControlNode.mm +++ b/Source/ASControlNode.mm @@ -16,6 +16,7 @@ // #import +#import #import #import #import @@ -37,8 +38,6 @@ @interface ASControlNode () { @private - ASDN::RecursiveMutex _controlLock; - // Control Attributes BOOL _enabled; BOOL _highlighted; @@ -103,10 +102,12 @@ - (instancetype)init #if TARGET_OS_TV - (void)didLoad { + [super didLoad]; + // On tvOS all controls, such as buttons, interact with the focus system even if they don't have a target set on them. // Here we add our own internal tap gesture to handle this behaviour. self.userInteractionEnabled = YES; - UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(pressDown)]; + UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_pressDown)]; tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)]; [self.view addGestureRecognizer:tapGestureRec]; } @@ -295,7 +296,7 @@ - (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeE // ASControlNode cannot be layer backed if adding a target ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair."); - ASDN::MutexLocker l(_controlLock); + ASLockScopeSelf(); if (!_controlEventDispatchTable) { _controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries. @@ -349,7 +350,7 @@ - (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)con NSParameterAssert(target); NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents); - ASDN::MutexLocker l(_controlLock); + ASLockScopeSelf(); // Grab the event target action array for this event. NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]; @@ -371,7 +372,7 @@ - (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)con - (NSSet *)allTargets { - ASDN::MutexLocker l(_controlLock); + ASLockScopeSelf(); NSMutableSet *targets = [[NSMutableSet alloc] init]; @@ -390,7 +391,7 @@ - (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNo { NSParameterAssert(controlEventMask != 0); - ASDN::MutexLocker l(_controlLock); + ASLockScopeSelf(); // Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask. _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^ @@ -432,31 +433,31 @@ - (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent: NSMutableArray *resolvedEventTargetActionArray = [[NSMutableArray alloc] init]; - _controlLock.lock(); - - // Enumerate the events in the mask, invoking the target-action pairs for each. - _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^ - (ASControlNodeEvent controlEvent) - { - // Iterate on each target action pair - for (ASControlTargetAction *targetAction in _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]) { - ASControlTargetAction *resolvedTargetAction = [[ASControlTargetAction alloc] init]; - resolvedTargetAction.action = targetAction.action; - resolvedTargetAction.target = targetAction.target; - - // NSNull means that a nil target was set, so start at self and travel the responder chain - if (!resolvedTargetAction.target && targetAction.createdWithNoTarget) { - // if the target cannot perform the action, travel the responder chain to try to find something that does - resolvedTargetAction.target = [self.view targetForAction:resolvedTargetAction.action withSender:self]; - } - - if (resolvedTargetAction.target) { - [resolvedEventTargetActionArray addObject:resolvedTargetAction]; + { + ASLockScopeSelf(); + + // Enumerate the events in the mask, invoking the target-action pairs for each. + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^ + (ASControlNodeEvent controlEvent) + { + // Iterate on each target action pair + for (ASControlTargetAction *targetAction in _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]) { + ASControlTargetAction *resolvedTargetAction = [[ASControlTargetAction alloc] init]; + resolvedTargetAction.action = targetAction.action; + resolvedTargetAction.target = targetAction.target; + + // NSNull means that a nil target was set, so start at self and travel the responder chain + if (!resolvedTargetAction.target && targetAction.createdWithNoTarget) { + // if the target cannot perform the action, travel the responder chain to try to find something that does + resolvedTargetAction.target = [self.view targetForAction:resolvedTargetAction.action withSender:self]; + } + + if (resolvedTargetAction.target) { + [resolvedEventTargetActionArray addObject:resolvedTargetAction]; + } } - } - }); - - _controlLock.unlock(); + }); + } //We don't want to hold the lock while calling out, we could potentially walk up the ownership tree causing a deadlock. #pragma clang diagnostic push diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index ea241bd16..7a7c7eb6a 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 @@ -192,6 +192,8 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable @interface ASLayoutElementStyle (Yoga) - (YGNodeRef)yogaNodeCreateIfNeeded; +- (void)destroyYogaNode; + @property (nonatomic, assign, readonly) YGNodeRef yogaNode; @property (nonatomic, assign, readwrite) ASStackLayoutDirection flexDirection; diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 6727cd23b..d656a8983 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -2,8 +2,13 @@ // ASDisplayNode+Layout.mm // Texture // -// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0 (the "License"); +// 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) through the 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 // @@ -12,14 +17,13 @@ #import #import +#import +#import #import #import #import #import -#import - -#pragma mark - #pragma mark - ASDisplayNode (ASLayoutElement) @implementation ASDisplayNode (ASLayoutElement) @@ -85,6 +89,7 @@ - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)par 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); } @@ -211,8 +216,9 @@ @implementation ASDisplayNode (ASLayoutInternal) * @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); @@ -227,7 +233,7 @@ - (void)_setNeedsLayoutFromAbove 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]; @@ -287,8 +293,10 @@ - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size } } -- (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]) { @@ -296,16 +304,29 @@ - (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds } CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); + NSUInteger calculatedVersion = _calculatedDisplayNodeLayout->version; - // 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 = NO; + if (_pendingDisplayNodeLayout != nullptr) { + NSUInteger pendingVersion = _pendingDisplayNodeLayout->version; + if (pendingVersion >= _layoutVersion) { + if (pendingVersion > calculatedVersion) { + pendingLayoutIsPreferred = YES; // Newer _pending + } else if (pendingVersion == calculatedVersion + && !ASSizeRangeEqualToSizeRange(_pendingDisplayNodeLayout->constrainedSize, + _calculatedDisplayNodeLayout->constrainedSize)) { + pendingLayoutIsPreferred = YES; // _pending with a different constrained size + } } } + BOOL calculatedLayoutIsReusable = (calculatedVersion >= _layoutVersion + && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))); + if (!pendingLayoutIsPreferred && calculatedLayoutIsReusable) { + return; + } as_activity_create_for_scope("Update node layout for current bounds"); as_log_verbose(ASLayoutLog(), "Node %@, bounds size %@, calculatedSize %@, calculatedIsDirty %d", self, NSStringFromCGSize(boundsSizeForLayout), NSStringFromCGSize(_calculatedDisplayNodeLayout->layout.size), _calculatedDisplayNodeLayout->version < _layoutVersion.load()); @@ -368,8 +389,10 @@ - (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds // 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; } @@ -389,7 +412,7 @@ - (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds - (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 diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h index 622acc299..bd8a44190 100644 --- a/Source/ASDisplayNode+Subclasses.h +++ b/Source/ASDisplayNode+Subclasses.h @@ -15,8 +15,6 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import - #import #import @@ -103,6 +101,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)nodeDidLayout; +/** + * @abstract Called when the node loads. + * @discussion Can be used for operations that are performed after the node's view is available. + * @note This method is guaranteed to be called on main. + */ +- (void)nodeDidLoad; + @end @interface ASDisplayNode (Subclassing) @@ -352,6 +357,12 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)willEnterHierarchy ASDISPLAYNODE_REQUIRES_SUPER; +/** + * Called just after the view is added to a window. + * Note: this may be called multiple times during view controller transitions. To overcome this: use didEnterVisibleState or it's equavalents. + */ +- (void)didEnterHierarchy ASDISPLAYNODE_REQUIRES_SUPER; + /** * Called after the view is removed from the window. */ diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 6ef80ccf1..078559639 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -24,7 +24,7 @@ #import #import #import -#import +#import #import #import @@ -94,12 +94,10 @@ - (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute { - if (AS_AT_LEAST_IOS9) { - UIUserInterfaceLayoutDirection layoutDirection = - [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute]; - self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight - ? YGDirectionLTR : YGDirectionRTL); - } + UIUserInterfaceLayoutDirection layoutDirection = + [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute]; + self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight + ? YGDirectionLTR : YGDirectionRTL); } - (void)setYogaParent:(ASDisplayNode *)yogaParent @@ -159,6 +157,8 @@ - (ASLayout *)layoutForYogaNode - (void)setupYogaCalculatedLayout { + ASLockScopeSelf(); + YGNodeRef yogaNode = self.style.yogaNode; uint32_t childCount = YGNodeGetChildCount(yogaNode); ASDisplayNodeAssert(childCount == self.yogaChildren.count, @@ -212,7 +212,10 @@ - (void)setupYogaCalculatedLayout parentSize.width = YGNodeLayoutGetWidth(parentNode); parentSize.height = YGNodeLayoutGetHeight(parentNode); } - _pendingDisplayNodeLayout = std::make_shared(layout, ASSizeRangeUnconstrained, parentSize, 0); + // For the root node in a Yoga tree, make sure to preserve the constrainedSize originally provided. + // This will be used for all relayouts triggered by children, since they escalate to root. + ASSizeRange range = parentNode ? ASSizeRangeUnconstrained : self.constrainedSizeForCalculatedLayout; + _pendingDisplayNodeLayout = std::make_shared(layout, range, parentSize, _layoutVersion); } } @@ -237,9 +240,18 @@ - (void)updateYogaMeasureFuncIfNeeded - (void)invalidateCalculatedYogaLayout { YGNodeRef yogaNode = self.style.yogaNode; - if (yogaNode && YGNodeGetMeasureFunc(yogaNode)) { + if (yogaNode && [self shouldHaveYogaMeasureFunc]) { // Yoga internally asserts that MarkDirty() may only be called on nodes with a measurement function. + BOOL needsTemporaryMeasureFunc = (YGNodeGetMeasureFunc(yogaNode) == NULL); + if (needsTemporaryMeasureFunc) { + ASDisplayNodeAssert(self.yogaLayoutInProgress == NO, + @"shouldHaveYogaMeasureFunc == YES, and inside a layout pass, but no measure func pointer! %@", self); + YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); + } YGNodeMarkDirty(yogaNode); + if (needsTemporaryMeasureFunc) { + YGNodeSetMeasureFunc(yogaNode, NULL); + } } self.yogaCalculatedLayout = nil; } @@ -255,7 +267,7 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize return; } - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // Prepare all children for the layout pass with the current Yoga tree configuration. ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 84da628b5..8968a1f50 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -36,17 +36,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. @@ -127,7 +127,7 @@ extern NSInteger const ASDefaultDrawingPriority; * */ -@interface ASDisplayNode : NSObject +@interface ASDisplayNode : NSObject /** @name Initializing a node object */ @@ -551,6 +551,20 @@ extern NSInteger const ASDefaultDrawingPriority; */ @property (nonatomic, readonly) BOOL supportsLayerBacking; +/** + * Whether or not the node layout should be automatically updated when it receives safeAreaInsetsDidChange. + * + * Defaults to NO. + */ +@property (nonatomic, assign) BOOL automaticallyRelayoutOnSafeAreaChanges; + +/** + * Whether or not the node layout should be automatically updated when it receives layoutMarginsDidChange. + * + * Defaults to NO. + */ +@property (nonatomic, assign) BOOL automaticallyRelayoutOnLayoutMarginsChanges; + @end /** @@ -659,6 +673,12 @@ extern NSInteger const ASDefaultDrawingPriority; * @default ASCornerRoundingTypeDefaultSlowCALayer */ @property (nonatomic, assign) ASCornerRoundingType cornerRoundingType; // default=Slow CALayer .cornerRadius (offscreen rendering) + +/** @abstract The radius to use when rounding corners of the ASDisplayNode. + * + * @discussion This property is thread-safe and should always be preferred over CALayer's cornerRadius property, + * even if corner rounding type is ASCornerRoundingTypeDefaultSlowCALayer. + */ @property (nonatomic, assign) CGFloat cornerRadius; // default=0.0 @property (nonatomic, assign) BOOL clipsToBounds; // default==NO @@ -702,7 +722,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 @@ -719,6 +739,33 @@ extern NSInteger const ASDefaultDrawingPriority; @property (nonatomic, assign) BOOL autoresizesSubviews; // default==YES (undefined for layer-backed nodes) @property (nonatomic, assign) UIViewAutoresizing autoresizingMask; // default==UIViewAutoresizingNone (undefined for layer-backed nodes) +/** + * @abstract Content margins + * + * @discussion This property is bridged to its UIView counterpart. + * + * If your layout depends on this property, you should probably enable automaticallyRelayoutOnLayoutMarginsChanges to ensure + * that the layout gets automatically updated when the value of this property changes. Or you can override layoutMarginsDidChange + * and make all the necessary updates manually. + */ +@property (nonatomic, assign) UIEdgeInsets layoutMargins; +@property (nonatomic, assign) BOOL preservesSuperviewLayoutMargins; // default is NO - set to enable pass-through or cascading behavior of margins from this view’s parent to its children +- (void)layoutMarginsDidChange; + +/** + * @abstract Safe area insets + * + * @discussion This property is bridged to its UIVIew counterpart. + * + * If your layout depends on this property, you should probably enable automaticallyRelayoutOnSafeAreaChanges to ensure + * that the layout gets automatically updated when the value of this property changes. Or you can override safeAreaInsetsDidChange + * and make all the necessary updates manually. + */ +@property (nonatomic, readonly) UIEdgeInsets safeAreaInsets; +@property (nonatomic, assign) BOOL insetsLayoutMarginsFromSafeArea; // Default: YES +- (void)safeAreaInsetsDidChange; + + // UIResponder methods // By default these fall through to the underlying view, but can be overridden. - (BOOL)canBecomeFirstResponder; // default==NO @@ -880,7 +927,7 @@ extern NSInteger const ASDefaultDrawingPriority; - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated shouldMeasureAsync:(BOOL)shouldMeasureAsync - measurementCompletion:(nullable void(^)())completion; + measurementCompletion:(nullable void(^)(void))completion; /** @@ -897,7 +944,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 cde2f0030..45337d987 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -18,7 +18,6 @@ #import #import -#import #import #import #import @@ -35,7 +34,11 @@ #import #import #import +#import +#import +#import #import +#import #import #import #import @@ -54,6 +57,10 @@ #else #define TIME_SCOPED(outVar) #endif +// This is trying to merge non-rangeManaged with rangeManaged, so both range-managed and standalone nodes wait before firing their exit-visibility handlers, as UIViewController transitions now do rehosting at both start & end of animation. +// Enable this will mitigate interface updating state when coalescing disabled. +// TODO(wsdwsd0829): Rework enabling code to ensure that interface state behavior is not altered when ASCATransactionQueue is disabled. +#define ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR 0 static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; @@ -62,7 +69,7 @@ // 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 @@ -92,7 +99,7 @@ BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLaye _ASPendingState *ASDisplayNodeGetPendingState(ASDisplayNode *node) { - ASDN::MutexLocker l(node->__instanceLock__); + ASLockScope(node); _ASPendingState *result = node->_pendingViewState; if (result == nil) { result = [[_ASPendingState alloc] init]; @@ -225,11 +232,12 @@ + (void)initialize class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); } +#if !AS_INITIALIZE_FRAMEWORK_MANUALLY + (void)load { - // Ensure this value is cached on the main thread before needed in the background. - ASScreenScale(); + ASInitializeFrameworkMainThread(); } +#endif + (Class)viewClass { @@ -258,6 +266,11 @@ - (void)_initializeInstance _viewClass = [self.class viewClass]; _layerClass = [self.class layerClass]; + BOOL isSynchronous = ![_viewClass isSubclassOfClass:[_ASDisplayView class]] + || ![_layerClass isSubclassOfClass:[_ASDisplayLayer class]]; + setFlag(Synchronous, isSynchronous); + + _contentsScaleForDisplay = ASScreenScale(); _drawingPriority = ASDefaultDrawingPriority; @@ -273,6 +286,14 @@ - (void)_initializeInstance _flags.canClearContentsOfLayer = YES; _flags.canCallSetNeedsDisplayOfLayer = YES; + + _fallbackSafeAreaInsets = UIEdgeInsetsZero; + _fallbackInsetsLayoutMarginsFromSafeArea = YES; + _isViewControllerRoot = NO; + + _automaticallyRelayoutOnSafeAreaChanges = NO; + _automaticallyRelayoutOnLayoutMarginsChanges = NO; + ASDisplayNodeLogEvent(self, @"init"); } @@ -352,6 +373,16 @@ - (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBl return self; } +- (void)lock +{ + __instanceLock__.lock(); +} + +- (void)unlock +{ + __instanceLock__.unlock(); +} + - (void)setViewBlock:(ASDisplayNodeViewBlock)viewBlock { ASDisplayNodeAssertFalse(self.nodeLoaded); @@ -440,7 +471,7 @@ - (void)_scheduleIvarsForMainDeallocation // is still deallocated on a background thread object_setIvar(self, ivar, nil); - ASPerformMainThreadDeallocation(value); + ASPerformMainThreadDeallocation(&value); } else { as_log_debug(ASMainThreadDeallocationLog(), "%@: Not trampolining ivar '%s' value %@.", self, ivar_getName(ivar), value); } @@ -455,7 +486,7 @@ - (void)_scheduleIvarsForMainDeallocation * * Result is of type NSValue<[Ivar]> */ -+ (NSValue * _Nonnull)_ivarsThatMayNeedMainDeallocation ++ (NSValue * _Nonnull)_ivarsThatMayNeedMainDeallocation NS_RETURNS_RETAINED { static NSCache *ivarsCache; static dispatch_once_t onceToken; @@ -545,6 +576,8 @@ - (UIView *)_locked_viewToLoad // Special handling of wrapping UIKit components if (checkFlag(Synchronous)) { + [self checkResponderCompatibility]; + // UIImageView layers. More details on the flags if ([_viewClass isSubclassOfClass:[UIImageView class]]) { _flags.canClearContentsOfLayer = NO; @@ -629,6 +662,8 @@ - (void)_didLoad for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) { block(self); } + + [_interfaceStateDelegate nodeDidLoad]; } - (void)didLoad @@ -828,6 +863,194 @@ - (void)nodeViewDidAddGestureRecognizer _flags.viewEverHadAGestureRecognizerAttached = YES; } +- (UIEdgeInsets)fallbackSafeAreaInsets +{ + ASDN::MutexLocker l(__instanceLock__); + return _fallbackSafeAreaInsets; +} + +- (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets +{ + BOOL needsManualUpdate; + BOOL updatesLayoutMargins; + + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssertThreadAffinity(self); + + if (UIEdgeInsetsEqualToEdgeInsets(insets, _fallbackSafeAreaInsets)) { + return; + } + + _fallbackSafeAreaInsets = insets; + needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked; + updatesLayoutMargins = needsManualUpdate && [self _locked_insetsLayoutMarginsFromSafeArea]; + } + + if (needsManualUpdate) { + [self safeAreaInsetsDidChange]; + } + + if (updatesLayoutMargins) { + [self layoutMarginsDidChange]; + } +} + +- (void)_fallbackUpdateSafeAreaOnChildren +{ + ASDisplayNodeAssertThreadAffinity(self); + + UIEdgeInsets insets = self.safeAreaInsets; + CGRect bounds = self.bounds; + + for (ASDisplayNode *child in self.subnodes) { + if (AS_AT_LEAST_IOS11 && !child.layerBacked) { + // In iOS 11 view-backed nodes already know what their safe area is. + continue; + } + + if (child.viewControllerRoot) { + // Its safe area is controlled by a view controller. Don't override it. + continue; + } + + CGRect childFrame = child.frame; + UIEdgeInsets childInsets = UIEdgeInsetsMake(MAX(insets.top - (CGRectGetMinY(childFrame) - CGRectGetMinY(bounds)), 0), + MAX(insets.left - (CGRectGetMinX(childFrame) - CGRectGetMinX(bounds)), 0), + MAX(insets.bottom - (CGRectGetMaxY(bounds) - CGRectGetMaxY(childFrame)), 0), + MAX(insets.right - (CGRectGetMaxX(bounds) - CGRectGetMaxX(childFrame)), 0)); + + child.fallbackSafeAreaInsets = childInsets; + } +} + +- (BOOL)isViewControllerRoot +{ + ASDN::MutexLocker l(__instanceLock__); + return _isViewControllerRoot; +} + +- (void)setViewControllerRoot:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _isViewControllerRoot = flag; +} + +- (BOOL)automaticallyRelayoutOnSafeAreaChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnSafeAreaChanges; +} + +- (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnSafeAreaChanges = flag; +} + +- (BOOL)automaticallyRelayoutOnLayoutMarginsChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnLayoutMarginsChanges; +} + +- (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnLayoutMarginsChanges = flag; +} + +#pragma mark - UIResponder + +#define HANDLE_NODE_RESPONDER_METHOD(__sel) \ + /* All responder methods should be called on the main thread */ \ + ASDisplayNodeAssertMainThread(); \ + if (checkFlag(Synchronous)) { \ + /* If the view is not a _ASDisplayView subclass (Synchronous) just call through to the view as we + expect it's a non _ASDisplayView subclass that will respond */ \ + return [_view __sel]; \ + } else { \ + if (ASSubclassOverridesSelector([_ASDisplayView class], _viewClass, @selector(__sel))) { \ + /* If the subclass overwrites canBecomeFirstResponder just call through + to it as we expect it will handle it */ \ + return [_view __sel]; \ + } else { \ + /* Call through to _ASDisplayView's superclass to get it handled */ \ + return [(_ASDisplayView *)_view __##__sel]; \ + } \ + } \ + +- (void)checkResponderCompatibility +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // There are certain cases we cannot handle and are not supported: + // 1. If the _view class is not a subclass of _ASDisplayView + if (checkFlag(Synchronous)) { + // 2. At least one UIResponder methods are overwritten in the node subclass + NSString *message = @"Overwritting %@ and having a backing view that is not an _ASDisplayView is not supported."; + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canBecomeFirstResponder)), ([NSString stringWithFormat:message, @"canBecomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(becomeFirstResponder)), ([NSString stringWithFormat:message, @"becomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canResignFirstResponder)), ([NSString stringWithFormat:message, @"canResignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(resignFirstResponder)), ([NSString stringWithFormat:message, @"resignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(isFirstResponder)), ([NSString stringWithFormat:message, @"isFirstResponder"])); + } +#endif +} + +- (BOOL)__canBecomeFirstResponder +{ + if (_view == nil) { + // By default we return NO if not view is created yet + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(canBecomeFirstResponder); +} + +- (BOOL)__becomeFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canBecomeFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(becomeFirstResponder); +} + +- (BOOL)__canResignFirstResponder +{ + if (_view == nil) { + // By default we return YES if no view is created yet + return YES; + } + + HANDLE_NODE_RESPONDER_METHOD(canResignFirstResponder); +} + +- (BOOL)__resignFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canResignFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(resignFirstResponder); +} + +- (BOOL)__isFirstResponder +{ + if (_view == nil) { + // If no view is created yet we can just return NO as it's unlikely it's the first responder + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(isFirstResponder); +} + #pragma mark - (NSString *)debugName @@ -911,7 +1134,9 @@ - (void)__layout // 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]; } @@ -926,6 +1151,8 @@ - (void)__layout [self layoutDidFinish]; }); } + + [self _fallbackUpdateSafeAreaOnChildren]; } - (void)layoutDidFinish @@ -942,15 +1169,11 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize { - // Use a pthread specific to mark when this method is called re-entrant on same thread. // We only want one calculateLayout signpost interval per thread. - // This is fast enough to do it unconditionally. - auto key = ASPthreadStaticKey(NULL); - BOOL isRootCall = (pthread_getspecific(key) == NULL); + static _Thread_local NSInteger tls_callDepth; as_activity_scope_verbose(as_activity_create("Calculate node layout", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); as_log_verbose(ASLayoutLog(), "Calculating layout for %@ sizeRange %@", self, NSStringFromASSizeRange(constrainedSize)); - if (isRootCall) { - pthread_setspecific(key, kCFBooleanTrue); + if (tls_callDepth++ == 0) { ASSignpostStart(ASSignpostCalculateLayout); } @@ -959,8 +1182,7 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize ASLayout *result = [self calculateLayoutThatFits:resolvedRange]; as_log_verbose(ASLayoutLog(), "Calculated layout %@", result); - if (isRootCall) { - pthread_setspecific(key, NULL); + if (--tls_callDepth == 0) { ASSignpostEnd(ASSignpostCalculateLayout); } return result; @@ -1505,7 +1727,7 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor BOOL isRight = (idx == 1 || idx == 2); CGSize size = CGSizeMake(radius + 1, radius + 1); - UIGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); + ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); CGContextRef ctx = UIGraphicsGetCurrentContext(); if (isRight == YES) { @@ -1522,11 +1744,9 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor // No lock needed, as _clipCornerLayers is only modified on the main thread. CALayer *clipCornerLayer = _clipCornerLayers[idx]; - clipCornerLayer.contents = (id)(UIGraphicsGetImageFromCurrentImageContext().CGImage); + clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().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]; }); @@ -2656,6 +2876,8 @@ - (void)__enterHierarchy } __instanceLock__.unlock(); + + [self didEnterHierarchy]; } - (void)__exitHierarchy @@ -2739,7 +2961,7 @@ - (void)setHierarchyState:(ASHierarchyState)newState // Entered or exited range managed state. if ((newState & ASHierarchyStateRangeManaged) != (oldState & ASHierarchyStateRangeManaged)) { if (newState & ASHierarchyStateRangeManaged) { - [self enterInterfaceState:self.supernode.interfaceState]; + [self enterInterfaceState:self.supernode.pendingInterfaceState]; } else { // The case of exiting a range-managed state should be fairly rare. Adding or removing the node // to a view hierarchy will cause its interfaceState to be either fully set or unset (all fields), @@ -2760,7 +2982,8 @@ - (void)setHierarchyState:(ASHierarchyState)newState } } - 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 @@ -2775,36 +2998,56 @@ - (void)willEnterHierarchy } } +- (void)didEnterHierarchy { + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"You should never call -willEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASDisplayNodeAssert(_flags.isInHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); +} + - (void)didExitHierarchy { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(_flags.isExitingHierarchy, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); - + + // This case is important when tearing down hierarchies. We must deliver a visibileStateDidChange:NO callback, as part our API guarantee that this method can be used for + // things like data analytics about user content viewing. We cannot call the method in the dealloc as any incidental retain operations in client code would fail. + // Additionally, it may be that a Standard UIView which is containing us is moving between hierarchies, and we should not send the call if we will be re-added in the + // same runloop. Strategy: strong reference (might be the last!), wait one runloop, and confirm we are still outside the hierarchy (both layer-backed and view-backed). + // TODO: This approach could be optimized by only performing the dispatch for root elements + recursively apply the interface state change. This would require a closer + // integration with _ASDisplayLayer to ensure that the superlayer pointer has been cleared by this stage (to check if we are root or not), or a different delegate call. + +#if !ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR if (![self supportsRangeManagedInterfaceState]) { self.interfaceState = ASInterfaceStateNone; - } else { - // This case is important when tearing down hierarchies. We must deliver a visibileStateDidChange:NO callback, as part our API guarantee that this method can be used for - // things like data analytics about user content viewing. We cannot call the method in the dealloc as any incidental retain operations in client code would fail. - // Additionally, it may be that a Standard UIView which is containing us is moving between hierarchies, and we should not send the call if we will be re-added in the - // same runloop. Strategy: strong reference (might be the last!), wait one runloop, and confirm we are still outside the hierarchy (both layer-backed and view-backed). - // TODO: This approach could be optimized by only performing the dispatch for root elements + recursively apply the interface state change. This would require a closer - // integration with _ASDisplayLayer to ensure that the superlayer pointer has been cleared by this stage (to check if we are root or not), or a different delegate call. - - if (ASInterfaceStateIncludesVisible(self.interfaceState)) { - dispatch_async(dispatch_get_main_queue(), ^{ - // This block intentionally retains self. - __instanceLock__.lock(); - unsigned isInHierarchy = _flags.isInHierarchy; - BOOL isVisible = ASInterfaceStateIncludesVisible(_interfaceState); - ASInterfaceState newState = (_interfaceState & ~ASInterfaceStateVisible); - __instanceLock__.unlock(); - - if (!isInHierarchy && isVisible) { - self.interfaceState = newState; + return; + } +#endif + if (ASInterfaceStateIncludesVisible(_pendingInterfaceState)) { + void(^exitVisibleInterfaceState)(void) = ^{ + // This block intentionally retains self. + __instanceLock__.lock(); + unsigned isStillInHierarchy = _flags.isInHierarchy; + BOOL isVisible = ASInterfaceStateIncludesVisible(_pendingInterfaceState); + ASInterfaceState newState = (_pendingInterfaceState & ~ASInterfaceStateVisible); + __instanceLock__.unlock(); + if (!isStillInHierarchy && isVisible) { +#if ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR + if (![self supportsRangeManagedInterfaceState]) { + newState = ASInterfaceStateNone; } - }); +#endif + self.interfaceState = newState; + } + }; + + if (!ASCATransactionQueue.sharedQueue.enabled) { + dispatch_async(dispatch_get_main_queue(), exitVisibleInterfaceState); + } else { + exitVisibleInterfaceState(); } } } @@ -2865,25 +3108,54 @@ - (ASInterfaceState)interfaceState } - (void)setInterfaceState:(ASInterfaceState)newState +{ + if (!ASCATransactionQueue.sharedQueue.enabled) { + [self applyPendingInterfaceState:newState]; + } else { + ASDN::MutexLocker l(__instanceLock__); + if (_pendingInterfaceState != newState) { + NSLog(@"pendingInterfaceState: %lu, interfaceState: %lu node: %@", newState, self.interfaceState, self); + _pendingInterfaceState = newState; + [[ASCATransactionQueue sharedQueue] enqueue:self]; + } + } +} + +- (ASInterfaceState)pendingInterfaceState +{ + ASDN::MutexLocker l(__instanceLock__); + return _pendingInterfaceState; +} + +- (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState { //This method is currently called on the main thread. The assert has been added here because all of the //did(Enter|Exit)(Display|Visible|Preload)State methods currently guarantee calling on main. ASDisplayNodeAssertMainThread(); - // It should never be possible for a node to be visible but not be allowed / expected to display. - ASDisplayNodeAssertFalse(ASInterfaceStateIncludesVisible(newState) && !ASInterfaceStateIncludesDisplay(newState)); + // This method manages __instanceLock__ itself, to ensure the lock is not held while didEnter/Exit(.*)State methods are called, thus avoid potential deadlocks ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); ASInterfaceState oldState = ASInterfaceStateNone; + ASInterfaceState newState = ASInterfaceStateNone; { ASDN::MutexLocker l(__instanceLock__); - if (_interfaceState == newState) { - return; + // newPendingState will not be used when ASCATransactionQueue is enabled + // and use _pendingInterfaceState instead for interfaceState update. + if (!ASCATransactionQueue.sharedQueue.enabled) { + _pendingInterfaceState = newPendingState; } oldState = _interfaceState; + newState = _pendingInterfaceState; + if (newState == oldState) { + return; + } _interfaceState = newState; } + // It should never be possible for a node to be visible but not be allowed / expected to display. + ASDisplayNodeAssertFalse(ASInterfaceStateIncludesVisible(newState) && !ASInterfaceStateIncludesDisplay(newState)); + // TODO: Trigger asynchronous measurement if it is not already cached or being calculated. // if ((newState & ASInterfaceStateMeasureLayout) != (oldState & ASInterfaceStateMeasureLayout)) { // } @@ -2980,6 +3252,12 @@ - (void)setInterfaceState:(ASInterfaceState)newState [self interfaceStateDidChange:newState fromState:oldState]; } +- (void)prepareForCATransactionCommit +{ + // Apply _pendingInterfaceState actual _interfaceState, note that ASInterfaceStateNone is not used. + [self applyPendingInterfaceState:ASInterfaceStateNone]; +} + - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { // Subclass hook @@ -3076,6 +3354,19 @@ - (void)didEnterPreloadState { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); + + // If this node has ASM enabled and is not yet visible, force a layout pass to apply its applicable pending layout, if any, + // so that its subnodes are inserted/deleted and start preloading right away. + // + // - If it has an up-to-date layout (and subnodes), calling -layoutIfNeeded will be fast. + // + // - If it 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 is uncommon, + // and running a measurement pass here is a fine trade-off because preloading any time after this point would be late. + if (self.automaticallyManagesSubnodes) { + [self layoutIfNeeded]; + } + [_interfaceStateDelegate didEnterPreloadState]; } @@ -3452,40 +3743,6 @@ - (void)asyncTraitCollectionDidChange { // Subclass override } - -#if TARGET_OS_TV -#pragma mark - UIFocusEnvironment Protocol (tvOS) - -- (void)setNeedsFocusUpdate -{ - -} - -- (void)updateFocusIfNeeded -{ - -} - -- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context -{ - return NO; -} - -- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator -{ - -} - -- (UIView *)preferredFocusedView -{ - if (self.nodeLoaded) { - return self.view; - } else { - return nil; - } -} -#endif - @end #pragma mark - ASDisplayNode (Debugging) diff --git a/Source/ASDisplayNodeExtras.h b/Source/ASDisplayNodeExtras.h index aefb445f9..ba0a38172 100644 --- a/Source/ASDisplayNodeExtras.h +++ b/Source/ASDisplayNodeExtras.h @@ -35,7 +35,7 @@ #endif /// For deallocation of objects on the main thread across multiple run loops. -extern void ASPerformMainThreadDeallocation(_Nullable id object); +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. @@ -208,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 279687662..385343ae6 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 } } @@ -65,7 +70,7 @@ extern ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNod // Directly clear the visible bit if we are not in a window. This means that the interface state is, // if not already, about to be set to invisible as it is not possible for an element to be visible // while outside of a window. - ASInterfaceState interfaceState = displayNode.interfaceState; + ASInterfaceState interfaceState = displayNode.pendingInterfaceState; return (window == nil ? (interfaceState &= (~ASInterfaceStateVisible)) : interfaceState); } else { // For not range managed nodes we might be on our own to try to guess if we're visible. diff --git a/Source/ASEditableTextNode.mm b/Source/ASEditableTextNode.mm index 98351f076..404049fcf 100644 --- a/Source/ASEditableTextNode.mm +++ b/Source/ASEditableTextNode.mm @@ -75,7 +75,7 @@ other content (setting scrollEnabled = NO on the UITextView itself, See issue: https://github.com/facebook/AsyncDisplayKit/issues/1063 */ -@interface ASPanningOverriddenUITextView : UITextView +@interface ASPanningOverriddenUITextView : ASTextKitComponentsTextView { BOOL _shouldBlockPanGesture; } @@ -215,7 +215,7 @@ - (void)didLoad 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/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h new file mode 100644 index 000000000..0df466b6f --- /dev/null +++ b/Source/ASExperimentalFeatures.h @@ -0,0 +1,37 @@ +// +// ASExperimentalFeatures.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN +ASDISPLAYNODE_EXTERN_C_BEGIN + +/** + * A bit mask of features. + */ +typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { + ASExperimentalGraphicsContexts = 1 << 0, // exp_graphics_contexts + ASExperimentalTextNode = 1 << 1, // exp_text_node + ASExperimentalInterfaceStateCoalescing = 1 << 2, // exp_interface_state_coalesce + ASExperimentalUnfairLock = 1 << 3, // exp_unfair_lock + ASExperimentalFeatureAll = 0xFFFFFFFF +}; + +/// Convert flags -> name array. +NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags); + +/// Convert name array -> flags. +ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray *array); + +ASDISPLAYNODE_EXTERN_C_END +NS_ASSUME_NONNULL_END diff --git a/Source/ASExperimentalFeatures.m b/Source/ASExperimentalFeatures.m new file mode 100644 index 000000000..5ce20c70b --- /dev/null +++ b/Source/ASExperimentalFeatures.m @@ -0,0 +1,45 @@ +// +// ASExperimentalFeatures.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags) +{ + NSArray *allNames = ASCreateOnce((@[@"exp_graphics_contexts", + @"exp_text_node", + @"exp_interface_state_coalesce"])); + + if (flags == ASExperimentalFeatureAll) { + return allNames; + } + + // Go through all names, testing each bit. + NSUInteger i = 0; + return ASArrayByFlatMapping(allNames, NSString *name, ({ + (flags & (1 << i++)) ? name : nil; + })); +} + +// O(N^2) but with counts this small, it's probably faster +// than hashing the strings. +ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray *array) +{ + NSArray *allNames = ASExperimentalFeaturesGetNames(ASExperimentalFeatureAll); + ASExperimentalFeatures result = 0; + for (NSString *str in array) { + NSUInteger i = [allNames indexOfObject:str]; + if (i != NSNotFound) { + result |= (1 << i); + } + } + return result; +} diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 75e5d5f65..cca62928c 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -20,7 +20,6 @@ #import #import #import -#import #import #import #import @@ -28,6 +27,7 @@ #import #import #import +#import #import #define ASAnimatedImageDebug 0 @@ -44,7 +44,7 @@ @implementation ASImageNode (AnimatedImage) - (void)setAnimatedImage:(id )animatedImage { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_setAnimatedImage:animatedImage]; } @@ -55,6 +55,7 @@ - (void)_locked_setAnimatedImage:(id )animatedImage } id previousAnimatedImage = _animatedImage; + _animatedImage = animatedImage; if (animatedImage != nil) { @@ -80,6 +81,11 @@ - (void)_locked_setAnimatedImage:(id )animatedImage } [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 @@ -89,13 +95,13 @@ - (void)animatedImageSet:(id )newAnimatedImage previous - (id )animatedImage { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _animatedImage; } - (void)setAnimatedImagePaused:(BOOL)animatedImagePaused { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _animatedImagePaused = animatedImagePaused; @@ -104,14 +110,14 @@ - (void)setAnimatedImagePaused:(BOOL)animatedImagePaused - (BOOL)animatedImagePaused { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _animatedImagePaused; } - (void)setCoverImageCompleted:(UIImage *)coverImage { if (ASInterfaceStateIncludesDisplay(self.interfaceState)) { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_setCoverImageCompleted:coverImage]; } } @@ -129,7 +135,7 @@ - (void)_locked_setCoverImageCompleted:(UIImage *)coverImage - (void)setCoverImage:(UIImage *)coverImage { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_setCoverImage:coverImage]; } @@ -170,7 +176,7 @@ - (void)setAnimatedImageRunLoopMode:(NSString *)runLoopMode - (void)setShouldAnimate:(BOOL)shouldAnimate { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_setShouldAnimate:shouldAnimate]; } @@ -203,7 +209,7 @@ - (void)startAnimating { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_startAnimating]; } @@ -245,7 +251,7 @@ - (void)stopAnimating { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_stopAnimating]; } @@ -316,7 +322,7 @@ - (void)displayLinkFired:(CADisplayLink *)displayLink CFTimeInterval timeBetweenLastFire; if (self.lastDisplayLinkFire == 0) { timeBetweenLastFire = 0; - } else if (AS_AT_LEAST_IOS10){ + } else if (AS_AVAILABLE_IOS_TVOS(10, 10)) { timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp; } else { timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire; diff --git a/Source/ASImageNode.h b/Source/ASImageNode.h index 547044c7d..99ef57549 100644 --- a/Source/ASImageNode.h +++ b/Source/ASImageNode.h @@ -141,6 +141,11 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image); @end +#if TARGET_OS_TV +@interface ASImageNode (tvOS) +@end +#endif + @interface ASImageNode (AnimatedImage) /** diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index df39741b0..b0266ebc3 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -22,9 +22,11 @@ #import #import #import -#import +#import +#import #import #import +#import #import #import #import @@ -213,11 +215,10 @@ - (UIImage *)placeholderImage ASDN::MutexLocker l(__instanceLock__); - UIGraphicsBeginImageContext(size); + ASGraphicsBeginImageContextWithOptions(size, NO, 1); [self.placeholderColor setFill]; UIRectFill(CGRectMake(0, 0, size.width, size.height)); - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); return image; } @@ -278,7 +279,7 @@ - (void)_locked_setImage:(UIImage *)image BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width || oldImageSize.height > kMinReleaseImageOnBackgroundSize.height; if (shouldReleaseImageOnBackgroundThread) { - ASPerformBackgroundDeallocation(oldImage); + ASPerformBackgroundDeallocation(&oldImage); } } @@ -305,7 +306,7 @@ - (void)setPlaceholderColor:(UIColor *)placeholderColor - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); ASImageNodeDrawParameters *drawParameters = [[ASImageNodeDrawParameters alloc] init]; drawParameters->_image = [self _locked_Image]; @@ -325,7 +326,7 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer // Hack for now to retain the weak entry that was created while this drawing happened drawParameters->_didDrawBlock = ^(ASWeakMapEntry *entry){ - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _weakCacheEntry = entry; }; @@ -472,7 +473,7 @@ + (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters: + (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled { - // The following `UIGraphicsBeginImageContextWithOptions` call will sometimes take take longer than 5ms on an + // The following `ASGraphicsBeginImageContextWithOptions` call will sometimes take take longer than 5ms on an // A5 processor for a 400x800 backingSize. // Check for cancellation before we call it. if (isCancelled()) { @@ -481,7 +482,7 @@ + (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:( // Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds // will do its rounding on pixel instead of point boundaries - UIGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0); + ASGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0); BOOL contextIsClean = YES; @@ -522,16 +523,13 @@ + (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:( key.didDisplayNodeContentWithRenderingContext(context, drawParameters); } - // The following `UIGraphicsGetImageFromCurrentImageContext` call will commonly take more than 20ms on an - // A5 processor. Check for cancellation before we call it. + // Check cancellation one last time before forming image. if (isCancelled()) { - UIGraphicsEndImageContext(); + ASGraphicsEndImageContext(); return nil; } - UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); - - UIGraphicsEndImageContext(); + UIImage *result = ASGraphicsGetImageAndEndCurrentContext(); if (key.imageModificationBlock) { result = key.imageModificationBlock(result); @@ -742,7 +740,7 @@ - (NSDictionary *)debugLabelAttributes extern asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor) { return ^(UIImage *originalImage) { - UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); UIBezierPath *roundOutline = [UIBezierPath bezierPathWithOvalInRect:(CGRect){CGPointZero, originalImage.size}]; // Make the image round @@ -758,24 +756,21 @@ extern asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock( [roundOutline stroke]; } - UIImage *modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return modifiedImage; + return ASGraphicsGetImageAndEndCurrentContext(); }; } extern asimagenode_modification_block_t ASImageNodeTintColorModificationBlock(UIColor *color) { return ^(UIImage *originalImage) { - UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); // Set color and render template [color setFill]; UIImage *templateImage = [originalImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [templateImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; - UIImage *modifiedImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIImage *modifiedImage = ASGraphicsGetImageAndEndCurrentContext(); // if the original image was stretchy, keep it stretchy if (!UIEdgeInsetsEqualToEdgeInsets(originalImage.capInsets, UIEdgeInsetsZero)) { diff --git a/Source/ASMapNode.mm b/Source/ASMapNode.mm index a9394a4d7..70a8951de 100644 --- a/Source/ASMapNode.mm +++ b/Source/ASMapNode.mm @@ -22,11 +22,13 @@ #import -#import +#import #import +#import #import #import #import +#import @interface ASMapNode() { @@ -106,14 +108,14 @@ - (void)didExitPreloadState - (BOOL)isLiveMap { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _liveMap; } - (void)setLiveMap:(BOOL)liveMap { ASDisplayNodeAssert(!self.isLayerBacked, @"ASMapNode can not use the interactive map feature whilst .isLayerBacked = YES, set .layerBacked = NO to use the interactive map feature."); - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (liveMap == _liveMap) { return; } @@ -125,19 +127,19 @@ - (void)setLiveMap:(BOOL)liveMap - (BOOL)needsMapReloadOnBoundsChange { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _needsMapReloadOnBoundsChange; } - (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; } - (MKMapSnapshotOptions *)options { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (!_options) { _options = [[MKMapSnapshotOptions alloc] init]; _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); @@ -151,7 +153,7 @@ - (MKMapSnapshotOptions *)options - (void)setOptions:(MKMapSnapshotOptions *)options { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (!_options || ![options isEqual:_options]) { _options = options; if (self.isLiveMap) { @@ -222,7 +224,7 @@ - (void)takeSnapshot CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height); - UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); + ASGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); [image drawAtPoint:CGPointZero]; UIImage *pinImage; @@ -254,8 +256,7 @@ - (void)takeSnapshot } } - image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + image = ASGraphicsGetImageAndEndCurrentContext(); } strongSelf.image = image; @@ -263,7 +264,7 @@ - (void)takeSnapshot }]; } -+ (UIImage *)defaultPinImageWithCenterOffset:(CGPoint *)centerOffset ++ (UIImage *)defaultPinImageWithCenterOffset:(CGPoint *)centerOffset NS_RETURNS_RETAINED { static MKAnnotationView *pin; static dispatch_once_t onceToken; @@ -324,7 +325,7 @@ - (void)removeLiveMap - (NSArray *)annotations { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _annotations; } @@ -332,7 +333,7 @@ - (void)setAnnotations:(NSArray *)annotations { annotations = [annotations copy] ? : @[]; - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _annotations = annotations; ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; if (self.isLiveMap) { @@ -353,7 +354,7 @@ - (void)setAnnotations:(NSArray *)annotations } } --(MKCoordinateRegion)regionToFitAnnotations:(NSArray> *)annotations +- (MKCoordinateRegion)regionToFitAnnotations:(NSArray> *)annotations { if([annotations count] == 0) return MKCoordinateRegionForMapRect(MKMapRectWorld); @@ -377,12 +378,11 @@ -(MKCoordinateRegion)regionToFitAnnotations:(NSArray> *)annotat } -(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { - ASDN::MutexLocker l(__instanceLock__); - return _showAnnotationsOptions; + return ASLockedSelf(_showAnnotationsOptions); } -(void)setShowAnnotationsOptions:(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _showAnnotationsOptions = showAnnotationsOptions; } diff --git a/Source/ASMultiplexImageNode.h b/Source/ASMultiplexImageNode.h index 15baced60..e4349d8cc 100644 --- a/Source/ASMultiplexImageNode.h +++ b/Source/ASMultiplexImageNode.h @@ -131,14 +131,12 @@ typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { */ @property (nonatomic, assign, readwrite) BOOL shouldRenderProgressImages; -#if TARGET_OS_IOS /** * @abstract The image manager that this image node should use when requesting images from the Photos framework. If this is `nil` (the default), then `PHImageManager.defaultManager` is used. * @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below. */ -@property (nullable, nonatomic, strong) PHImageManager *imageManager; -#endif +@property (nullable, nonatomic, strong) PHImageManager *imageManager API_AVAILABLE(ios(8.0), tvos(10.0)); @end @@ -245,7 +243,6 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier */ - (nullable NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(ASImageIdentifier)imageIdentifier; -#if TARGET_OS_IOS /** * @abstract A PHAsset for the specific asset local identifier * @param imageNode The sender. @@ -256,12 +253,10 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier * @note This method may be called from any thread. * @return A PHAsset corresponding to `assetLocalIdentifier`, or nil if none is available. */ -- (nullable PHAsset *)multiplexImageNode:(ASMultiplexImageNode *)imageNode assetForLocalIdentifier:(NSString *)assetLocalIdentifier; -#endif +- (nullable PHAsset *)multiplexImageNode:(ASMultiplexImageNode *)imageNode assetForLocalIdentifier:(NSString *)assetLocalIdentifier API_AVAILABLE(ios(8.0), tvos(10.0)); @end -#pragma mark - -#if TARGET_OS_IOS +#pragma mark - @interface NSURL (ASPhotosFrameworkURLs) /** @@ -275,9 +270,8 @@ didFinishDownloadingImageWithIdentifier:(ASImageIdentifier)imageIdentifier + (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode - options:(PHImageRequestOptions *)options AS_WARN_UNUSED_RESULT; + options:(PHImageRequestOptions *)options NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT API_AVAILABLE(ios(8.0), tvos(10.0)); @end -#endif NS_ASSUME_NONNULL_END diff --git a/Source/ASMultiplexImageNode.mm b/Source/ASMultiplexImageNode.mm index 81f3f43c1..50057dd52 100644 --- a/Source/ASMultiplexImageNode.mm +++ b/Source/ASMultiplexImageNode.mm @@ -16,15 +16,20 @@ // #import + +#if TARGET_OS_IOS #import +#endif #import -#import #import +#import +#import #import #import #import #import +#import #if AS_PIN_REMOTE_IMAGE #import @@ -136,6 +141,7 @@ - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imag @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil. */ - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock; +#endif /** @abstract Loads the image corresponding to the given image request from the Photos framework. @@ -143,8 +149,8 @@ - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL com @param request The photos image request to load. May not be nil. @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil. */ -- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock; -#endif +- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock API_AVAILABLE(ios(8.0), tvos(10.0)); + /** @abstract Downloads the image corresponding to the given imageIdentifier from the given URL. @param imageIdentifier The identifier for the image to be downloaded. May not be nil. @@ -345,31 +351,29 @@ - (void)setDataSource:(id )dataSource _dataSource = dataSource; _dataSourceFlags.image = [_dataSource respondsToSelector:@selector(multiplexImageNode:imageForImageIdentifier:)]; _dataSourceFlags.URL = [_dataSource respondsToSelector:@selector(multiplexImageNode:URLForImageIdentifier:)]; - #if TARGET_OS_IOS - _dataSourceFlags.asset = [_dataSource respondsToSelector:@selector(multiplexImageNode:assetForLocalIdentifier:)]; - #endif + if (AS_AVAILABLE_IOS_TVOS(9, 10)) { + _dataSourceFlags.asset = [_dataSource respondsToSelector:@selector(multiplexImageNode:assetForLocalIdentifier:)]; + } } - (void)setShouldRenderProgressImages:(BOOL)shouldRenderProgressImages { - __instanceLock__.lock(); + [self lock]; if (shouldRenderProgressImages == _shouldRenderProgressImages) { - __instanceLock__.unlock(); + [self unlock]; return; } _shouldRenderProgressImages = shouldRenderProgressImages; - - __instanceLock__.unlock(); + [self unlock]; [self _updateProgressImageBlockOnDownloaderIfNeeded]; } - (BOOL)shouldRenderProgressImages { - ASDN::MutexLocker l(__instanceLock__); - return _shouldRenderProgressImages; + return ASLockedSelf(_shouldRenderProgressImages); } #pragma mark - @@ -532,10 +536,10 @@ - (void)_clearImage CGSize imageSize = image.size; BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || imageSize.height > kMinReleaseImageOnBackgroundSize.height; + [self _setImage:nil]; if (shouldReleaseImageOnBackgroundThread) { - ASPerformBackgroundDeallocation(image); + ASPerformBackgroundDeallocation(&image); } - [self _setImage:nil]; } #pragma mark - @@ -616,7 +620,7 @@ - (void)_loadNextImage return; } - #if TARGET_OS_IOS +#if TARGET_OS_IOS // If it's an assets-library URL, we need to fetch it from the assets library. if ([[nextImageURL scheme] isEqualToString:kAssetsLibraryURLScheme]) { // Load the asset. @@ -624,48 +628,54 @@ - (void)_loadNextImage as_log_verbose(ASImageLoadingLog(), "Acquired image from assets library for %@ %@", weakSelf, nextImageIdentifier); finishedLoadingBlock(downloadedImage, nextImageIdentifier, error); }]; + + return; } - // Likewise, if it's a iOS 8 Photo asset, we need to fetch it accordingly. - else if (ASPhotosFrameworkImageRequest *request = [ASPhotosFrameworkImageRequest requestWithURL:nextImageURL]) { - [self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) { - as_log_verbose(ASImageLoadingLog(), "Acquired image from Photos for %@ %@", weakSelf, nextImageIdentifier); - finishedLoadingBlock(image, nextImageIdentifier, error); - }]; +#endif + + if (AS_AVAILABLE_IOS_TVOS(9, 10)) { + // Likewise, if it's a Photos asset, we need to fetch it accordingly. + if (ASPhotosFrameworkImageRequest *request = [ASPhotosFrameworkImageRequest requestWithURL:nextImageURL]) { + [self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from Photos for %@ %@", weakSelf, nextImageIdentifier); + finishedLoadingBlock(image, nextImageIdentifier, error); + }]; + + return; + } } - #endif - else // Otherwise, it's a web URL that we can download. - { - // First, check the cache. - [self _fetchImageWithIdentifierFromCache:nextImageIdentifier URL:nextImageURL completion:^(UIImage *imageFromCache) { + + // Otherwise, it's a web URL that we can download. + // First, check the cache. + [self _fetchImageWithIdentifierFromCache:nextImageIdentifier URL:nextImageURL completion:^(UIImage *imageFromCache) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + // If we had a cache-hit, we're done. + if (imageFromCache) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from cache for %@ id: %@ img: %@", strongSelf, nextImageIdentifier, imageFromCache); + finishedLoadingBlock(imageFromCache, nextImageIdentifier, nil); + return; + } + + // If the next image to load has changed, bail. + if (!ASObjectIsEqual([strongSelf _nextImageIdentifierToDownload], nextImageIdentifier)) { + finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged userInfo:nil]); + return; + } + + // Otherwise, we've got to download it. + [strongSelf _downloadImageWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) { __typeof__(self) strongSelf = weakSelf; - if (!strongSelf) - return; - - // If we had a cache-hit, we're done. - if (imageFromCache) { - as_log_verbose(ASImageLoadingLog(), "Acquired image from cache for %@ id: %@ img: %@", strongSelf, nextImageIdentifier, imageFromCache); - finishedLoadingBlock(imageFromCache, nextImageIdentifier, nil); - return; - } - - // If the next image to load has changed, bail. - if (!ASObjectIsEqual([strongSelf _nextImageIdentifierToDownload], nextImageIdentifier)) { - finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged userInfo:nil]); - return; + if (downloadedImage) { + as_log_verbose(ASImageLoadingLog(), "Acquired image from download for %@ id: %@ img: %@", strongSelf, nextImageIdentifier, downloadedImage); + } else { + as_log_error(ASImageLoadingLog(), "Error downloading image for %@ id: %@ err: %@", strongSelf, nextImageIdentifier, error); } - - // Otherwise, we've got to download it. - [strongSelf _downloadImageWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) { - __typeof__(self) strongSelf = weakSelf; - if (downloadedImage) { - as_log_verbose(ASImageLoadingLog(), "Acquired image from download for %@ id: %@ img: %@", strongSelf, nextImageIdentifier, downloadedImage); - } else { - as_log_error(ASImageLoadingLog(), "Error downloading image for %@ id: %@ err: %@", strongSelf, nextImageIdentifier, error); - } - finishedLoadingBlock(downloadedImage, nextImageIdentifier, error); - }]; + finishedLoadingBlock(downloadedImage, nextImageIdentifier, error); }]; - } + }]; } #if TARGET_OS_IOS - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock @@ -673,7 +683,11 @@ - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL com ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); ASDisplayNodeAssertNotNil(assetURL, @"assetURL is required"); ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); - + + // ALAssetsLibrary was replaced in iOS 8 and deprecated in iOS 9. + // We'll drop support very soon. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init]; [assetLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) { @@ -685,8 +699,9 @@ - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL com } failureBlock:^(NSError *error) { completionBlock(nil, error); }]; +#pragma clang diagnostic pop } - +#endif - (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock { ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); @@ -774,7 +789,7 @@ - (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identif _phImageRequestOperation = newImageRequestOp; [phImageRequestQueue addOperation:newImageRequestOp]; } -#endif + - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock { ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); @@ -818,7 +833,7 @@ - (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL c [self _setDownloadIdentifier:[_downloader downloadImageWithURL:imageURL callbackQueue:dispatch_get_main_queue() downloadProgress:downloadProgressBlock - completion:^(id imageContainer, NSError *error, id downloadIdentifier) { + completion:^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { // We dereference iVars directly, so we can't have weakSelf going nil on us. __typeof__(self) strongSelf = weakSelf; if (!strongSelf) @@ -879,7 +894,7 @@ - (void)_finishedLoadingImage:(UIImage *)image forIdentifier:(id)imageIdentifier @implementation NSURL (ASPhotosFrameworkURLs) -+ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options ++ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options NS_RETURNS_RETAINED { ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:assetLocalIdentifier]; request.options = options; diff --git a/Source/ASNetworkImageLoadInfo.h b/Source/ASNetworkImageLoadInfo.h new file mode 100644 index 000000000..6db366a9c --- /dev/null +++ b/Source/ASNetworkImageLoadInfo.h @@ -0,0 +1,43 @@ +// +// ASNetworkImageLoadInfo.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, ASNetworkImageSourceType) { + ASNetworkImageSourceUnspecified = 0, + ASNetworkImageSourceSynchronousCache, + ASNetworkImageSourceAsynchronousCache, + ASNetworkImageSourceFileURL, + ASNetworkImageSourceDownload, +}; + +AS_SUBCLASSING_RESTRICTED +@interface ASNetworkImageLoadInfo : NSObject + +/// The type of source from which the image was loaded. +@property (readonly) ASNetworkImageSourceType sourceType; + +/// The image URL that was downloaded. +@property (readonly) NSURL *url; + +/// The download identifier, if one was provided. +@property (nullable, readonly) id downloadIdentifier; + +/// The userInfo object provided by the downloader, if one was provided. +@property (nullable, readonly) id userInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ASNetworkImageLoadInfo.m b/Source/ASNetworkImageLoadInfo.m new file mode 100644 index 000000000..00baa1bf7 --- /dev/null +++ b/Source/ASNetworkImageLoadInfo.m @@ -0,0 +1,36 @@ +// +// ASNetworkImageLoadInfo.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@implementation ASNetworkImageLoadInfo + +- (instancetype)initWithURL:(NSURL *)url sourceType:(ASNetworkImageSourceType)sourceType downloadIdentifier:(id)downloadIdentifier userInfo:(id)userInfo +{ + if (self = [super init]) { + _url = [url copy]; + _sourceType = sourceType; + _downloadIdentifier = downloadIdentifier; + _userInfo = userInfo; + } + return self; +} + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + return self; +} + +@end diff --git a/Source/ASNetworkImageNode.h b/Source/ASNetworkImageNode.h index dc911bede..ec97e8469 100644 --- a/Source/ASNetworkImageNode.h +++ b/Source/ASNetworkImageNode.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASNetworkImageNodeDelegate, ASImageCacheProtocol, ASImageDownloaderProtocol; +@class ASNetworkImageLoadInfo; /** @@ -83,13 +84,16 @@ 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. - */ -@property (nullable, nonatomic, strong, readwrite) NSArray *URLs; + * 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. @@ -116,13 +120,19 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, readwrite) BOOL shouldRenderProgressImages; /** - * The image quality of the current image. This is a number between 0 and 1 and can be used to track + * The image quality of the current image. + * + * If the URL is set, this is a number between 0 and 1 and can be used to track * progressive progress. Calculated by dividing number of bytes / expected number of total bytes. + * This is zero until the first progressive render or the final display. + * + * If the URL is unset, this is 1 if defaultImage or image is set to non-nil. + * */ @property (nonatomic, assign, readonly) CGFloat currentImageQuality; /** - * The image quality (value between 0 and 1) of the last image that completed displaying. + * The currentImageQuality (value between 0 and 1) of the last image that completed displaying. */ @property (nonatomic, assign, readonly) CGFloat renderedImageQuality; @@ -130,6 +140,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - + /** * The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to * notifications such as finished decoding and downloading an image. @@ -137,6 +148,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 Additional information about the image load. + * + * @discussion Called on a background queue. + */ +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageLoadInfo *)info; + /** * Notification that the image node finished downloading an image. * diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 2b22ea526..76eaf7c21 100755 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -20,13 +20,17 @@ #import #import #import -#import +#import +#import #import #import #import #import #import #import +#import + +#import #if AS_PIN_REMOTE_IMAGE #import @@ -34,13 +38,13 @@ @interface ASNetworkImageNode () { - // Only access any of these with __instanceLock__. + // Only access any of these while locked. __weak id _delegate; - NSArray *_URLs; + NSURL *_URL; UIImage *_defaultImage; - NSUUID *_cacheUUID; + NSInteger _cacheSentinel; id _downloadIdentifier; // The download identifier that we have set a progress block on, if any. id _downloadIdentifierForProgressBlock; @@ -56,6 +60,7 @@ @interface ASNetworkImageNode () unsigned int delegateDidFailWithError:1; unsigned int delegateDidFinishDecoding:1; unsigned int delegateDidLoadImage:1; + unsigned int delegateDidLoadImageWithInfo:1; } _delegateFlags; @@ -66,7 +71,6 @@ @interface ASNetworkImageNode () unsigned int downloaderImplementsSetPriority:1; unsigned int downloaderImplementsAnimatedImage:1; unsigned int downloaderImplementsCancelWithResume:1; - unsigned int downloaderImplementsDownloadURLs:1; } _downloaderFlags; // Immutable and set on init only. We don't need to lock in this case. @@ -74,7 +78,6 @@ @interface ASNetworkImageNode () struct { unsigned int cacheSupportsClearing:1; unsigned int cacheSupportsSynchronousFetch:1; - unsigned int cacheSupportsCachedURLs:1; } _cacheFlags; } @@ -96,11 +99,9 @@ - (instancetype)initWithCache:(id)cache downloader:(id *)URLs { - if (URL) { - [self setURLs:@[URL] resetToDefault:reset]; - } else { - [self setURLs:nil resetToDefault:reset]; - } + [self setURL:[URLs firstObject]]; } -- (NSURL *)URL +// Deprecated +- (NSArray *)URLs { - return [self.URLs lastObject]; + return @[self.URL]; } -- (void)setURLs:(NSArray *)URLs +- (void)setURL:(NSURL *)URL { - [self setURLs:URLs resetToDefault:YES]; + [self setURL:URL resetToDefault:YES]; } -- (void)setURLs:(NSArray *)URLs resetToDefault:(BOOL)reset +- (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); - if (ASObjectIsEqual(URLs, _URLs)) { + if (ASObjectIsEqual(URL, _URL)) { return; } @@ -200,30 +195,30 @@ - (void)setURLs:(NSArray *)URLs resetToDefault:(BOOL)reset _imageWasSetExternally = NO; [self _locked_cancelImageDownloadWithResumePossibility:NO]; - + _imageLoaded = NO; - _URLs = URLs; + _URL = URL; - BOOL hasURL = (_URLs.count == 0); - if (reset || hasURL) { - [self _locked_setCurrentImageQuality:(hasURL ? 0.0 : 1.0)]; + // If URL is nil and URL was not equal to _URL (checked at the top), then we previously had a URL but it's been nil'd out. + BOOL hadURL = (URL == nil); + if (reset || hadURL) { + [self _setCurrentImageQuality:(hadURL ? 0.0 : 1.0)]; [self _locked__setImage:_defaultImage]; } } - + [self setNeedsPreload]; } -- (NSArray *)URLs +- (NSURL *)URL { - ASDN::MutexLocker l(__instanceLock__); - return _URLs; + return ASLockedSelf(_URL); } - (void)setDefaultImage:(UIImage *)defaultImage { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_setDefaultImage:defaultImage]; } @@ -237,86 +232,74 @@ - (void)_locked_setDefaultImage:(UIImage *)defaultImage _defaultImage = defaultImage; if (!_imageLoaded) { - [self _locked_setCurrentImageQuality:((_URLs.count == 0) ? 0.0 : 1.0)]; + [self _setCurrentImageQuality:((_URL == nil) ? 0.0 : 1.0)]; [self _locked__setImage:defaultImage]; - } } - (UIImage *)defaultImage { - ASDN::MutexLocker l(__instanceLock__); - return _defaultImage; + return ASLockedSelf(_defaultImage); } - (void)setCurrentImageQuality:(CGFloat)currentImageQuality { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _currentImageQuality = currentImageQuality; } - (CGFloat)currentImageQuality { - ASDN::MutexLocker l(__instanceLock__); - return _currentImageQuality; + return ASLockedSelf(_currentImageQuality); } /** - * Always use this methods internally to update the current image quality + * Always use these methods internally to update the current image quality * We want to maintain the order that currentImageQuality is set regardless of the calling thread, - * so we always have to dispatch to the main threadto ensure that we queue the operations in the correct order. + * so we always have to dispatch to the main thread to ensure that we queue the operations in the correct order. * (see comment in displayDidFinish) */ - (void)_setCurrentImageQuality:(CGFloat)imageQuality -{ - ASDN::MutexLocker l(__instanceLock__); - [self _locked_setCurrentImageQuality:imageQuality]; -} - -- (void)_locked_setCurrentImageQuality:(CGFloat)imageQuality { dispatch_async(dispatch_get_main_queue(), ^{ - // As the setting of the image quality is dispatched the lock is gone by the time the block is executing. - // Therefore we have to grab the lock again - __instanceLock__.lock(); - _currentImageQuality = imageQuality; - __instanceLock__.unlock(); + self.currentImageQuality = imageQuality; }); } - (void)setRenderedImageQuality:(CGFloat)renderedImageQuality { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _renderedImageQuality = renderedImageQuality; } - (CGFloat)renderedImageQuality { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _renderedImageQuality; } - (void)setDelegate:(id)delegate { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _delegate = delegate; _delegateFlags.delegateDidStartFetchingData = [delegate respondsToSelector:@selector(imageNodeDidStartFetchingData:)]; _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 { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _delegate; } - (void)setShouldRenderProgressImages:(BOOL)shouldRenderProgressImages { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (shouldRenderProgressImages == _shouldRenderProgressImages) { return; } @@ -328,14 +311,14 @@ - (void)setShouldRenderProgressImages:(BOOL)shouldRenderProgressImages - (BOOL)shouldRenderProgressImages { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _shouldRenderProgressImages; } - (BOOL)placeholderShouldPersist { - ASDN::MutexLocker l(__instanceLock__); - return (self.image == nil && self.animatedImage == nil && _URLs.count != 0); + ASLockScopeSelf(); + return (self.image == nil && self.animatedImage == nil && _URL != nil); } /* displayWillStartAsynchronously: in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary @@ -345,17 +328,24 @@ - (void)displayWillStartAsynchronously:(BOOL)asynchronously [super displayWillStartAsynchronously:asynchronously]; if (asynchronously == NO && _cacheFlags.cacheSupportsSynchronousFetch) { - ASDN::MutexLocker l(__instanceLock__); - - if (_imageLoaded == NO && _URLs.count > 0 && _downloadIdentifier == nil) { - for (NSURL *url in [_URLs reverseObjectEnumerator]) { - UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:url] asdk_image]; - if (result) { - [self _locked_setCurrentImageQuality:1.0]; - [self _locked__setImage:result]; - - _imageLoaded = YES; - break; + ASLockScopeSelf(); + + NSURL *url = _URL; + if (_imageLoaded == NO && url && _downloadIdentifier == nil) { + UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:url] asdk_image]; + if (result) { + [self _setCurrentImageQuality:1.0]; + [self _locked__setImage:result]; + _imageLoaded = YES; + + // Call out to the delegate. + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASUnlockScope(self); + auto info = [[ASNetworkImageLoadInfo alloc] initWithURL:url sourceType:ASNetworkImageSourceSynchronousCache downloadIdentifier:nil userInfo:nil]; + [_delegate imageNode:self didLoadImage:result info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { + ASUnlockScope(self); + [_delegate imageNode:self didLoadImage:result]; } } } @@ -365,9 +355,7 @@ - (void)displayWillStartAsynchronously:(BOOL)asynchronously [self didEnterPreloadState]; if (self.image == nil && _downloaderFlags.downloaderImplementsSetPriority) { - __instanceLock__.lock(); - id downloadIdentifier = _downloadIdentifier; - __instanceLock__.unlock(); + id downloadIdentifier = ASLockedSelf(_downloadIdentifier); if (downloadIdentifier != nil) { [_downloader setPriority:ASImageDownloaderPriorityImminent withDownloadIdentifier:downloadIdentifier]; } @@ -380,12 +368,10 @@ - (void)didEnterVisibleState { [super didEnterVisibleState]; - __instanceLock__.lock(); - id downloadIdentifier = nil; - if (_downloaderFlags.downloaderImplementsSetPriority) { - downloadIdentifier = _downloadIdentifier; - } - __instanceLock__.unlock(); + id downloadIdentifier = ({ + ASLockScopeSelf(); + _downloaderFlags.downloaderImplementsSetPriority ? _downloadIdentifier : nil; + }); if (downloadIdentifier != nil) { [_downloader setPriority:ASImageDownloaderPriorityVisible withDownloadIdentifier:downloadIdentifier]; @@ -398,12 +384,10 @@ - (void)didExitVisibleState { [super didExitVisibleState]; - __instanceLock__.lock(); - id downloadIdentifier = nil; - if (_downloaderFlags.downloaderImplementsSetPriority) { - downloadIdentifier = _downloadIdentifier; - } - __instanceLock__.unlock(); + id downloadIdentifier = ({ + ASLockScopeSelf(); + _downloaderFlags.downloaderImplementsSetPriority ? _downloadIdentifier : nil; + }); if (downloadIdentifier != nil) { [_downloader setPriority:ASImageDownloaderPriorityPreload withDownloadIdentifier:downloadIdentifier]; @@ -416,11 +400,8 @@ - (void)didExitPreloadState { [super didExitPreloadState]; - __instanceLock__.lock(); - BOOL imageWasSetExternally = _imageWasSetExternally; - __instanceLock__.unlock(); // If the image was set explicitly we don't want to remove it while exiting the preload state - if (imageWasSetExternally) { + if (ASLockedSelf(_imageWasSetExternally)) { return; } @@ -439,7 +420,7 @@ - (void)didEnterPreloadState - (void)handleProgressImage:(UIImage *)progressImage progress:(CGFloat)progress downloadIdentifier:(nullable id)downloadIdentifier { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // Getting a result back for a different download identifier, download must not have been successfully canceled if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { @@ -447,7 +428,7 @@ - (void)handleProgressImage:(UIImage *)progressImage progress:(CGFloat)progress } as_log_verbose(ASImageLoadingLog(), "Received progress image for %@ q: %.2g id: %@", self, progress, progressImage); - [self _locked_setCurrentImageQuality:progress]; + [self _setCurrentImageQuality:progress]; [self _locked__setImage:progressImage]; } @@ -459,12 +440,12 @@ - (void)_updateProgressImageBlockOnDownloaderIfNeeded } // Read state. - __instanceLock__.lock(); + [self lock]; BOOL shouldRender = _shouldRenderProgressImages && ASInterfaceStateIncludesVisible(_interfaceState); id oldDownloadIDForProgressBlock = _downloadIdentifierForProgressBlock; id newDownloadIDForProgressBlock = shouldRender ? _downloadIdentifier : nil; BOOL clearAndReattempt = NO; - __instanceLock__.unlock(); + [self unlock]; // If we're already bound to the correct download, we're done. if (ASObjectIsEqual(oldDownloadIDForProgressBlock, newDownloadIDForProgressBlock)) { @@ -488,7 +469,7 @@ - (void)_updateProgressImageBlockOnDownloaderIfNeeded // Update state local state with lock held. { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // Check if the oldDownloadIDForProgressBlock still is the same as the _downloadIdentifierForProgressBlock if (_downloadIdentifierForProgressBlock == oldDownloadIDForProgressBlock) { _downloadIdentifierForProgressBlock = newDownloadIDForProgressBlock; @@ -511,7 +492,7 @@ - (void)_updateProgressImageBlockOnDownloaderIfNeeded - (void)_cancelDownloadAndClearImageWithResumePossibility:(BOOL)storeResume { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_cancelDownloadAndClearImageWithResumePossibility:storeResume]; } @@ -520,24 +501,22 @@ - (void)_locked_cancelDownloadAndClearImageWithResumePossibility:(BOOL)storeResu [self _locked_cancelImageDownloadWithResumePossibility:storeResume]; [self _locked_setAnimatedImage:nil]; - [self _locked_setCurrentImageQuality:0.0]; + [self _setCurrentImageQuality:0.0]; [self _locked__setImage:_defaultImage]; _imageLoaded = NO; if (_cacheFlags.cacheSupportsClearing) { - if (_URLs.count != 0) { - as_log_verbose(ASImageLoadingLog(), "Clearing cached image for %@ url: %@", self, _URLs); - for (NSURL *url in _URLs) { - [_cache clearFetchedImageFromCacheWithURL:url]; - } + if (_URL != nil) { + as_log_verbose(ASImageLoadingLog(), "Clearing cached image for %@ url: %@", self, _URL); + [_cache clearFetchedImageFromCacheWithURL:_URL]; } } } - (void)_cancelImageDownloadWithResumePossibility:(BOOL)storeResume { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self _locked_cancelImageDownloadWithResumePossibility:storeResume]; } @@ -557,14 +536,13 @@ - (void)_locked_cancelImageDownloadWithResumePossibility:(BOOL)storeResume } } _downloadIdentifier = nil; - - _cacheUUID = nil; + _cacheSentinel++; } -- (void)_downloadImageWithCompletion:(void (^)(id imageContainer, NSError*, id downloadIdentifier))finished +- (void)_downloadImageWithCompletion:(void (^)(id imageContainer, NSError*, id downloadIdentifier, id userInfo))finished { ASPerformBlockOnBackgroundThread(^{ - NSArray *urls; + NSURL *url; id downloadIdentifier; BOOL cancelAndReattempt = NO; @@ -572,35 +550,24 @@ - (void)_downloadImageWithCompletion:(void (^)(id ima // We need to reobtain the lock after and ensure that the task we've kicked off still matches our URL. If not, we need to cancel // it and try again. { - ASDN::MutexLocker l(__instanceLock__); - urls = _URLs; + ASLockScopeSelf(); + url = _URL; } - if (_downloaderFlags.downloaderImplementsDownloadURLs) { - downloadIdentifier = [_downloader downloadImageWithURLs:urls - callbackQueue:dispatch_get_main_queue() - downloadProgress:NULL - completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { - if (finished != NULL) { - finished(imageContainer, error, downloadIdentifier); - } - }]; - } else { - downloadIdentifier = [_downloader downloadImageWithURL:[urls lastObject] - callbackQueue:dispatch_get_main_queue() - downloadProgress:NULL - completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) { - if (finished != NULL) { - finished(imageContainer, error, downloadIdentifier); - } - }]; - } - + + downloadIdentifier = [_downloader downloadImageWithURL:url + callbackQueue:dispatch_get_main_queue() + downloadProgress:NULL + completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { + if (finished != NULL) { + finished(imageContainer, error, downloadIdentifier, userInfo); + } + }]; as_log_verbose(ASImageLoadingLog(), "Downloading image for %@ url: %@", self, url); { - ASDN::MutexLocker l(__instanceLock__); - if (ASObjectIsEqual(_URLs, urls)) { + ASLockScopeSelf(); + if (ASObjectIsEqual(_URL, url)) { // The download we kicked off is correct, no need to do any more work. _downloadIdentifier = downloadIdentifier; } else { @@ -625,30 +592,28 @@ - (void)_downloadImageWithCompletion:(void (^)(id ima - (void)_lazilyLoadImageIfNecessary { - __instanceLock__.lock(); + [self lock]; __weak id delegate = _delegate; BOOL delegateDidStartFetchingData = _delegateFlags.delegateDidStartFetchingData; BOOL isImageLoaded = _imageLoaded; - NSArray *URLs = _URLs; + NSURL *URL = _URL; id currentDownloadIdentifier = _downloadIdentifier; - __instanceLock__.unlock(); + [self unlock]; - if (!isImageLoaded && URLs.count > 0 && currentDownloadIdentifier == nil) { + if (!isImageLoaded && URL != nil && currentDownloadIdentifier == nil) { if (delegateDidStartFetchingData) { [delegate imageNodeDidStartFetchingData:self]; } - // We only support file URLs if there is one URL currently - if (URLs.count == 1 && [URLs lastObject].isFileURL) { + if (URL.isFileURL) { dispatch_async(dispatch_get_main_queue(), ^{ - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // Bail out if not the same URL anymore - if (!ASObjectIsEqual(URLs, _URLs)) { + if (!ASObjectIsEqual(URL, _URL)) { return; } - NSURL *URL = [URLs lastObject]; if (_shouldCacheImage) { [self _locked__setImage:[UIImage imageNamed:URL.path.lastPathComponent]]; } else { @@ -686,101 +651,118 @@ - (void)_lazilyLoadImageIfNecessary _imageLoaded = YES; - [self _locked_setCurrentImageQuality:1.0]; + [self _setCurrentImageQuality:1.0]; - if (_delegateFlags.delegateDidLoadImage) { - ASDN::MutexUnlocker u(__instanceLock__); + if (_delegateFlags.delegateDidLoadImageWithInfo) { + ASUnlockScope(self); + auto info = [[ASNetworkImageLoadInfo alloc] initWithURL:URL sourceType:ASNetworkImageSourceFileURL downloadIdentifier:nil userInfo:nil]; + [delegate imageNode:self didLoadImage:self.image info:info]; + } else if (_delegateFlags.delegateDidLoadImage) { + ASUnlockScope(self); [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: %@ urls: %@", self, [imageContainer asdk_image], URLs); - - // 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; - } + auto finished = ^(id imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSourceType imageSource, id userInfo) { + ASPerformBlockOnBackgroundThread(^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } - //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) { - 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]]; + as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); + + // Grab the lock for the rest of the block + ASLockScope(strongSelf); + + //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; } - 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]; + + //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) + if (ASInterfaceStateIncludesPreload(strongSelf->_interfaceState) == NO) { + strongSelf->_downloadIdentifier = nil; + strongSelf->_cacheSentinel++; + return; } - } else if (error && strongSelf->_delegateFlags.delegateDidFailWithError) { - ASDN::MutexUnlocker u(strongSelf->__instanceLock__); - [delegate imageNode:strongSelf didFailWithError:error]; - } + + UIImage *newImage; + if (imageContainer != nil) { + [strongSelf _setCurrentImageQuality:1.0]; + NSData *animatedImageData = [imageContainer asdk_animatedImageData]; + if (animatedImageData && strongSelf->_downloaderFlags.downloaderImplementsAnimatedImage) { + id animatedImage = [strongSelf->_downloader animatedImageWithData:animatedImageData]; + [strongSelf _locked_setAnimatedImage:animatedImage]; + } else { + newImage = [imageContainer asdk_image]; + [strongSelf _locked__setImage:newImage]; + } + strongSelf->_imageLoaded = YES; + } + + strongSelf->_downloadIdentifier = nil; + strongSelf->_cacheSentinel++; + + void (^calloutBlock)(ASNetworkImageNode *inst); + + if (newImage) { + if (_delegateFlags.delegateDidLoadImageWithInfo) { + calloutBlock = ^(ASNetworkImageNode *strongSelf) { + auto info = [[ASNetworkImageLoadInfo alloc] initWithURL:URL sourceType:imageSource downloadIdentifier:downloadIdentifier userInfo:userInfo]; + [delegate imageNode:strongSelf didLoadImage:newImage info:info]; + }; + } else if (_delegateFlags.delegateDidLoadImage) { + calloutBlock = ^(ASNetworkImageNode *strongSelf) { + [delegate imageNode:strongSelf didLoadImage:newImage]; + }; + } + } else if (error && _delegateFlags.delegateDidFailWithError) { + calloutBlock = ^(ASNetworkImageNode *strongSelf) { + [delegate imageNode:strongSelf didFailWithError:error]; + }; + } + + if (calloutBlock) { + ASPerformBlockOnMainThread(^{ + if (auto strongSelf = weakSelf) { + calloutBlock(strongSelf); + } + }); + } + }); }; // As the _cache and _downloader is only set once in the intializer we don't have to use a // lock in here if (_cache != nil) { - NSUUID *cacheUUID = [NSUUID UUID]; - __instanceLock__.lock(); - _cacheUUID = cacheUUID; - __instanceLock__.unlock(); + NSInteger cacheSentinel = ASLockedSelf(++_cacheSentinel); - as_log_verbose(ASImageLoadingLog(), "Decaching image for %@ urls: %@", self, URLs); + 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)) { + // If the cache sentinel changed, that means this request was cancelled. + if (ASLockedSelf(_cacheSentinel != cacheSentinel)) { return; } if ([imageContainer asdk_image] == nil && _downloader != nil) { - [self _downloadImageWithCompletion:finished]; + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload, userInfo); + }]; } else { - as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs); - finished(imageContainer, nil, nil); + as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL); + finished(imageContainer, nil, nil, ASNetworkImageSourceAsynchronousCache, nil); } }; - - if (_cacheFlags.cacheSupportsCachedURLs) { - [_cache cachedImageWithURLs:URLs - callbackQueue:dispatch_get_main_queue() - completion:completion]; - } else { - [_cache cachedImageWithURL:[URLs lastObject] - callbackQueue:dispatch_get_main_queue() - completion:completion]; - } + [_cache cachedImageWithURL:URL + callbackQueue:dispatch_get_main_queue() + completion:completion]; } else { - [self _downloadImageWithCompletion:finished]; + [self _downloadImageWithCompletion:^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { + finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload, userInfo); + }]; } } } @@ -794,7 +776,8 @@ - (void)displayDidFinish id delegate = nil; - __instanceLock__.lock(); + { + ASLockScopeSelf(); if (_delegateFlags.delegateDidFinishDecoding && self.layer.contents != nil) { /* We store the image quality in _currentImageQuality whenever _image is set. On the following displayDidFinish, we'll know that _currentImageQuality is the quality of the image that has just finished rendering. In order for this to be accurate, we @@ -809,8 +792,7 @@ - (void)displayDidFinish // Assign the delegate to be used delegate = _delegate; } - - __instanceLock__.unlock(); + } if (delegate != nil) { [delegate imageNodeDidFinishDecoding:self]; diff --git a/Source/ASNodeController+Beta.h b/Source/ASNodeController+Beta.h index b21a86f41..df85f303f 100644 --- a/Source/ASNodeController+Beta.h +++ b/Source/ASNodeController+Beta.h @@ -31,6 +31,9 @@ - (void)loadNode; // for descriptions see definition +- (void)nodeDidLoad ASDISPLAYNODE_REQUIRES_SUPER; +- (void)nodeDidLayout ASDISPLAYNODE_REQUIRES_SUPER; + - (void)didEnterVisibleState ASDISPLAYNODE_REQUIRES_SUPER; - (void)didExitVisibleState ASDISPLAYNODE_REQUIRES_SUPER; diff --git a/Source/ASNodeController+Beta.m b/Source/ASNodeController+Beta.m index bf14df070..1bd873e76 100644 --- a/Source/ASNodeController+Beta.m +++ b/Source/ASNodeController+Beta.m @@ -93,6 +93,7 @@ - (void)setShouldInvertStrongReference:(BOOL)shouldInvertStrongReference } // subclass overrides +- (void)nodeDidLoad {} - (void)nodeDidLayout {} - (void)didEnterVisibleState {} diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index 719a45687..72c34df9e 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -221,9 +221,9 @@ - (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy [self setDelegate:nil]; } -- (void)didEnterVisibleState +- (void)didEnterHierarchy { - [super didEnterVisibleState]; + [super didEnterHierarchy]; // Check that our view controller does not automatically set our content insets // It would be better to have a -didEnterHierarchy hook to put this in, but diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index 6b08540cc..291cc4abf 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -20,8 +20,15 @@ NS_ASSUME_NONNULL_BEGIN +@protocol ASCATransactionQueueObserving +- (void)prepareForCATransactionCommit; +@end + +@interface ASAbstractRunLoopQueue : NSObject +@end + AS_SUBCLASSING_RESTRICTED -@interface ASRunLoopQueue : NSObject +@interface ASRunLoopQueue : ASAbstractRunLoopQueue /** * Create a new queue with the given run loop and handler. @@ -41,21 +48,44 @@ AS_SUBCLASSING_RESTRICTED - (void)enqueue:(ObjectType)object; -@property (nonatomic, readonly) BOOL isEmpty; +@property (atomic, readonly) BOOL isEmpty; @property (nonatomic, assign) NSUInteger batchSize; // Default == 1. @property (nonatomic, assign) BOOL ensureExclusiveMembership; // Default == YES. Set-like behavior. @end +AS_SUBCLASSING_RESTRICTED +@interface ASCATransactionQueue : ASAbstractRunLoopQueue + +@property (atomic, readonly) BOOL isEmpty; + +@property (atomic, readonly, getter=isEnabled) BOOL enabled; + +/** + * The queue to run on main run loop before CATransaction commit. + * + * @discussion this queue will run after ASRunLoopQueue and before CATransaction commit + * to get last chance of updating/coalesce info like interface state. + * Each node will only be called once per transaction commit to reflect interface change. + */ +@property (class, atomic, readonly) ASCATransactionQueue *sharedQueue; ++ (ASCATransactionQueue *)sharedQueue NS_RETURNS_RETAINED; + +- (void)enqueue:(id)object; + +@end + + AS_SUBCLASSING_RESTRICTED @interface ASDeallocQueue : NSObject -+ (instancetype)sharedDeallocationQueue; +@property (class, atomic, readonly) ASDeallocQueue *sharedDeallocationQueue; ++ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED; - (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 1902ad23d..99524bec0 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -16,6 +16,7 @@ // #import +#import #import #import #import @@ -25,6 +26,7 @@ #import #import #import +#import #define ASRunLoopQueueLoggingEnabled 0 #define ASRunLoopQueueVerboseLoggingEnabled 0 @@ -45,7 +47,7 @@ @implementation ASDeallocQueue { ASDN::RecursiveMutex _queueLock; } -+ (instancetype)sharedDeallocationQueue ++ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED { static ASDeallocQueue *deallocQueue = nil; static dispatch_once_t onceToken; @@ -55,16 +57,13 @@ + (instancetype)sharedDeallocationQueue 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; + if (objectPtr != NULL && *objectPtr != nil) { + ASDN::MutexLocker l(_queueLock); + _queue.push_back(*objectPtr); + *objectPtr = nil; } - - _queueLock.lock(); - _queue.push_back(object); - _queueLock.unlock(); } - (void)threadMain @@ -184,27 +183,6 @@ - (void)dealloc @end -#pragma mark - ASRunLoopQueue - -@interface ASRunLoopQueue () { - CFRunLoopRef _runLoop; - CFRunLoopSourceRef _runLoopSource; - CFRunLoopObserverRef _runLoopObserver; - NSPointerArray *_internalQueue; // Use NSPointerArray so we can decide __strong or __weak per-instance. - ASDN::RecursiveMutex _internalQueueLock; - - // In order to not pollute the top-level activities, each queue has 1 root activity. - os_activity_t _rootActivity; - -#if ASRunLoopQueueLoggingEnabled - NSTimer *_runloopQueueLoggingTimer; -#endif -} - -@property (nonatomic, copy) void (^queueConsumer)(id dequeuedItem, BOOL isQueueDrained); - -@end - #if AS_KDEBUG_ENABLE /** * This is real, private CA API. Valid as of iOS 10. @@ -221,7 +199,23 @@ + (int)currentState; @end #endif -@implementation ASRunLoopQueue +#pragma mark - ASAbstractRunLoopQueue + +@interface ASAbstractRunLoopQueue (Private) ++ (void)load; ++ (void)registerCATransactionObservers; +@end + +@implementation ASAbstractRunLoopQueue + +- (instancetype)init +{ + if (self != [super init]) { + return nil; + } + ASDisplayNodeAssert(self.class != [ASAbstractRunLoopQueue class], @"Should never create instances of abstract class ASAbstractRunLoopQueue."); + return self; +} #if AS_KDEBUG_ENABLE + (void)load @@ -244,14 +238,17 @@ + (void)registerCATransactionObservers } preLayoutHandler = ^{ ASSignpostStartCustom(ASSignpostCATransactionLayout, 0, [CATransaction currentState]); + NSLog(@"@@@@ preLayout"); }; preCommitHandler = ^{ int state = [CATransaction currentState]; ASSignpostEndCustom(ASSignpostCATransactionLayout, 0, state, ASSignpostColorDefault); ASSignpostStartCustom(ASSignpostCATransactionCommit, 0, state); + NSLog(@"@@@@ preCommit"); }; postCommitHandler = ^{ ASSignpostEndCustom(ASSignpostCATransactionCommit, 0, [CATransaction currentState], ASSignpostColorDefault); + NSLog(@"@@@@ postCommit"); // Can't add new observers inside an observer. rdar://problem/31253952 dispatch_async(dispatch_get_main_queue(), ^{ [self registerCATransactionObservers]; @@ -268,6 +265,31 @@ + (void)registerCATransactionObservers #endif // AS_KDEBUG_ENABLE +@end + +#pragma mark - ASRunLoopQueue + +@interface ASRunLoopQueue () { + CFRunLoopRef _runLoop; + CFRunLoopSourceRef _runLoopSource; + CFRunLoopObserverRef _runLoopObserver; + NSPointerArray *_internalQueue; // Use NSPointerArray so we can decide __strong or __weak per-instance. + ASDN::RecursiveMutex _internalQueueLock; + + // In order to not pollute the top-level activities, each queue has 1 root activity. + os_activity_t _rootActivity; + +#if ASRunLoopQueueLoggingEnabled + NSTimer *_runloopQueueLoggingTimer; +#endif +} + +@property (nonatomic, copy) void (^queueConsumer)(id dequeuedItem, BOOL isQueueDrained); + +@end + +@implementation ASRunLoopQueue + - (instancetype)initWithRunLoop:(CFRunLoopRef)runloop retainObjects:(BOOL)retainsObjects handler:(void (^)(id _Nullable, BOOL))handlerBlock { if (self = [super init]) { @@ -454,4 +476,254 @@ - (BOOL)isEmpty return _internalQueue.count == 0; } +#pragma mark - NSLocking + +- (void)lock +{ + _internalQueueLock.lock(); +} + +- (void)unlock +{ + _internalQueueLock.unlock(); +} + +@end + +#pragma mark - ASCATransactionQueue + +@interface ASCATransactionQueue () { + CFRunLoopRef _runLoop; + CFRunLoopSourceRef _runLoopSource; + CFRunLoopObserverRef _preTransactionObserver; + CFRunLoopObserverRef _postTransactionObserver; + NSPointerArray *_internalQueue; + ASDN::RecursiveMutex _internalQueueLock; + BOOL _CATransactionCommitInProgress; + + // In order to not pollute the top-level activities, each queue has 1 root activity. + os_activity_t _rootActivity; + +#if ASRunLoopQueueLoggingEnabled + NSTimer *_runloopQueueLoggingTimer; +#endif +} + +@end + +@implementation ASCATransactionQueue + +// CoreAnimation commit order is 2000000, the goal of this is to process shortly beforehand +// but after most other scheduled work on the runloop has processed. +static int const kASASCATransactionQueueOrder = 1000000; +// This will mark the end of current loop and any node enqueued between kASASCATransactionQueueOrder +// and kASASCATransactionQueuePostOrder will apply interface change immediately. +static int const kASASCATransactionQueuePostOrder = 3000000; + ++ (ASCATransactionQueue *)sharedQueue NS_RETURNS_RETAINED +{ + static dispatch_once_t onceToken; + static ASCATransactionQueue *sharedQueue; + dispatch_once(&onceToken, ^{ + sharedQueue = [[ASCATransactionQueue alloc] init]; + }); + return sharedQueue; +} + +- (instancetype)init +{ + if (self = [super init]) { + _runLoop = CFRunLoopGetMain(); + NSPointerFunctionsOptions options = NSPointerFunctionsStrongMemory; + _internalQueue = [[NSPointerArray alloc] initWithOptions:options]; + + // We don't want to pollute the top-level app activities with run loop batches, so we create one top-level + // activity per queue, and each batch activity joins that one instead. + _rootActivity = as_activity_create("Process run loop queue items", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DEFAULT); + { + // Log a message identifying this queue into the queue's root activity. + as_activity_scope_verbose(_rootActivity); + as_log_verbose(ASDisplayLog(), "Created run loop queue: %@", self); + } + + // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, + // __unsafe_unretained allows us to avoid flagging the memory cycle detector. + __unsafe_unretained __typeof__(self) weakSelf = self; + void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + NSLog(@"!!! preCommit"); +// [weakSelf processQueue]; + while (true) { + UIWindow *window = [[NSClassFromString(@"UIApplication") sharedApplication] keyWindow]; + if (!window.hidden) { + NSLog(@"^^^^ windowLayoutIfNeeded"); + [window layoutIfNeeded]; + } + if (_internalQueue.count > 0) { + [weakSelf processQueue]; + } else { + break; + } + } + }; + void (^postHandlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + ASDN::MutexLocker l(_internalQueueLock); + _CATransactionCommitInProgress = NO; + NSLog(@"!!! postCommit"); + }; + _preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueueOrder, handlerBlock); + _postTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueuePostOrder, postHandlerBlock); + + CFRunLoopAddObserver(_runLoop, _preTransactionObserver, kCFRunLoopCommonModes); + CFRunLoopAddObserver(_runLoop, _postTransactionObserver, kCFRunLoopCommonModes); + + // It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of + // the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done + CFRunLoopSourceContext sourceContext = {}; + sourceContext.perform = runLoopSourceCallback; +#if ASRunLoopQueueLoggingEnabled + sourceContext.info = (__bridge void *)self; +#endif + _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext); + CFRunLoopAddSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes); + +#if ASRunLoopQueueLoggingEnabled + _runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:_runloopQueueLoggingTimer forMode:NSRunLoopCommonModes]; +#endif + } + return self; +} + +- (void)dealloc +{ + CFRunLoopRemoveSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes); + CFRelease(_runLoopSource); + _runLoopSource = nil; + + if (CFRunLoopObserverIsValid(_preTransactionObserver)) { + CFRunLoopObserverInvalidate(_preTransactionObserver); + } + if (CFRunLoopObserverIsValid(_postTransactionObserver)) { + CFRunLoopObserverInvalidate(_postTransactionObserver); + } + CFRelease(_preTransactionObserver); + CFRelease(_postTransactionObserver); + _preTransactionObserver = nil; + _postTransactionObserver = nil; +} + +#if ASRunLoopQueueLoggingEnabled +- (void)checkRunLoop +{ + NSLog(@"<%@> - Jobs: %ld", self, _internalQueue.size()); +} +#endif + +- (void)processQueue +{ + // If we have an execution block, this vector will be populated, otherwise remains empty. + // This is to avoid needlessly retaining/releasing the objects if we don't have a block. + std::vector itemsToProcess; + + { + ASDN::MutexLocker l(_internalQueueLock); + + // Mark the queue will end coalescing shortly until after CATransactionCommit. + // This will give the queue a chance to apply any further interfaceState changes/enqueue + // immediately within current runloop instead of pushing the work to next runloop cycle. + _CATransactionCommitInProgress = YES; + + NSInteger internalQueueCount = _internalQueue.count; + // Early-exit if the queue is empty. + if (internalQueueCount == 0) { + return; + } + + ASSignpostStart(ASSignpostRunLoopQueueBatch); + + /** + * For each item in the next batch, if it's non-nil then NULL it out + * and if we have an execution block then add it in. + * This could be written a bunch of different ways but + * this particular one nicely balances readability, safety, and efficiency. + */ + NSInteger foundItemCount = 0; + for (NSInteger i = 0; i < internalQueueCount && foundItemCount < internalQueueCount; i++) { + /** + * It is safe to use unsafe_unretained here. If the queue is weak, the + * object will be added to the autorelease pool. If the queue is strong, + * it will retain the object until we transfer it (retain it) in itemsToProcess. + */ + __unsafe_unretained id ptr = (__bridge id)[_internalQueue pointerAtIndex:i]; + if (ptr != nil) { + foundItemCount++; + itemsToProcess.push_back(ptr); + [_internalQueue replacePointerAtIndex:i withPointer:NULL]; + } + } + + [_internalQueue compact]; + } + + // itemsToProcess will be empty if _queueConsumer == nil so no need to check again. + auto count = itemsToProcess.size(); + if (count > 0) { + as_activity_scope_verbose(as_activity_create("Process run loop queue batch", _rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + auto itemsEnd = itemsToProcess.cend(); + for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) { + __unsafe_unretained id value = *iterator; + [value prepareForCATransactionCommit]; + as_log_verbose(ASDisplayLog(), "processed %@", value); + } + if (count > 1) { + as_log_verbose(ASDisplayLog(), "processed %lu items", (unsigned long)count); + } + } + + ASSignpostEnd(ASSignpostRunLoopQueueBatch); +} + +- (void)enqueue:(id)object +{ + if (!object) { + return; + } + + if (!self.enabled || _CATransactionCommitInProgress) { + [object prepareForCATransactionCommit]; + return; + } + + ASDN::MutexLocker l(_internalQueueLock); + + // Check if the object exists. + BOOL foundObject = NO; + + for (id currentObject in _internalQueue) { + if (currentObject == object) { + foundObject = YES; + break; + } + } + + if (!foundObject) { + NSLog(@"^^^^ Adding object to queue: %@", object); + [_internalQueue addPointer:(__bridge void *)object]; + + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(_runLoop); + } +} + +- (BOOL)isEmpty +{ + ASDN::MutexLocker l(_internalQueueLock); + return _internalQueue.count == 0; +} + +- (BOOL)isEnabled +{ + return ASActivateExperimentalFeature(ASExperimentalInterfaceStateCoalescing); +} + @end diff --git a/Source/ASScrollNode.mm b/Source/ASScrollNode.mm index 7d1beaa3d..a7458f19d 100644 --- a/Source/ASScrollNode.mm +++ b/Source/ASScrollNode.mm @@ -18,9 +18,10 @@ #import #import #import -#import +#import #import #import +#import @interface ASScrollView : UIScrollView @end @@ -81,7 +82,7 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize { - ASDN::MutexLocker l(__instanceLock__); // Lock for using our instance variables. + ASLockScopeSelf(); // Lock for using our instance variables. ASSizeRange contentConstrainedSize = constrainedSize; if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections)) { @@ -99,10 +100,12 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize // 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; } @@ -121,7 +124,7 @@ - (void)layout { [super layout]; - ASDN::MutexLocker l(__instanceLock__); // Lock for using our two instance variables. + ASLockScopeSelf(); // Lock for using our two instance variables. if (_automaticallyManagesContentSize) { CGSize contentSize = _contentCalculatedSizeFromLayout; @@ -135,13 +138,13 @@ - (void)layout - (BOOL)automaticallyManagesContentSize { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _automaticallyManagesContentSize; } - (void)setAutomaticallyManagesContentSize:(BOOL)automaticallyManagesContentSize { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _automaticallyManagesContentSize = automaticallyManagesContentSize; if (_automaticallyManagesContentSize == YES && ASScrollDirectionContainsVerticalDirection(_scrollableDirections) == NO @@ -154,14 +157,17 @@ - (void)setAutomaticallyManagesContentSize:(BOOL)automaticallyManagesContentSize - (ASScrollDirection)scrollableDirections { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _scrollableDirections; } - (void)setScrollableDirections:(ASScrollDirection)scrollableDirections { - ASDN::MutexLocker l(__instanceLock__); - _scrollableDirections = scrollableDirections; + ASLockScopeSelf(); + if (_scrollableDirections != scrollableDirections) { + _scrollableDirections = scrollableDirections; + [self setNeedsLayout]; + } } @end diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index 8322faaf4..dc13110dd 100644 --- a/Source/ASTableNode.h +++ b/Source/ASTableNode.h @@ -172,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. @@ -198,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. @@ -209,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:. @@ -238,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:(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 d68e4a9d7..6420d8f70 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -104,6 +104,18 @@ - (instancetype)init return [self initWithStyle:UITableViewStylePlain]; } +#if ASDISPLAYNODE_ASSERTIONS_ENABLED +- (void)dealloc +{ + if (self.nodeLoaded) { + __weak UIView *view = self.view; + ASPerformBlockOnMainThread(^{ + ASDisplayNodeCAssertNil(view.superview, @"Node's view should be removed from hierarchy."); + }); + } +} +#endif + #pragma mark ASDisplayNode - (void)didLoad @@ -764,8 +776,11 @@ - (BOOL)isProcessingUpdates return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO); } -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +- (void)onDidFinishProcessingUpdates:(void (^)())completion { + if (!completion) { + return; + } if (!self.nodeLoaded) { completion(); } else { diff --git a/Source/ASTableView.h b/Source/ASTableView.h index ba6736eba..38b5bffb1 100644 --- a/Source/ASTableView.h +++ b/Source/ASTableView.h @@ -180,7 +180,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. @@ -219,7 +219,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:(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."); diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 7bcd0fd5c..a9396bd64 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -114,11 +114,14 @@ - (void)setElement:(ASCollectionElement *)element if (node) { self.backgroundColor = node.backgroundColor; - self.selectionStyle = node.selectionStyle; self.selectedBackgroundView = node.selectedBackgroundView; +#if TARGET_OS_IOS self.separatorInset = node.separatorInset; - self.selectionStyle = node.selectionStyle; +#endif + self.selectionStyle = node.selectionStyle; + self.focusStyle = node.focusStyle; self.accessoryType = node.accessoryType; + self.tintColor = node.tintColor; // the following ensures that we clip the entire cell to it's bounds if node.clipsToBounds is set (the default) // This is actually a workaround for a bug we are seeing in some rare cases (selected background view @@ -130,6 +133,15 @@ - (void)setElement:(ASCollectionElement *)element [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]; @@ -178,15 +190,6 @@ @interface ASTableView () = 9. - */ - CALayer *_retainedLayer; CGFloat _nodesConstrainedWidth; BOOL _queuedNodeHeightUpdate; @@ -215,7 +218,12 @@ @interface ASTableView () *)convertIndexPathsToTableNode:(NSArray *)indexPaths @@ -733,7 +725,7 @@ - (BOOL)isProcessingUpdates return [_dataController isProcessingUpdates]; } -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +- (void)onDidFinishProcessingUpdates:(void (^)())completion { [_dataController onDidFinishProcessingUpdates:completion]; } @@ -760,7 +752,7 @@ - (void)layoutSubviews _nodesConstrainedWidth = constrainedWidth; [self beginUpdates]; - [_dataController relayoutAllNodes]; + [_dataController relayoutAllNodesWithInvalidationBlock:nil]; [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; } else { if (_cellsForLayoutUpdates.count > 0) { @@ -930,6 +922,7 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa ASCellNode *node = [_dataController.visibleMap elementForItemAtIndexPath:indexPath].node; CGFloat height = node.calculatedSize.height; +#if TARGET_OS_IOS /** * Weirdly enough, Apple expects the return value here to _include_ the height * of the separator, if there is one! So if our node wants to be 43.5, we need @@ -939,6 +932,8 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa if (tableView.separatorStyle != UITableViewCellSeparatorStyleNone) { height += 1.0 / ASScreenScale(); } +#endif + return height; } @@ -983,7 +978,14 @@ - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sou - (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; @@ -1003,15 +1005,22 @@ - (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)c [_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]; @@ -1215,14 +1224,10 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView [super scrollViewDidScroll:scrollView]; return; } - // If a scroll happenes the current range mode needs to go to full ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; if (ASInterfaceStateIncludesVisible(interfaceState)) { - _rangeController.contentOffsetHasChanged = YES; - [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; [self _checkForBatchFetching]; - } - + } for (_ASTableViewCell *tableCell in _cellsForVisibilityUpdates) { [[tableCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView @@ -1274,6 +1279,10 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView [super scrollViewWillBeginDragging:scrollView]; return; } + // If a scroll happens the current range mode needs to go to full + _rangeController.contentHasBeenScrolled = YES; + [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; + for (_ASTableViewCell *tableViewCell in _cellsForVisibilityUpdates) { [[tableViewCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging inScrollView:scrollView @@ -1319,7 +1328,12 @@ - (CGFloat)leadingScreensForBatching - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { - _leadingScreensForBatching = leadingScreensForBatching; + if (_leadingScreensForBatching != leadingScreensForBatching) { + _leadingScreensForBatching = leadingScreensForBatching; + ASPerformBlockOnMainThread(^{ + [self _checkForBatchFetching]; + }); + } } - (BOOL)automaticallyAdjustsContentOffset @@ -1762,6 +1776,7 @@ - (BOOL)dataController:(ASDataController *)dataController presentedSizeForElemen } CGRect rect = [self rectForRowAtIndexPath:indexPath]; +#if TARGET_OS_IOS /** * Weirdly enough, Apple expects the return value in tableView:heightForRowAtIndexPath: to _include_ the height * of the separator, if there is one! So if rectForRow would return 44.0 we need to use 43.5. @@ -1769,6 +1784,7 @@ - (BOOL)dataController:(ASDataController *)dataController presentedSizeForElemen if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { rect.size.height -= 1.0 / ASScreenScale(); } +#endif return (fabs(rect.size.height - size.height) < FLT_EPSILON); } @@ -1890,16 +1906,24 @@ - (void)willMoveToWindow:(UIWindow *)newWindow - (void)didMoveToWindow { BOOL visible = (self.window != nil); + BOOL rangeControllerUpdated = NO; + ASDisplayNode *node = self.tableNode; if (!visible && node.inHierarchy) { + if (![node supportsRangeManagedInterfaceState]) { + rangeControllerUpdated = YES; + // Exit CellNodes first before Collection to match UIKit behaviors (tear down bottom up). + // Although we have not yet cleared the interfaceState's Visible bit (this happens in __exitHierarchy), + // the ASRangeController will get the correct value from -interfaceStateForRangeController:. + [_rangeController updateRanges]; + } [node __exitHierarchy]; } // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their // their update in the layout pass - if (![node supportsRangeManagedInterfaceState]) { - [_rangeController setNeedsUpdate]; - [_rangeController updateIfNeeded]; + if (![node supportsRangeManagedInterfaceState] && !rangeControllerUpdated) { + [_rangeController updateRanges]; } // When we aren't visible, we will only fetch up to the visible area. Now that we are visible, @@ -1909,4 +1933,18 @@ - (void)didMoveToWindow } } +- (void)willMoveToSuperview:(UIView *)newSuperview +{ + if (self.superview == nil && newSuperview != nil) { + _keepalive_node = self.tableNode; + } +} + +- (void)didMoveToSuperview +{ + if (self.superview == nil) { + _keepalive_node = nil; + } +} + @end diff --git a/Source/ASTextNode+Beta.h b/Source/ASTextNode+Beta.h index 8c7556811..d60e0c13c 100644 --- a/Source/ASTextNode+Beta.h +++ b/Source/ASTextNode+Beta.h @@ -21,20 +21,6 @@ NS_ASSUME_NONNULL_BEGIN -// When enabled, use ASTextNode2 for subclasses, random instances, or all instances of ASTextNode. -// See ASAvailability.h declaration of ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE for a compile-time option. -typedef NS_OPTIONS(NSUInteger, ASTextNodeExperimentOptions) { - // All subclass instances use the experimental implementation. - ASTextNodeExperimentSubclasses = 1 << 0, - // Random instances of ASTextNode (50% chance) (not subclasses) use experimental impl. - // Useful for profiling with apps that have no custom text node subclasses. - ASTextNodeExperimentRandomInstances = 1 << 1, - // All instances of ASTextNode itself use experimental implementation. Supersedes `.randomInstances`. - ASTextNodeExperimentAllInstances = 1 << 2, - // Add highlighting etc. for debugging. - ASTextNodeExperimentDebugging = 1 << 3 -}; - @interface ASTextNode () /** @@ -52,14 +38,6 @@ typedef NS_OPTIONS(NSUInteger, ASTextNodeExperimentOptions) { */ @property (nonatomic, assign) UIEdgeInsets textContainerInset; -/** - * Opt in to an experimental implementation of text node. The implementation may improve performance and correctness, - * but may not support all features and has not been thoroughly tested in production. - * - * @precondition You may not call this after allocating any text nodes. You may only call this once. - */ -+ (void)setExperimentOptions:(ASTextNodeExperimentOptions)options; - /** * Returns YES if this node is using the experimental implementation. NO otherwise. Will not change. */ diff --git a/Source/ASTextNode.h b/Source/ASTextNode.h index f3670bb0f..269ca8f81 100644 --- a/Source/ASTextNode.h +++ b/Source/ASTextNode.h @@ -17,31 +17,10 @@ #import #import -#if ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE - #import -#endif +#import NS_ASSUME_NONNULL_BEGIN -@protocol ASTextNodeDelegate; - -#if !ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE - -/** - * Highlight styles. - */ -typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { - /** - * Highlight style for text on a light background. - */ - ASTextNodeHighlightStyleLight, - - /** - * Highlight style for text on a dark background. - */ - ASTextNodeHighlightStyleDark -}; - /** @abstract Draws interactive rich text. @discussion Backed by TextKit. @@ -238,70 +217,6 @@ typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { @end -#else - -@interface ASTextNode : ASTextNode2 -@end - -#endif - -/** - * @abstract Text node delegate. - */ -@protocol ASTextNodeDelegate -@optional - -/** - @abstract Indicates to the delegate that a link was tapped within a text node. - @param textNode The ASTextNode containing the link that was tapped. - @param attribute The attribute that was tapped. Will not be nil. - @param value The value of the tapped attribute. - @param point The point within textNode, in textNode's coordinate system, that was tapped. - @param textRange The range of highlighted text. - */ -- (void)textNode:(ASTextNode *)textNode tappedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; - -/** - @abstract Indicates to the delegate that a link was tapped within a text node. - @param textNode The ASTextNode containing the link that was tapped. - @param attribute The attribute that was tapped. Will not be nil. - @param value The value of the tapped attribute. - @param point The point within textNode, in textNode's coordinate system, that was tapped. - @param textRange The range of highlighted text. - @discussion In addition to implementing this method, the delegate must be set on the text - node before it is loaded (the recognizer is created in -didLoad) - */ -- (void)textNode:(ASTextNode *)textNode longPressedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; - -//! @abstract Called when the text node's truncation string has been tapped. -- (void)textNodeTappedTruncationToken:(ASTextNode *)textNode; - -/** - @abstract Indicates to the text node if an attribute should be considered a link. - @param textNode The text node containing the entity attribute. - @param attribute The attribute that was tapped. Will not be nil. - @param value The value of the tapped attribute. - @param point The point within textNode, in textNode's coordinate system, that was touched to trigger a highlight. - @discussion If not implemented, the default value is YES. - @return YES if the entity attribute should be a link, NO otherwise. - */ -- (BOOL)textNode:(ASTextNode *)textNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point; - -/** - @abstract Indicates to the text node if an attribute is a valid long-press target - @param textNode The text node containing the entity attribute. - @param attribute The attribute that was tapped. Will not be nil. - @param value The value of the tapped attribute. - @param point The point within textNode, in textNode's coordinate system, that was long-pressed. - @discussion If not implemented, the default value is NO. - @return YES if the entity attribute should be treated as a long-press target, NO otherwise. - */ -- (BOOL)textNode:(ASTextNode *)textNode shouldLongPressLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point; - -@end - -#if !ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE - @interface ASTextNode (Unavailable) - (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock __unavailable; @@ -334,6 +249,4 @@ typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 6caaec8cd..e98dfb261 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -18,16 +18,18 @@ #import #import -#if !ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE #import -#include +#import #import #import -#import +#import +#import +#import #import #import +#import #import #import @@ -39,6 +41,7 @@ #import #import #import +#import /** * If set, we will record all values set to attributedText into an array @@ -62,10 +65,13 @@ @interface ASTextNodeRendererKey : NSObject @property (assign, nonatomic) CGSize constrainedSize; @end -@implementation ASTextNodeRendererKey +@implementation ASTextNodeRendererKey { + std::mutex _m; +} - (NSUInteger)hash { + std::lock_guard _l(_m); #pragma clang diagnostic push #pragma clang diagnostic warning "-Wpadded" struct { @@ -85,7 +91,14 @@ - (BOOL)isEqual:(ASTextNodeRendererKey *)object return YES; } - return _attributes == object.attributes && CGSizeEqualToSize(_constrainedSize, object.constrainedSize); + // NOTE: Skip the class check for this specialized, internal Key object. + + // Lock both objects, avoiding deadlock. + std::lock(_m, object->_m); + std::lock_guard lk1(_m, std::adopt_lock); + std::lock_guard lk2(object->_m, std::adopt_lock); + + return _attributes == object->_attributes && CGSizeEqualToSize(_constrainedSize, object->_constrainedSize); } @end @@ -176,6 +189,7 @@ @implementation ASTextNode { NSArray *_exclusionPaths; NSAttributedString *_attributedText; + NSAttributedString *_truncationAttributedText; NSAttributedString *_composedTruncationText; NSString *_highlightedLinkAttributeName; @@ -229,6 +243,8 @@ - (void)dealloc { CGColorRelease(_shadowColor); + // TODO: This may not be necessary post-iOS-9 when most UIKit assign APIs + // were changed to weak. if (_longPressGestureRecognizer) { _longPressGestureRecognizer.delegate = nil; [_longPressGestureRecognizer removeTarget:nil action:NULL]; @@ -309,13 +325,13 @@ - (BOOL)supportsLayerBacking - (ASTextKitRenderer *)_renderer { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return [self _locked_renderer]; } - (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return [self _locked_rendererWithBounds:bounds]; } @@ -351,27 +367,19 @@ - (ASTextKitAttributes)_locked_rendererAttributes - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { - __instanceLock__.lock(); - BOOL needsUpdate = !UIEdgeInsetsEqualToEdgeInsets(textContainerInset, _textContainerInset); - if (needsUpdate) { - _textContainerInset = textContainerInset; - } - __instanceLock__.unlock(); - - if (needsUpdate) { + if (ASLockedSelfCompareAssignCustom(_textContainerInset, textContainerInset, UIEdgeInsetsEqualToEdgeInsets)) { [self setNeedsLayout]; } } - (UIEdgeInsets)textContainerInset { - ASDN::MutexLocker l(__instanceLock__); - return _textContainerInset; + return ASLockedSelf(_textContainerInset); } - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); @@ -420,7 +428,7 @@ + (CGFloat)ascenderWithAttributedString:(NSAttributedString *)attributedString - (NSAttributedString *)attributedText { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _attributedText; } @@ -433,7 +441,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Don't hold textLock for too long. { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (ASObjectIsEqual(attributedText, _attributedText)) { return; } @@ -447,10 +455,10 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Since truncation text matches style of attributedText, invalidate it now. [self _invalidateTruncationText]; - NSUInteger length = attributedText.length; + NSUInteger length = _attributedText.length; if (length > 0) { - self.style.ascender = [[self class] ascenderWithAttributedString:attributedText]; - self.style.descender = [[attributedText attribute:NSFontAttributeName atIndex:attributedText.length - 1 effectiveRange:NULL] descender]; + self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText]; + self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:length - 1 effectiveRange:NULL] descender]; } // Tell the display node superclasses that the cached layout is incorrect now @@ -461,7 +469,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Accessiblity - self.accessibilityLabel = attributedText.string; + self.accessibilityLabel = _attributedText.string; self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string. } @@ -469,32 +477,22 @@ - (void)setAttributedText:(NSAttributedString *)attributedText - (void)setExclusionPaths:(NSArray *)exclusionPaths { - { - ASDN::MutexLocker l(__instanceLock__); - - if (ASObjectIsEqual(exclusionPaths, _exclusionPaths)) { - return; - } - - _exclusionPaths = [exclusionPaths copy]; + if (ASLockedSelfCompareAssignCopy(_exclusionPaths, exclusionPaths)) { + [self setNeedsLayout]; + [self setNeedsDisplay]; } - - [self setNeedsLayout]; - [self setNeedsDisplay]; } - (NSArray *)exclusionPaths { - ASDN::MutexLocker l(__instanceLock__); - - return _exclusionPaths; + return ASLockedSelf(_exclusionPaths); } #pragma mark - Drawing - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return [[ASTextNodeDrawParameter alloc] initWithRendererAttributes:[self _locked_rendererAttributes] backgroundColor:self.backgroundColor @@ -539,14 +537,14 @@ - (id)linkAttributeValueAtPoint:(CGPoint)point } - (id)_linkAttributeValueAtPoint:(CGPoint)point - attributeName:(out NSString **)attributeNameOut + attributeName:(out NSString * __autoreleasing *)attributeNameOut range:(out NSRange *)rangeOut inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); ASTextKitRenderer *renderer = [self _locked_renderer]; NSRange visibleRange = renderer.firstVisibleRange; @@ -673,14 +671,14 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer - (ASTextNodeHighlightStyle)highlightStyle { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _highlightStyle; } - (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _highlightStyle = highlightStyle; } @@ -764,8 +762,8 @@ - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *) } if (highlightTargetLayer != nil) { - ASDN::MutexLocker l(__instanceLock__); - ASTextKitRenderer *renderer = [self _renderer]; + ASLockScopeSelf(); + ASTextKitRenderer *renderer = [self _locked_renderer]; NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; @@ -852,7 +850,7 @@ - (NSArray *)highlightRectsForTextRange:(NSRange)textRange - (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRendererMeasureOption)measureOption { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); NSArray *rects = [[self _locked_renderer] rectsForTextRange:textRange measureOption:measureOption]; NSMutableArray *adjustedRects = [NSMutableArray array]; @@ -870,7 +868,7 @@ - (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRende - (CGRect)trailingRect { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); CGRect rect = [[self _locked_renderer] trailingRect]; return ASTextNodeAdjustRenderRectForShadowPadding(rect, self.shadowPadding); @@ -878,7 +876,7 @@ - (CGRect)trailingRect - (CGRect)frameForTextRange:(NSRange)textRange { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); CGRect frame = [[self _locked_renderer] frameForTextRange:textRange]; return ASTextNodeAdjustRenderRectForShadowPadding(frame, self.shadowPadding); @@ -888,12 +886,9 @@ - (CGRect)frameForTextRange:(NSRange)textRange - (void)setPlaceholderColor:(UIColor *)placeholderColor { - ASDN::MutexLocker l(__instanceLock__); - - _placeholderColor = placeholderColor; - - // prevent placeholders if we don't have a color - self.placeholderEnabled = placeholderColor != nil; + if (ASLockedSelfCompareAssignCopy(_placeholderColor, placeholderColor)) { + self.placeholderEnabled = CGColorGetAlpha(placeholderColor.CGColor) > 0; + } } - (UIImage *)placeholderImage @@ -905,9 +900,9 @@ - (UIImage *)placeholderImage return nil; } - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); - UIGraphicsBeginImageContext(size); + ASGraphicsBeginImageContextWithOptions(size, NO, 1.0); [self.placeholderColor setFill]; ASTextKitRenderer *renderer = [self _locked_renderer]; @@ -926,8 +921,7 @@ - (UIImage *)placeholderImage } } - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); return image; } @@ -987,7 +981,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event if (inAdditionalTruncationMessage) { NSRange visibleRange = NSMakeRange(0, 0); { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); visibleRange = [self _locked_renderer].firstVisibleRange; } NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange]; @@ -1066,14 +1060,14 @@ - (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer - (BOOL)_pendingLinkTap { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && _delegate != nil; } - (BOOL)_pendingTruncationTap { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return [_highlightedLinkAttributeName isEqualToString:ASTextNodeTruncationTokenAttributeName]; } @@ -1082,96 +1076,65 @@ - (BOOL)_pendingTruncationTap - (CGColorRef)shadowColor { - ASDN::MutexLocker l(__instanceLock__); - - return _shadowColor; + return ASLockedSelf(_shadowColor); } - (void)setShadowColor:(CGColorRef)shadowColor { - __instanceLock__.lock(); + [self lock]; if (_shadowColor != shadowColor && CGColorEqualToColor(shadowColor, _shadowColor) == NO) { CGColorRelease(_shadowColor); _shadowColor = CGColorRetain(shadowColor); _cachedShadowUIColor = [UIColor colorWithCGColor:shadowColor]; - __instanceLock__.unlock(); + [self unlock]; [self setNeedsDisplay]; return; } - __instanceLock__.unlock(); + [self unlock]; } - (CGSize)shadowOffset { - ASDN::MutexLocker l(__instanceLock__); - - return _shadowOffset; + return ASLockedSelf(_shadowOffset); } - (void)setShadowOffset:(CGSize)shadowOffset { - { - ASDN::MutexLocker l(__instanceLock__); - - if (CGSizeEqualToSize(_shadowOffset, shadowOffset)) { - return; - } - _shadowOffset = shadowOffset; + if (ASLockedSelfCompareAssignCustom(_shadowOffset, shadowOffset, CGSizeEqualToSize)) { + [self setNeedsDisplay]; } - - [self setNeedsDisplay]; } - (CGFloat)shadowOpacity { - ASDN::MutexLocker l(__instanceLock__); - - return _shadowOpacity; + return ASLockedSelf(_shadowOpacity); } - (void)setShadowOpacity:(CGFloat)shadowOpacity { - { - ASDN::MutexLocker l(__instanceLock__); - - if (_shadowOpacity == shadowOpacity) { - return; - } - - _shadowOpacity = shadowOpacity; + if (ASLockedSelfCompareAssign(_shadowOpacity, shadowOpacity)) { + [self setNeedsDisplay]; } - - [self setNeedsDisplay]; } - (CGFloat)shadowRadius { - ASDN::MutexLocker l(__instanceLock__); - - return _shadowRadius; + return ASLockedSelf(_shadowRadius); } - (void)setShadowRadius:(CGFloat)shadowRadius { - { - ASDN::MutexLocker l(__instanceLock__); - - if (_shadowRadius == shadowRadius) { - return; - } - - _shadowRadius = shadowRadius; + if (ASLockedSelfCompareAssign(_shadowRadius, shadowRadius)) { + [self setNeedsDisplay]; } - - [self setNeedsDisplay]; } - (UIEdgeInsets)shadowPadding { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return [self _locked_renderer].shadower.shadowPadding; } @@ -1187,92 +1150,54 @@ - (UIEdgeInsets)shadowPadding return defaultTruncationAttributedString; } -- (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText +- (NSAttributedString *)truncationAttributedText { - { - ASDN::MutexLocker l(__instanceLock__); - - if (ASObjectIsEqual(_truncationAttributedText, truncationAttributedText)) { - return; - } + return ASLockedSelf(_truncationAttributedText); +} - _truncationAttributedText = [truncationAttributedText copy]; +- (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText +{ + if (ASLockedSelfCompareAssignCopy(_truncationAttributedText, truncationAttributedText)) { + [self _invalidateTruncationText]; } - - [self _invalidateTruncationText]; } - (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage { - { - ASDN::MutexLocker l(__instanceLock__); - - if (ASObjectIsEqual(_additionalTruncationMessage, additionalTruncationMessage)) { - return; - } - - _additionalTruncationMessage = [additionalTruncationMessage copy]; + if (ASLockedSelfCompareAssignCopy(_additionalTruncationMessage, additionalTruncationMessage)) { + [self _invalidateTruncationText]; } - - [self _invalidateTruncationText]; } - (void)setTruncationMode:(NSLineBreakMode)truncationMode { - { - ASDN::MutexLocker l(__instanceLock__); - - if (_truncationMode == truncationMode) { - return; - } - - _truncationMode = truncationMode; + if (ASLockedSelfCompareAssign(_truncationMode, truncationMode)) { + [self setNeedsDisplay]; } - - [self setNeedsDisplay]; } - (BOOL)isTruncated { - ASDN::MutexLocker l(__instanceLock__); - - return [[self _locked_renderer] isTruncated]; + return ASLockedSelf([[self _locked_renderer] isTruncated]); } - (void)setPointSizeScaleFactors:(NSArray *)pointSizeScaleFactors { - { - ASDN::MutexLocker l(__instanceLock__); - if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors]) { - return; - } - - _pointSizeScaleFactors = pointSizeScaleFactors; + if (ASLockedSelfCompareAssignCopy(_pointSizeScaleFactors, pointSizeScaleFactors)) { + [self setNeedsDisplay]; } - - [self setNeedsDisplay]; } - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines { - { - ASDN::MutexLocker l(__instanceLock__); - - if (_maximumNumberOfLines == maximumNumberOfLines) { - return; - } - - _maximumNumberOfLines = maximumNumberOfLines; + if (ASLockedSelfCompareAssign(_maximumNumberOfLines, maximumNumberOfLines)) { + [self setNeedsDisplay]; } - - [self setNeedsDisplay]; } - (NSUInteger)lineCount { - ASDN::MutexLocker l(__instanceLock__); - - return [[self _locked_renderer] lineCount]; + return ASLockedSelf([[self _locked_renderer] lineCount]); } #pragma mark - Truncation Message @@ -1280,7 +1205,7 @@ - (NSUInteger)lineCount - (void)_invalidateTruncationText { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _composedTruncationText = nil; } @@ -1293,7 +1218,7 @@ - (void)_invalidateTruncationText */ - (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // Check if we even have an additional truncation message. if (!_additionalTruncationMessage) { @@ -1384,69 +1309,35 @@ + (void)_registerAttributedText:(NSAttributedString *)str } #endif -// 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; - -+ (void)setExperimentOptions:(ASTextNodeExperimentOptions)options ++ (id)allocWithZone:(struct _NSZone *)zone { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ASDN::StaticMutexLocker lock(_experimentLock); - - // They must call this before allocating any text nodes. - ASDisplayNodeAssertFalse(_hasAllocatedNode); - - _experimentOptions = options; - - // Set superclass of all subclasses to ASTextNode2 - if (options & ASTextNodeExperimentSubclasses) { - unsigned int classCount; - Class originalClass = [ASTextNode class]; - Class newClass = [ASTextNode2 class]; - Class *classes = objc_copyClassList(&classCount); - for (int i = 0; i < classCount; i++) { - Class c = classes[i]; - if (class_getSuperclass(c) == originalClass) { + // If they're not experimenting, just forward. + if (!ASActivateExperimentalFeature(ASExperimentalTextNode)) { + return [super allocWithZone:zone]; + } + + // We are plain ASTextNode. Just swap in an ASTextNode2 instead. + if (self == [ASTextNode class]) { + return (ASTextNode *)[ASTextNode2 allocWithZone:zone]; + } + + // We are descended from ASTextNode. We need to change the superclass for the + // ASTextNode subclass to ASTextNode2. + // Walk up the class hierarchy until we find ASTextNode. + Class s; + for (Class c = self; c != [ASTextNode class]; c = s) { + s = class_getSuperclass(c); + if (s == [ASTextNode class]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - class_setSuperclass(c, newClass); + // Direct descendent. Update superclass of c and end. + class_setSuperclass(c, [ASTextNode2 class]); #pragma clang diagnostic pop - } - } - free(classes); - } - - if (options & ASTextNodeExperimentDebugging) { - [ASTextNode2 enableDebugging]; + break; } - }); -} - -+ (id)allocWithZone:(struct _NSZone *)zone -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ASDN::StaticMutexLocker lock(_experimentLock); - _hasAllocatedNode = YES; - }); - - // All instances || (random instances && rand() != 0) - BOOL useExperiment = (_experimentOptions & ASTextNodeExperimentAllInstances) - || ((_experimentOptions & ASTextNodeExperimentRandomInstances) - && (arc4random_uniform(2) != 0)); - - if (useExperiment) { - return (ASTextNode *)[ASTextNode2 allocWithZone:zone]; - } else { - return [super allocWithZone:zone]; } -} -- (BOOL)usingExperiment -{ - return NO; + return [super allocWithZone:zone]; } @end @@ -1474,10 +1365,3 @@ - (NSAttributedString *)truncationAttributedString } @end - -#else - -@implementation ASTextNode -@end - -#endif diff --git a/Source/ASTextNode2.h b/Source/ASTextNode2.h index aef3ac468..9eecbf546 100644 --- a/Source/ASTextNode2.h +++ b/Source/ASTextNode2.h @@ -11,28 +11,7 @@ // #import - -#if !ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE -// Import this to get ASTextNodeHighlightStyle -#import -#else -@protocol ASTextNodeDelegate; - -/** - * Highlight styles. - */ -typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { - /** - * Highlight style for text on a light background. - */ - ASTextNodeHighlightStyleLight, - - /** - * Highlight style for text on a dark background. - */ - ASTextNodeHighlightStyleDark -}; -#endif +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index ecd9496c5..0569a8988 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -17,7 +17,8 @@ #import #import -#import +#import +#import #import #import @@ -30,6 +31,7 @@ #import #import #import +#import @interface ASTextCacheValue : NSObject { @package @@ -232,7 +234,7 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize [self _ensureTruncationText]; NSMutableAttributedString *mutableText = [attributedText mutableCopy]; - [self prepareAttributedStringForDrawing:mutableText]; + [self prepareAttributedString:mutableText]; ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:container text:mutableText]; [self setNeedsDisplay]; @@ -259,7 +261,7 @@ + (CGFloat)ascenderWithAttributedString:(NSAttributedString *)attributedString - (NSAttributedString *)attributedText { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _attributedText; } @@ -272,7 +274,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Don't hold textLock for too long. { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (ASObjectIsEqual(attributedText, _attributedText)) { return; } @@ -319,9 +321,9 @@ - (NSArray *)exclusionPaths return _textContainer.exclusionPaths; } -- (void)prepareAttributedStringForDrawing:(NSMutableAttributedString *)attributedString +- (void)prepareAttributedString:(NSMutableAttributedString *)attributedString { - ASDN::MutexLocker lock(__instanceLock__); + ASLockScopeSelf(); // Apply paragraph style if needed [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:kNilOptions usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { @@ -334,12 +336,6 @@ - (void)prepareAttributedStringForDrawing:(NSMutableAttributedString *)attribute [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; }]; - // Apply background color if needed - UIColor *backgroundColor = self.backgroundColor; - if (CGColorGetAlpha(backgroundColor.CGColor) > 0) { - [attributedString addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:NSMakeRange(0, attributedString.length)]; - } - // Apply shadow if needed if (_shadowOpacity > 0 && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero)) && CGColorGetAlpha(_shadowColor) > 0) { NSShadow *shadow = [[NSShadow alloc] init]; @@ -362,18 +358,23 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer ASTextContainer *copiedContainer = [_textContainer copy]; copiedContainer.size = self.bounds.size; NSMutableAttributedString *mutableText = [self.attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; - [self prepareAttributedStringForDrawing:mutableText]; + + [self prepareAttributedString:mutableText]; + return @{ - @"container": copiedContainer, - @"text": mutableText - }; + @"container": copiedContainer, + @"text": mutableText, + @"bgColor": self.backgroundColor ?: [NSNull null] + }; } /** * If it can't find a compatible layout, this method creates one. + * + * NOTE: Be careful to copy `text` if needed. */ + (ASTextLayout *)compatibleLayoutWithContainer:(ASTextContainer *)container - text:(NSAttributedString *)text + text:(NSAttributedString *)text NS_RETURNS_RETAINED { // Allocate layoutCacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) @@ -389,7 +390,7 @@ + (ASTextLayout *)compatibleLayoutWithContainer:(ASTextContainer *)container cacheValue = [textLayoutCache objectForKey:text]; if (cacheValue == nil) { cacheValue = [[ASTextCacheValue alloc] init]; - [textLayoutCache setObject:cacheValue forKey:text]; + [textLayoutCache setObject:cacheValue forKey:[text copy]]; } cacheValue; }); @@ -456,15 +457,29 @@ + (ASTextLayout *)compatibleLayoutWithContainer:(ASTextContainer *)container return layout; } -+ (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; ++ (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing { ASTextContainer *container = layoutDict[@"container"]; NSAttributedString *text = layoutDict[@"text"]; + UIColor *bgColor = layoutDict[@"bgColor"]; ASTextLayout *layout = [self compatibleLayoutWithContainer:container text:text]; if (isCancelledBlock()) { return; } + + // Fill background color. + if (bgColor == (id)[NSNull null]) { + bgColor = nil; + } + + // They may have already drawn into this context in the pre-context block + // so unfortunately we have to use the normal blend mode, not copy. + if (bgColor && CGColorGetAlpha(bgColor.CGColor) > 0) { + [bgColor setFill]; + UIRectFillUsingBlendMode(bounds, kCGBlendModeNormal); + } + CGContextRef context = UIGraphicsGetCurrentContext(); ASDisplayNodeAssert(context, @"This is no good without a context."); @@ -490,7 +505,7 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // TODO: The copy and application of size shouldn't be required, but it is currently. // See discussion in https://github.com/TextureGroup/Texture/pull/396 @@ -576,14 +591,14 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer - (ASTextNodeHighlightStyle)highlightStyle { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _highlightStyle; } - (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _highlightStyle = highlightStyle; } @@ -666,7 +681,7 @@ - (CGRect)frameForTextRange:(NSRange)textRange - (void)setPlaceholderColor:(UIColor *)placeholderColor { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _placeholderColor = placeholderColor; @@ -736,7 +751,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event if (inAdditionalTruncationMessage) { NSRange visibleRange = NSMakeRange(0, 0); { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // TODO: The copy and application of size shouldn't be required, but it is currently. // See discussion in https://github.com/TextureGroup/Texture/pull/396 ASTextContainer *containerCopy = [_textContainer copy]; @@ -822,14 +837,14 @@ - (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer - (BOOL)_pendingLinkTap { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && _delegate != nil; } - (BOOL)_pendingTruncationTap { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return [_highlightedLinkAttributeName isEqualToString:ASTextNodeTruncationTokenAttributeName]; } @@ -844,30 +859,30 @@ - (BOOL)_pendingTruncationTap */ - (CGColorRef)shadowColor { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _shadowColor; } - (void)setShadowColor:(CGColorRef)shadowColor { - __instanceLock__.lock(); + [self lock]; if (_shadowColor != shadowColor && CGColorEqualToColor(shadowColor, _shadowColor) == NO) { CGColorRelease(_shadowColor); _shadowColor = CGColorRetain(shadowColor); - __instanceLock__.unlock(); + [self unlock]; [self setNeedsDisplay]; return; } - __instanceLock__.unlock(); + [self unlock]; } - (CGSize)shadowOffset { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _shadowOffset; } @@ -875,7 +890,7 @@ - (CGSize)shadowOffset - (void)setShadowOffset:(CGSize)shadowOffset { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (CGSizeEqualToSize(_shadowOffset, shadowOffset)) { return; @@ -888,7 +903,7 @@ - (void)setShadowOffset:(CGSize)shadowOffset - (CGFloat)shadowOpacity { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _shadowOpacity; } @@ -896,7 +911,7 @@ - (CGFloat)shadowOpacity - (void)setShadowOpacity:(CGFloat)shadowOpacity { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (_shadowOpacity == shadowOpacity) { return; @@ -910,7 +925,7 @@ - (void)setShadowOpacity:(CGFloat)shadowOpacity - (CGFloat)shadowRadius { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _shadowRadius; } @@ -918,7 +933,7 @@ - (CGFloat)shadowRadius - (void)setShadowRadius:(CGFloat)shadowRadius { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (_shadowRadius == shadowRadius) { return; @@ -939,11 +954,21 @@ - (UIEdgeInsets)shadowPadding - (void)setPointSizeScaleFactors:(NSArray *)scaleFactors { AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); - _pointSizeScaleFactors = [scaleFactors copy]; + { + ASLockScopeSelf(); + if (ASObjectIsEqual(scaleFactors, _pointSizeScaleFactors)) { + return; + } + + _pointSizeScaleFactors = [scaleFactors copy]; + } + + [self setNeedsLayout]; } - (NSArray *)pointSizeScaleFactors { + ASLockScopeSelf(); return _pointSizeScaleFactors; } @@ -962,67 +987,47 @@ - (NSArray *)pointSizeScaleFactors - (void)_ensureTruncationText { if (_textContainer.truncationToken == nil) { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _textContainer.truncationToken = [self _locked_composedTruncationText]; } } - (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText { - { - ASDN::MutexLocker l(__instanceLock__); - - if (ASObjectIsEqual(_truncationAttributedText, truncationAttributedText)) { - return; - } - - _truncationAttributedText = [truncationAttributedText copy]; + if (ASLockedSelfCompareAssignCopy(_truncationAttributedText, truncationAttributedText)) { + [self _invalidateTruncationText]; } - - [self _invalidateTruncationText]; } - (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage { - { - ASDN::MutexLocker l(__instanceLock__); - - if (ASObjectIsEqual(_additionalTruncationMessage, additionalTruncationMessage)) { - return; - } - - _additionalTruncationMessage = [additionalTruncationMessage copy]; + if (ASLockedSelfCompareAssignCopy(_additionalTruncationMessage, additionalTruncationMessage)) { + [self _invalidateTruncationText]; } - - [self _invalidateTruncationText]; } - (void)setTruncationMode:(NSLineBreakMode)truncationMode { - ASDN::MutexLocker lock(__instanceLock__); - if (_truncationMode == truncationMode) { - return; - } - _truncationMode = truncationMode; - - ASTextTruncationType truncationType; - switch (truncationMode) { - case NSLineBreakByTruncatingHead: - truncationType = ASTextTruncationTypeStart; - break; - case NSLineBreakByTruncatingTail: - truncationType = ASTextTruncationTypeEnd; - break; - case NSLineBreakByTruncatingMiddle: - truncationType = ASTextTruncationTypeMiddle; - break; - default: - truncationType = ASTextTruncationTypeNone; + if (ASLockedSelfCompareAssign(_truncationMode, truncationMode)) { + ASTextTruncationType truncationType; + switch (truncationMode) { + case NSLineBreakByTruncatingHead: + truncationType = ASTextTruncationTypeStart; + break; + case NSLineBreakByTruncatingTail: + truncationType = ASTextTruncationTypeEnd; + break; + case NSLineBreakByTruncatingMiddle: + truncationType = ASTextTruncationTypeMiddle; + break; + default: + truncationType = ASTextTruncationTypeNone; + } + + _textContainer.truncationType = truncationType; + + [self setNeedsDisplay]; } - - _textContainer.truncationType = truncationType; - - [self setNeedsDisplay]; } - (BOOL)isTruncated @@ -1048,7 +1053,7 @@ - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines - (NSUInteger)lineCount { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); return 0; } @@ -1067,7 +1072,7 @@ - (void)_invalidateTruncationText */ - (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); // Check if we even have an additional truncation message. if (!_additionalTruncationMessage) { diff --git a/Source/ASTextNodeCommon.h b/Source/ASTextNodeCommon.h new file mode 100644 index 000000000..0ff51b402 --- /dev/null +++ b/Source/ASTextNodeCommon.h @@ -0,0 +1,86 @@ +// +// ASTextNodeCommon.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@class ASTextNode; + +/** + * Highlight styles. + */ +typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { + /** + * Highlight style for text on a light background. + */ + ASTextNodeHighlightStyleLight, + + /** + * Highlight style for text on a dark background. + */ + ASTextNodeHighlightStyleDark +}; + +/** + * @abstract Text node delegate. + */ +@protocol ASTextNodeDelegate +@optional + +/** + @abstract Indicates to the delegate that a link was tapped within a text node. + @param textNode The ASTextNode containing the link that was tapped. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was tapped. + @param textRange The range of highlighted text. + */ +- (void)textNode:(ASTextNode *)textNode tappedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; + +/** + @abstract Indicates to the delegate that a link was tapped within a text node. + @param textNode The ASTextNode containing the link that was tapped. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was tapped. + @param textRange The range of highlighted text. + @discussion In addition to implementing this method, the delegate must be set on the text + node before it is loaded (the recognizer is created in -didLoad) + */ +- (void)textNode:(ASTextNode *)textNode longPressedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; + +//! @abstract Called when the text node's truncation string has been tapped. +- (void)textNodeTappedTruncationToken:(ASTextNode *)textNode; + +/** + @abstract Indicates to the text node if an attribute should be considered a link. + @param textNode The text node containing the entity attribute. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was touched to trigger a highlight. + @discussion If not implemented, the default value is YES. + @return YES if the entity attribute should be a link, NO otherwise. + */ +- (BOOL)textNode:(ASTextNode *)textNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point; + +/** + @abstract Indicates to the text node if an attribute is a valid long-press target + @param textNode The text node containing the entity attribute. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within textNode, in textNode's coordinate system, that was long-pressed. + @discussion If not implemented, the default value is NO. + @return YES if the entity attribute should be treated as a long-press target, NO otherwise. + */ +- (BOOL)textNode:(ASTextNode *)textNode shouldLongPressLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point; + +@end + diff --git a/Source/ASVideoNode.mm b/Source/ASVideoNode.mm index 9274d6fa5..85650e885 100644 --- a/Source/ASVideoNode.mm +++ b/Source/ASVideoNode.mm @@ -16,11 +16,13 @@ // #import -#import +#import +#import #import #import #import #import +#import static BOOL ASAssetIsEqual(AVAsset *asset1, AVAsset *asset2) { return ASObjectIsEqual(asset1, asset2) @@ -130,7 +132,7 @@ - (ASDisplayNode *)constructPlayerNode - (AVPlayerItem *)constructPlayerItem { ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); AVPlayerItem *playerItem = nil; if (_assetURL != nil) { @@ -257,9 +259,7 @@ - (void)layout - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { - __instanceLock__.lock(); - ASDisplayNode *playerNode = _playerNode; - __instanceLock__.unlock(); + ASDisplayNode *playerNode = ASLockedSelf(_playerNode); CGSize calculatedSize = constrainedSize; @@ -320,9 +320,7 @@ - (void)imageAtTime:(CMTime)imageTime completionHandler:(void(^)(UIImage *image) - (void)setVideoPlaceholderImage:(UIImage *)image { - __instanceLock__.lock(); - NSString *gravity = _gravity; - __instanceLock__.unlock(); + NSString *gravity = self.gravity; if (image != nil) { self.contentMode = ASContentModeFromVideoGravity(gravity); @@ -332,7 +330,7 @@ - (void)setVideoPlaceholderImage:(UIImage *)image - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (object == _currentPlayerItem) { if ([keyPath isEqualToString:kStatus]) { @@ -400,7 +398,7 @@ - (void)didEnterPreloadState { [super didEnterPreloadState]; - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); AVAsset *asset = self.asset; // Return immediately if the asset is nil; if (asset == nil || self.playerState != ASVideoNodePlayerStateUnknown) { @@ -441,7 +439,7 @@ - (void)didExitPreloadState [super didExitPreloadState]; { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); self.player = nil; self.currentItem = nil; @@ -453,15 +451,16 @@ - (void)didEnterVisibleState { [super didEnterVisibleState]; - __instanceLock__.lock(); BOOL shouldPlay = NO; - if (_shouldBePlaying || _shouldAutoplay) { - if (_player != nil && CMTIME_IS_VALID(_lastPlaybackTime)) { - [_player seekToTime:_lastPlaybackTime]; + { + ASLockScopeSelf(); + if (_shouldBePlaying || _shouldAutoplay) { + if (_player != nil && CMTIME_IS_VALID(_lastPlaybackTime)) { + [_player seekToTime:_lastPlaybackTime]; + } + shouldPlay = YES; } - shouldPlay = YES; } - __instanceLock__.unlock(); if (shouldPlay) { [self play]; @@ -472,7 +471,7 @@ - (void)didExitVisibleState { [super didExitVisibleState]; - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (_shouldBePlaying) { [self pause]; @@ -487,7 +486,7 @@ - (void)didExitVisibleState - (void)setPlayerState:(ASVideoNodePlayerState)playerState { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); ASVideoNodePlayerState oldState = _playerState; @@ -513,7 +512,7 @@ - (void)setAssetURL:(NSURL *)assetURL - (NSURL *)assetURL { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (_assetURL != nil) { return _assetURL; @@ -535,7 +534,7 @@ - (void)setAsset:(AVAsset *)asset - (AVAsset *)asset { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _asset; } @@ -546,7 +545,7 @@ - (void)setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL [self didExitPreloadState]; { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); self.videoPlaceholderImage = nil; _asset = asset; _assetURL = assetURL; @@ -557,7 +556,7 @@ - (void)setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL - (void)setVideoComposition:(AVVideoComposition *)videoComposition { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _videoComposition = videoComposition; _currentPlayerItem.videoComposition = videoComposition; @@ -565,13 +564,13 @@ - (void)setVideoComposition:(AVVideoComposition *)videoComposition - (AVVideoComposition *)videoComposition { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _videoComposition; } - (void)setAudioMix:(AVAudioMix *)audioMix { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _audioMix = audioMix; _currentPlayerItem.audioMix = audioMix; @@ -579,19 +578,19 @@ - (void)setAudioMix:(AVAudioMix *)audioMix - (AVAudioMix *)audioMix { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _audioMix; } - (AVPlayer *)player { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _player; } - (AVPlayerLayer *)playerLayer { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return (AVPlayerLayer *)_playerNode.layer; } @@ -618,7 +617,7 @@ - (void)setDelegate:(id)delegate - (void)setGravity:(NSString *)gravity { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (_playerNode.isNodeLoaded) { ((AVPlayerLayer *)_playerNode.layer).videoGravity = gravity; } @@ -628,19 +627,19 @@ - (void)setGravity:(NSString *)gravity - (NSString *)gravity { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _gravity; } - (BOOL)muted { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _muted; } - (void)setMuted:(BOOL)muted { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _player.muted = muted; _muted = muted; @@ -650,33 +649,31 @@ - (void)setMuted:(BOOL)muted - (void)play { - __instanceLock__.lock(); + ASLockScopeSelf(); if (![self isStateChangeValid:ASVideoNodePlayerStatePlaying]) { - __instanceLock__.unlock(); return; } if (_player == nil) { - __instanceLock__.unlock(); - [self setNeedsPreload]; - __instanceLock__.lock(); + ASUnlockScope(self); + [self setNeedsPreload]; } if (_playerNode == nil) { _playerNode = [self constructPlayerNode]; - __instanceLock__.unlock(); + { + ASUnlockScope(self); [self addSubnode:_playerNode]; - __instanceLock__.lock(); - + } + [self setNeedsLayout]; } [_player play]; _shouldBePlaying = YES; - __instanceLock__.unlock(); } - (BOOL)ready @@ -686,7 +683,7 @@ - (BOOL)ready - (void)pause { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (![self isStateChangeValid:ASVideoNodePlayerStatePaused]) { return; } @@ -696,7 +693,7 @@ - (void)pause - (BOOL)isPlaying { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return (_player.rate > 0 && !_player.error); } @@ -713,7 +710,7 @@ - (BOOL)isStateChangeValid:(ASVideoNodePlayerState)state - (void)resetToPlaceholder { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (_playerNode != nil) { [_playerNode removeFromSupernode]; @@ -778,13 +775,13 @@ - (void)errorWhilePlaying:(NSNotification *)notification - (AVPlayerItem *)currentItem { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _currentPlayerItem; } - (void)setCurrentItem:(AVPlayerItem *)currentItem { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self removePlayerItemObservers:_currentPlayerItem]; @@ -797,22 +794,23 @@ - (void)setCurrentItem:(AVPlayerItem *)currentItem - (ASDisplayNode *)playerNode { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _playerNode; } - (void)setPlayerNode:(ASDisplayNode *)playerNode { - __instanceLock__.lock(); - _playerNode = playerNode; - __instanceLock__.unlock(); + { + ASLockScopeSelf(); + _playerNode = playerNode; + } [self setNeedsLayout]; } - (void)setPlayer:(AVPlayer *)player { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self removePlayerObservers:_player]; @@ -827,13 +825,13 @@ - (void)setPlayer:(AVPlayer *)player - (BOOL)shouldBePlaying { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); return _shouldBePlaying; } - (void)setShouldBePlaying:(BOOL)shouldBePlaying { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); _shouldBePlaying = shouldBePlaying; } diff --git a/Source/ASVideoPlayerNode.mm b/Source/ASVideoPlayerNode.mm index e6f766b41..7859448a6 100644 --- a/Source/ASVideoPlayerNode.mm +++ b/Source/ASVideoPlayerNode.mm @@ -25,7 +25,8 @@ #import #import -#import +#import +#import static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; @@ -167,7 +168,7 @@ - (NSURL *)assetURL { NSURL *url = nil; { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if ([_pendingAsset isKindOfClass:AVURLAsset.class]) { url = ((AVURLAsset *)_pendingAsset).URL; } @@ -180,7 +181,7 @@ - (void)setAsset:(AVAsset *)asset { ASDisplayNodeAssertMainThread(); - __instanceLock__.lock(); + [self lock]; // Clean out pending asset _pendingAsset = nil; @@ -188,22 +189,18 @@ - (void)setAsset:(AVAsset *)asset // Set asset based on interface state if ((ASInterfaceStateIncludesPreload(self.interfaceState))) { // Don't hold the lock while accessing the subnode - __instanceLock__.unlock(); + [self unlock]; _videoNode.asset = asset; return; } _pendingAsset = asset; - __instanceLock__.unlock(); + [self unlock]; } - (AVAsset *)asset { - __instanceLock__.lock(); - AVAsset *asset = _pendingAsset; - __instanceLock__.unlock(); - - return asset ?: _videoNode.asset; + return ASLockedSelf(_pendingAsset) ?: _videoNode.asset; } #pragma mark - ASDisplayNode @@ -212,7 +209,7 @@ - (void)didLoad { [super didLoad]; { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); [self createControls]; } } @@ -223,7 +220,7 @@ - (void)didEnterPreloadState AVAsset *pendingAsset = nil; { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); pendingAsset = _pendingAsset; _pendingAsset = nil; } @@ -244,7 +241,7 @@ - (BOOL)supportsLayerBacking - (void)createControls { { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (_controlsDisabled) { return; @@ -610,7 +607,7 @@ - (void)togglePlayPause - (void)showSpinner { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (!_spinnerNode) { @@ -643,7 +640,7 @@ - (void)showSpinner - (void)removeSpinner { - ASDN::MutexLocker l(__instanceLock__); + ASLockScopeSelf(); if (!_spinnerNode) { return; diff --git a/Source/ASViewController.h b/Source/ASViewController.h index bdd343b30..3d1c7e13a 100644 --- a/Source/ASViewController.h +++ b/Source/ASViewController.h @@ -63,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Set this block to customize the ASDisplayTraits returned when the VC transitions to the given window size. */ -@property (nonatomic, copy) ASDisplayTraitsForTraitWindowSizeBlock overrideDisplayTraitsWithWindowSize; +@property (nonatomic, copy) ASDisplayTraitsForTraitWindowSizeBlock overrideDisplayTraitsWithWindowSize ASDISPLAYNODE_DEPRECATED_MSG("This property is actually never accessed inside the framework"); /** * @abstract Passthrough property to the the .interfaceState of the node. @@ -78,6 +78,11 @@ NS_ASSUME_NONNULL_BEGIN // Refer to examples/SynchronousConcurrency, AsyncViewController.m @property (nonatomic, assign) BOOL neverShowPlaceholders; +/* Custom container UIViewController subclasses can use this property to add to the overlay + that UIViewController calculates for the safeAreaInsets for contained view controllers. + */ +@property(nonatomic) UIEdgeInsets additionalSafeAreaInsets; + @end @interface ASViewController (ASRangeControllerUpdateRangeProtocol) diff --git a/Source/ASViewController.mm b/Source/ASViewController.mm index 2826fae9d..8aded78c3 100644 --- a/Source/ASViewController.mm +++ b/Source/ASViewController.mm @@ -32,6 +32,7 @@ @implementation ASViewController NSInteger _visibilityDepth; BOOL _selfConformsToRangeModeProtocol; BOOL _nodeConformsToRangeModeProtocol; + UIEdgeInsets _fallbackAdditionalSafeAreaInsets; } - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil @@ -73,10 +74,14 @@ - (void)_initializeInstance if (_node == nil) { return; } + + _node.viewControllerRoot = YES; _selfConformsToRangeModeProtocol = [self conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]; _nodeConformsToRangeModeProtocol = [_node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]; _automaticallyAdjustRangeModeBasedOnViewEvents = _selfConformsToRangeModeProtocol || _nodeConformsToRangeModeProtocol; + + _fallbackAdditionalSafeAreaInsets = UIEdgeInsetsZero; // In case the node will get loaded if (_node.nodeLoaded) { @@ -96,7 +101,7 @@ - (void)_initializeInstance - (void)dealloc { - ASPerformBackgroundDeallocation(_node); + ASPerformBackgroundDeallocation(&_node); } - (void)loadView @@ -159,6 +164,20 @@ - (void)viewDidLayoutSubviews [_node recursivelyEnsureDisplaySynchronously:YES]; } [super viewDidLayoutSubviews]; + + if (!AS_AT_LEAST_IOS11) { + [self _updateNodeFallbackSafeArea]; + } +} + +- (void)_updateNodeFallbackSafeArea +{ + UIEdgeInsets safeArea = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0); + UIEdgeInsets additionalInsets = self.additionalSafeAreaInsets; + + safeArea = ASConcatInsets(safeArea, additionalInsets); + + _node.fallbackSafeAreaInsets = safeArea; } ASVisibilityDidMoveToParentViewController; @@ -264,6 +283,25 @@ - (ASInterfaceState)interfaceState return _node.interfaceState; } +- (UIEdgeInsets)additionalSafeAreaInsets +{ + if (AS_AVAILABLE_IOS(11.0)) { + return super.additionalSafeAreaInsets; + } + + return _fallbackAdditionalSafeAreaInsets; +} + +- (void)setAdditionalSafeAreaInsets:(UIEdgeInsets)additionalSafeAreaInsets +{ + if (AS_AVAILABLE_IOS(11.0)) { + [super setAdditionalSafeAreaInsets:additionalSafeAreaInsets]; + } else { + _fallbackAdditionalSafeAreaInsets = additionalSafeAreaInsets; + [self _updateNodeFallbackSafeArea]; + } +} + #pragma mark - ASTraitEnvironment - (ASPrimitiveTraitCollection)primitiveTraitCollectionForUITraitCollection:(UITraitCollection *)traitCollection diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index b1903945e..19d49afe8 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -16,10 +16,14 @@ // #import +#import #import #import #import #import +#import +#import +#import #import #import @@ -35,6 +39,7 @@ #import #import #import +#import #import #import @@ -79,6 +84,7 @@ #import #import #import +#import #import #import #import @@ -118,6 +124,7 @@ #import #import #import +#import #import #import #import diff --git a/Source/Base/ASAssert.h b/Source/Base/ASAssert.h index 21bdcf2da..336ac3d0e 100644 --- a/Source/Base/ASAssert.h +++ b/Source/Base/ASAssert.h @@ -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/ASAssert.m b/Source/Base/ASAssert.m index 7759ff53f..d346dfedb 100644 --- a/Source/Base/ASAssert.m +++ b/Source/Base/ASAssert.m @@ -11,29 +11,18 @@ // #import -#import -static pthread_key_t ASMainThreadAssertionsDisabledKey() -{ - return ASPthreadStaticKey(NULL); -} +static _Thread_local int tls_mainThreadAssertionsDisabledCount; BOOL ASMainThreadAssertionsAreDisabled() { - return (size_t)pthread_getspecific(ASMainThreadAssertionsDisabledKey()) > 0; + return tls_mainThreadAssertionsDisabledCount > 0; } void ASPushMainThreadAssertionsDisabled() { - pthread_key_t key = ASMainThreadAssertionsDisabledKey(); - size_t oldValue = (size_t)pthread_getspecific(key); - pthread_setspecific(key, (void *)(oldValue + 1)); + tls_mainThreadAssertionsDisabledCount += 1; } void ASPopMainThreadAssertionsDisabled() { - pthread_key_t key = ASMainThreadAssertionsDisabledKey(); - size_t oldValue = (size_t)pthread_getspecific(key); - if (oldValue > 0) { - pthread_setspecific(key, (void *)(oldValue - 1)); - } else { - ASDisplayNodeCFailAssert(@"Attempt to pop thread assertion-disabling without corresponding push."); - } + tls_mainThreadAssertionsDisabledCount -= 1; + ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push."); } diff --git a/Source/Base/ASAvailability.h b/Source/Base/ASAvailability.h index 64c5a127e..c05ddd3fa 100644 --- a/Source/Base/ASAvailability.h +++ b/Source/Base/ASAvailability.h @@ -19,10 +19,6 @@ #pragma once -#ifndef kCFCoreFoundationVersionNumber_iOS_9_0 - #define kCFCoreFoundationVersionNumber_iOS_9_0 1240.10 -#endif - #ifndef kCFCoreFoundationVersionNumber_iOS_10_0 #define kCFCoreFoundationVersionNumber_iOS_10_0 1348.00 #endif @@ -31,10 +27,24 @@ #define kCFCoreFoundationVersionNumber_iOS_11_0 1438.10 #endif -#define AS_AT_LEAST_IOS9 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_9_0) +#ifndef __IPHONE_11_0 + #define __IPHONE_11_0 110000 +#endif + #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, *) + #define AS_AVAILABLE_TVOS(ver) __builtin_available(tvOS ver, *) + #define AS_AVAILABLE_IOS_TVOS(ver1, ver2) __builtin_available(iOS ver1, tvOS ver2, *) +#else + #define AS_AVAILABLE_IOS(ver) (TARGET_OS_IOS && AS_AT_LEAST_IOS##ver) + #define AS_AVAILABLE_TVOS(ver) (TARGET_OS_TV && AS_AT_LEAST_IOS##ver) + #define AS_AVAILABLE_IOS_TVOS(ver1, ver2) (AS_AVAILABLE_IOS(ver1) || AS_AVAILABLE_TVOS(ver2)) +#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. @@ -46,11 +56,8 @@ #define YOGA __has_include(YOGA_HEADER_PATH) #endif -// When enabled, use ASTextNode2 for ALL instances of ASTextNode. -// This includes what ASButtonNode uses internally, as well as all app references to ASTextNode. -// See ASTextNode+Beta.h declaration of ASTextNodeExperimentOptions for more details. -#ifndef ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE - #define ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE 0 +#ifdef ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE + #error "ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE is unavailable. See ASConfiguration.h." #endif #define AS_PIN_REMOTE_IMAGE __has_include() diff --git a/Source/Base/ASBaseDefines.h b/Source/Base/ASBaseDefines.h index 9eb1ec0b0..6706b6da4 100755 --- a/Source/Base/ASBaseDefines.h +++ b/Source/Base/ASBaseDefines.h @@ -211,15 +211,6 @@ #define AS_SUBCLASSING_RESTRICTED #endif -#define ASPthreadStaticKey(dtor) ({ \ - static dispatch_once_t onceToken; \ - static pthread_key_t key; \ - dispatch_once(&onceToken, ^{ \ - pthread_key_create(&key, dtor); \ - }); \ - key; \ -}) - #define ASCreateOnce(expr) ({ \ static dispatch_once_t onceToken; \ static __typeof__(expr) staticVar; \ @@ -241,6 +232,29 @@ ((c *) ([__val class] == [c class] ? __val : nil));\ }) +// Compare two primitives, assign if different. Returns whether the assignment happened. +#define ASCompareAssign(lvalue, newValue) ({ \ + BOOL result = (lvalue != newValue); \ + if (result) { lvalue = newValue; } \ + result; \ +}) + +#define ASCompareAssignObjects(lvalue, newValue) \ + ASCompareAssignCustom(lvalue, newValue, ASObjectIsEqual) + +// e.g. ASCompareAssignCustom(_myInsets, insets, UIEdgeInsetsEqualToEdgeInsets) +#define ASCompareAssignCustom(lvalue, newValue, isequal) ({ \ + BOOL result = !(isequal(lvalue, newValue)); \ + if (result) { lvalue = newValue; } \ + result; \ +}) + +#define ASCompareAssignCopy(lvalue, newValue) ({ \ + BOOL result = !ASObjectIsEqual(lvalue, newValue); \ + if (result) { lvalue = [newValue copyWithZone:NULL]; } \ + result; \ +}) + /** * Create a new set by mapping `collection` over `work`, ignoring nil. */ diff --git a/Source/Base/ASLog.h b/Source/Base/ASLog.h index e4f54fd81..e87404631 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_TVOS(9, 9)) { \ + __val = os_log_create(subsystem, category); \ +} else { \ + __val = (os_log_t)0; \ +} \ +__val; \ +}) + +#define as_log_debug(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + os_log_debug(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_info(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + os_log_info(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_error(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 9)) { \ + os_log_error(log, format, ##__VA_ARGS__); \ +} else { \ + (void)0; \ +} \ + +#define as_log_fault(log, format, ...) \ +if (AS_AVAILABLE_IOS_TVOS(9, 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 8e65c4b55..e1c42ea79 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/Debug/AsyncDisplayKit+Debug.m b/Source/Debug/AsyncDisplayKit+Debug.m index 20d67cd70..b6d0d0d4a 100644 --- a/Source/Debug/AsyncDisplayKit+Debug.m +++ b/Source/Debug/AsyncDisplayKit+Debug.m @@ -17,6 +17,7 @@ #import #import +#import #import #import #import @@ -148,7 +149,7 @@ - (void)layout UIColor *clipsBorderColor = [UIColor colorWithRed:30/255.0 green:90/255.0 blue:50/255.0 alpha:0.7]; CGRect imgRect = CGRectMake(0, 0, 2.0 * borderWidth + 1.0, 2.0 * borderWidth + 1.0); - UIGraphicsBeginImageContext(imgRect.size); + ASGraphicsBeginImageContextWithOptions(imgRect.size, NO, 1); [fillColor setFill]; UIRectFill(imgRect); @@ -156,8 +157,7 @@ - (void)layout [self drawEdgeIfClippedWithEdges:clippedEdges color:clipsBorderColor borderWidth:borderWidth imgRect:imgRect]; [self drawEdgeIfClippedWithEdges:clipsToBoundsClippedEdges color:borderColor borderWidth:borderWidth imgRect:imgRect]; - UIImage *debugHighlightImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIImage *debugHighlightImage = ASGraphicsGetImageAndEndCurrentContext(); UIEdgeInsets edgeInsets = UIEdgeInsetsMake(borderWidth, borderWidth, borderWidth, borderWidth); debugOverlay.image = [debugHighlightImage resizableImageWithCapInsets:edgeInsets resizingMode:UIImageResizingModeStretch]; @@ -213,7 +213,7 @@ - (void)drawEdgeIfClippedWithEdges:(UIRectEdge)rectEdge color:(UIColor *)color b @interface _ASRangeDebugOverlayView : UIView -+ (instancetype)sharedInstance; ++ (instancetype)sharedInstance NS_RETURNS_RETAINED; - (void)addRangeController:(ASRangeController *)rangeController; @@ -311,7 +311,7 @@ + (UIWindow *)keyWindow return [[NSClassFromString(@"UIApplication") sharedApplication] keyWindow]; } -+ (instancetype)sharedInstance ++ (_ASRangeDebugOverlayView *)sharedInstance NS_RETURNS_RETAINED { static _ASRangeDebugOverlayView *__rangeDebugOverlay = nil; @@ -389,6 +389,8 @@ - (void)layoutToFitAllBarsExcept:(NSInteger)barsToClip if (totalHeight > 0) { totalHeight -= (BAR_THICKNESS * barsToClip); + } else if (totalHeight == 0) { + } if (barsToClip == 0) { @@ -453,6 +455,7 @@ - (void)updateRangeController:(ASRangeController *)controller _ASRangeDebugBarView *viewToUpdate = [self barViewForRangeController:controller]; CGRect boundsRect = self.bounds; + CGRect visibleRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, ASRangeTuningParametersZero, scrollableDirections, scrollDirection); CGRect displayRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, displayTuningParameters, scrollableDirections, scrollDirection); CGRect preloadRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, preloadTuningParameters, scrollableDirections, scrollDirection); @@ -473,7 +476,9 @@ - (void)updateRangeController:(ASRangeController *)controller } if (ASScrollDirectionContainsVerticalDirection(scrollDirection)) { - + if (self.bounds.size.height == 0) { + return; + } if (displayRect.size.height >= preloadRect.size.height) { displayRangeLargerThanPreload = YES; } else { @@ -481,9 +486,9 @@ - (void)updateRangeController:(ASRangeController *)controller } if (displayRangeLargerThanPreload) { - visibleRatio = visibleRect.size.height / displayRect.size.height; + visibleRatio = displayRect.size.height != 0 ? visibleRect.size.height / displayRect.size.height : 1; displayRatio = 1.0; - preloadRatio = preloadRect.size.height / displayRect.size.height; + preloadRatio = displayRect.size.height != 0 ? preloadRect.size.height / displayRect.size.height : 1; } else { visibleRatio = visibleRect.size.height / preloadRect.size.height; displayRatio = displayRect.size.height / preloadRect.size.height; @@ -491,7 +496,9 @@ - (void)updateRangeController:(ASRangeController *)controller } } else { - + if (self.bounds.size.width == 0) { + return; + } if (displayRect.size.width >= preloadRect.size.width) { displayRangeLargerThanPreload = YES; } else { @@ -752,7 +759,7 @@ - (ASImageNode *)createRangeNodeWithColor:(UIColor *)color return rangeBarImageNode; } -+ (NSAttributedString *)whiteAttributedStringFromString:(NSString *)string withSize:(CGFloat)size ++ (NSAttributedString *)whiteAttributedStringFromString:(NSString *)string withSize:(CGFloat)size NS_RETURNS_RETAINED { NSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor whiteColor], NSFontAttributeName : [UIFont systemFontOfSize:size]}; diff --git a/Source/Details/ASBasicImageDownloader.h b/Source/Details/ASBasicImageDownloader.h index 7238048c0..5d9a7b384 100644 --- a/Source/Details/ASBasicImageDownloader.h +++ b/Source/Details/ASBasicImageDownloader.h @@ -25,14 +25,16 @@ NS_ASSUME_NONNULL_BEGIN @interface ASBasicImageDownloader : NSObject /** - * A shared image downloader which can be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes + * A shared image downloader which can be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes. + * The userInfo provided by this downloader is `nil`. * * This is a very basic image downloader. It does not support caching, progressive downloading and likely * isn't something you should use in production. If you'd like something production ready, see @c ASPINRemoteImageDownloader * * @note It is strongly recommended you include PINRemoteImage and use @c ASPINRemoteImageDownloader instead. */ -+ (instancetype)sharedImageDownloader; +@property (class, readonly) ASBasicImageDownloader *sharedImageDownloader; ++ (ASBasicImageDownloader *)sharedImageDownloader NS_RETURNS_RETAINED; + (instancetype)new __attribute__((unavailable("+[ASBasicImageDownloader sharedImageDownloader] must be used."))); - (instancetype)init __attribute__((unavailable("+[ASBasicImageDownloader sharedImageDownloader] must be used."))); diff --git a/Source/Details/ASBasicImageDownloader.mm b/Source/Details/ASBasicImageDownloader.mm index 0d8084265..ab88dfa4e 100644 --- a/Source/Details/ASBasicImageDownloader.mm +++ b/Source/Details/ASBasicImageDownloader.mm @@ -130,7 +130,7 @@ - (void)completeWithImage:(UIImage *)image error:(NSError *)error if (completionBlock) { dispatch_async(callbackQueue, ^{ - completionBlock(image, error, nil); + completionBlock(image, error, nil, nil); }); } } @@ -206,7 +206,7 @@ @interface ASBasicImageDownloader () @implementation ASBasicImageDownloader -+ (instancetype)sharedImageDownloader ++ (ASBasicImageDownloader *)sharedImageDownloader { static ASBasicImageDownloader *sharedImageDownloader = nil; static dispatch_once_t once = 0; @@ -235,9 +235,9 @@ - (instancetype)_init #pragma mark ASImageDownloaderProtocol. - (id)downloadImageWithURL:(NSURL *)URL - callbackQueue:(dispatch_queue_t)callbackQueue - downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress - completion:(ASImageDownloaderCompletion)completion + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion { ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:URL]; diff --git a/Source/Details/ASBatchContext.mm b/Source/Details/ASBatchContext.m similarity index 65% rename from Source/Details/ASBatchContext.mm rename to Source/Details/ASBatchContext.m index 8598126d2..5ac780ddc 100644 --- a/Source/Details/ASBatchContext.mm +++ b/Source/Details/ASBatchContext.m @@ -1,5 +1,5 @@ // -// ASBatchContext.mm +// ASBatchContext.m // Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. @@ -18,7 +18,7 @@ #import #import -#import +#import typedef NS_ENUM(NSInteger, ASBatchContextState) { ASBatchContextStateFetching, @@ -26,54 +26,44 @@ typedef NS_ENUM(NSInteger, ASBatchContextState) { ASBatchContextStateCompleted }; -@interface ASBatchContext () -{ - ASBatchContextState _state; - ASDN::RecursiveMutex __instanceLock__; +@implementation ASBatchContext { + atomic_int _state; } -@end - -@implementation ASBatchContext - (instancetype)init { if (self = [super init]) { - _state = ASBatchContextStateCompleted; + _state = ATOMIC_VAR_INIT(ASBatchContextStateCompleted); } return self; } - (BOOL)isFetching { - ASDN::MutexLocker l(__instanceLock__); - return _state == ASBatchContextStateFetching; + return atomic_load(&_state) == ASBatchContextStateFetching; } - (BOOL)batchFetchingWasCancelled { - ASDN::MutexLocker l(__instanceLock__); - return _state == ASBatchContextStateCancelled; + return atomic_load(&_state) == ASBatchContextStateCancelled; } - (void)beginBatchFetching { - ASDN::MutexLocker l(__instanceLock__); - _state = ASBatchContextStateFetching; + atomic_store(&_state, ASBatchContextStateFetching); } - (void)completeBatchFetching:(BOOL)didComplete { if (didComplete) { as_log_debug(ASCollectionLog(), "Completed batch fetch with context %@", self); - ASDN::MutexLocker l(__instanceLock__); - _state = ASBatchContextStateCompleted; + atomic_store(&_state, ASBatchContextStateCompleted); } } - (void)cancelBatchFetching { - ASDN::MutexLocker l(__instanceLock__); - _state = ASBatchContextStateCancelled; + atomic_store(&_state, ASBatchContextStateCancelled); } @end diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index b2d2f3ab8..8792637ba 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -91,7 +91,7 @@ extern NSString * const ASCollectionInvalidUpdateException; - (NSUInteger)dataController:(ASDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; -- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; +- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout; /** The constrained size range for layout. Called only if no data controller layout delegate is provided. @@ -244,8 +244,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. @@ -258,9 +261,15 @@ 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:(void (^)(void))completion; - (void)waitUntilAllUpdatesAreProcessed; +/** + * See ASCollectionNode.h for full documentation of these methods. + */ +@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized; +- (void)onDidFinishSynchronizing:(void (^)(void))completion; + /** * Notifies the data controller object that its environment has changed. The object will request its environment delegate for new information * and propagate the information to all visible elements, including ones that are being prepared in background. @@ -271,6 +280,11 @@ extern NSString * const ASCollectionInvalidUpdateException; */ - (void)environmentDidChange; +/** + * Reset visibleMap and pendingMap when asyncDataSource and asyncDelegate of collection view become nil. + */ +- (void)clearData; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index f42d01306..877904769 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -17,6 +17,8 @@ #import +#include + #import #import #import @@ -54,6 +56,8 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; +typedef void (^ASDataControllerSynchronizationBlock)(); + @interface ASDataController () { id _layoutDelegate; @@ -65,10 +69,14 @@ @interface ASDataController () { ASMainSerialQueue *_mainSerialQueue; dispatch_queue_t _editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. - dispatch_group_t _editingTransactionGroup; // Group of all edit transaction blocks. Useful for waiting. + dispatch_group_t _editingTransactionGroup; // Group of all edit transaction blocks. Useful for waiting. + std::atomic _editingTransactionGroupCount; BOOL _initialReloadDataHasBeenCalled; + BOOL _synchronized; + NSMutableSet *_onDidFinishSynchronizingBlocks; + struct { unsigned int supplementaryNodeKindsInSections:1; unsigned int supplementaryNodesOfKindInSection:1; @@ -98,7 +106,7 @@ - (instancetype)initWithDataSource:(id)dataSource node:( _dataSourceFlags.supplementaryNodeKindsInSections = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeKindsInSections:)]; _dataSourceFlags.supplementaryNodesOfKindInSection = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodesOfKind:inSection:)]; - _dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:)]; + _dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:shouldAsyncLayout:)]; _dataSourceFlags.constrainedSizeForNodeAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForNodeAtIndexPath:)]; _dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)]; _dataSourceFlags.contextForSection = [_dataSource respondsToSelector:@selector(dataController:contextForSection:)]; @@ -112,6 +120,9 @@ - (instancetype)initWithDataSource:(id)dataSource node:( _nextSectionID = 0; _mainSerialQueue = [[ASMainSerialQueue alloc] init]; + + _synchronized = YES; + _onDidFinishSynchronizingBlocks = [NSMutableSet set]; const char *queueName = [[NSString stringWithFormat:@"org.AsyncDisplayKit.ASDataController.editingTransactionQueue:%p", self] cStringUsingEncoding:NSASCIIStringEncoding]; _editingTransactionQueue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL); @@ -352,7 +363,8 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map nodeBlock = [dataSource dataController:self nodeBlockAtIndexPath:indexPath]; } } else { - nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath]; + BOOL shouldAsyncLayout = YES; + nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath shouldAsyncLayout:&shouldAsyncLayout]; } ASSizeRange constrainedSize = ASSizeRangeUnconstrained; @@ -440,35 +452,71 @@ - (void)waitUntilAllUpdatesAreProcessed - (BOOL)isProcessingUpdates { ASDisplayNodeAssertMainThread(); - if (_mainSerialQueue.numberOfScheduledBlocks > 0) { - return YES; - } else if (dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_NOW) != 0) { - // After waiting for zero duration, a nonzero value is returned if blocks are still running. - return YES; - } - // Both the _mainSerialQueue and _editingTransactionQueue are drained; we are fully quiesced. - return NO; +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // Using dispatch_group_wait is much more expensive than our manually managed count, but it's crucial they always match. + BOOL editingTransactionQueueBusy = dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_NOW) != 0; + ASDisplayNodeAssert(editingTransactionQueueBusy == (_editingTransactionGroupCount > 0), + @"editingTransactionQueueBusy = %@, but _editingTransactionGroupCount = %d !", + editingTransactionQueueBusy ? @"YES" : @"NO", (int)_editingTransactionGroupCount); +#endif + + return _mainSerialQueue.numberOfScheduledBlocks > 0 || _editingTransactionGroupCount > 0; } -- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion +- (void)onDidFinishProcessingUpdates:(void (^)())completion { ASDisplayNodeAssertMainThread(); + if (!completion) { + return; + } if ([self isProcessingUpdates] == NO) { ASPerformBlockOnMainThread(completion); } else { dispatch_async(_editingTransactionQueue, ^{ // Retry the block. If we're done processing updates, it'll run immediately, otherwise // wait again for updates to quiesce completely. - [_mainSerialQueue performBlockOnMainThread:^{ + // Don't use _mainSerialQueue so that we don't affect -isProcessingUpdates. + dispatch_async(dispatch_get_main_queue(), ^{ [self onDidFinishProcessingUpdates:completion]; - }]; + }); }); } } +- (BOOL)isSynchronized { + return _synchronized; +} + +- (void)onDidFinishSynchronizing:(void (^)())completion { + ASDisplayNodeAssertMainThread(); + if (!completion) { + return; + } + if ([self isSynchronized]) { + ASPerformBlockOnMainThread(completion); + } else { + // Hang on to the completion block so that it gets called the next time view is synchronized to data. + [_onDidFinishSynchronizingBlocks addObject:[completion copy]]; + } +} + - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet { ASDisplayNodeAssertMainThread(); + + _synchronized = NO; + + [changeSet addCompletionHandler:^(BOOL finished) { + _synchronized = YES; + [self onDidFinishProcessingUpdates:^{ + if (_synchronized) { + for (ASDataControllerSynchronizationBlock block in _onDidFinishSynchronizingBlocks) { + block(); + } + [_onDidFinishSynchronizingBlocks removeAllObjects]; + } + }]; + }]; if (changeSet.includesReloadData) { if (_initialReloadDataHasBeenCalled) { @@ -526,7 +574,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet BOOL canDelegate = (self.layoutDelegate != nil); ASElementMap *newMap; - id layoutContext; + ASCollectionLayoutContext *layoutContext; { as_activity_scope(as_activity_create("Latch new data for collection update", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); @@ -540,7 +588,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet 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]; @@ -558,6 +606,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet as_log_debug(ASCollectionLog(), "New content: %@", newMap.smallDescription); Class layoutDelegateClass = [self.layoutDelegate class]; + ++_editingTransactionGroupCount; dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ __block __unused os_activity_scope_state_s preparationScope = {}; // unused if deployment target < iOS10 as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope); @@ -577,6 +626,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet self.visibleMap = newMap; }]; }]; + --_editingTransactionGroupCount; }; // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements @@ -584,9 +634,17 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet [layoutDelegateClass calculateLayoutWithContext:layoutContext]; completion(); } else { - NSArray *elementsToProcess = ASArrayByFlatMapping(newMap, - ASCollectionElement *element, - (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); + NSMutableArray *elementsToProcess = [NSMutableArray array]; + for (ASCollectionElement *element in newMap) { + ASCellNode *nodeIfAllocated = element.nodeIfAllocated; + if (nodeIfAllocated.shouldUseUIKitCell) { + // If the node exists and we know it is a passthrough cell, we know it will never have a .calculatedLayout. + continue; + } else if (nodeIfAllocated.calculatedLayout == nil) { + // If the node hasn't been allocated, or it doesn't have a valid layout, let's process it. + [elementsToProcess addObject:element]; + } + } [self _allocateNodesFromElements:elementsToProcess completion:completion]; } }); @@ -599,43 +657,38 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet /** * 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++; @@ -759,7 +812,7 @@ - (void)relayoutNodes:(id)nodes nodesSizeChanged:(NSMutableAr } } -- (void)relayoutAllNodes +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock { ASDisplayNodeAssertMainThread(); if (!_initialReloadDataHasBeenCalled) { @@ -770,6 +823,13 @@ - (void)relayoutAllNodes // 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]; }]; } @@ -820,6 +880,15 @@ - (void)environmentDidChange }); } +- (void)clearData +{ + ASDisplayNodeAssertMainThread(); + if (_initialReloadDataHasBeenCalled) { + [self waitUntilAllUpdatesAreProcessed]; + self.visibleMap = self.pendingMap = [[ASElementMap alloc] init]; + } +} + # pragma mark - Helper methods - (void)_scheduleBlockOnMainSerialQueue:(dispatch_block_t)block diff --git a/Source/Details/ASElementMap.h b/Source/Details/ASElementMap.h index fb53ee818..a73f80a92 100644 --- a/Source/Details/ASElementMap.h +++ b/Source/Details/ASElementMap.h @@ -31,6 +31,11 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED @interface ASElementMap : NSObject +/** + * The total number of elements in this map. + */ +@property (readonly) NSUInteger count; + /** * The number of sections (of items) in this map. */ @@ -64,10 +69,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 ddcd18c99..f5646de78 100644 --- a/Source/Details/ASElementMap.m +++ b/Source/Details/ASElementMap.m @@ -47,6 +47,8 @@ - (instancetype)init - (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]; @@ -73,6 +75,11 @@ - (instancetype)initWithSections:(NSArray *)sections items:(ASColle return self; } +- (NSUInteger)count +{ + return _elementToIndexPathMap.count; +} + - (NSArray *)itemIndexPaths { return ASIndexPathsForTwoDimensionalArray(_sectionsOfItems); @@ -157,8 +164,25 @@ - (ASCollectionElement *)elementForLayoutAttributes:(UICollectionViewLayoutAttri - (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 diff --git a/Source/Details/ASGraphicsContext.h b/Source/Details/ASGraphicsContext.h new file mode 100644 index 000000000..0713f104f --- /dev/null +++ b/Source/Details/ASGraphicsContext.h @@ -0,0 +1,56 @@ +// +// ASGraphicsContext.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +@class UIImage; + +/** + * Functions for creating one-shot graphics contexts that do not have to copy + * their contents when an image is generated from them. This is efficient + * for our use, since we do not reuse graphics contexts. + * + * The API mirrors the UIGraphics API, with the exception that forming an image + * ends the context as well. + * + * Note: You must not mix-and-match between ASGraphics* and UIGraphics* functions + * within the same drawing operation. + */ + +NS_ASSUME_NONNULL_BEGIN +ASDISPLAYNODE_EXTERN_C_BEGIN + +/** + * Creates a one-shot context. + * + * Behavior is the same as UIGraphicsBeginImageContextWithOptions. + */ +extern void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale); + +/** + * Generates and image and ends the current one-shot context. + * + * Behavior is the same as UIGraphicsGetImageFromCurrentImageContext followed by UIGraphicsEndImageContext. + */ +extern UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext(void) NS_RETURNS_RETAINED; + +/** + * Call this if you want to end the current context without making an image. + * + * Behavior is the same as UIGraphicsEndImageContext. + */ +extern void ASGraphicsEndImageContext(void); + +ASDISPLAYNODE_EXTERN_C_END +NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASGraphicsContext.m b/Source/Details/ASGraphicsContext.m new file mode 100644 index 000000000..37810a106 --- /dev/null +++ b/Source/Details/ASGraphicsContext.m @@ -0,0 +1,171 @@ +// +// ASGraphicsContext.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASGraphicsContext.h" +#import +#import +#import +#import +#import +#import +#import + +/** + * Our version of the private CGBitmapGetAlignedBytesPerRow function. + * + * In both 32-bit and 64-bit, this function rounds up to nearest multiple of 32 + * in iOS 9, 10, and 11. We'll try to catch if this ever changes by asserting that + * the bytes-per-row for a 1x1 context from the system is 32. + */ +static size_t ASGraphicsGetAlignedBytesPerRow(size_t baseValue) { + // Add 31 then zero out low 5 bits. + return (baseValue + 31) & ~0x1F; +} + +/** + * A key used to associate CGContextRef -> NSMutableData, nonatomic retain. + * + * That way the data will be released when the context dies. If they pull an image, + * we will retain the data object (in a CGDataProvider) before releasing the context. + */ +static UInt8 __contextDataAssociationKey; + +#pragma mark - Graphics Contexts + +extern void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIGraphicsBeginImageContextWithOptions(size, opaque, scale); + return; + } + + // We use "reference contexts" to get device-specific options that UIKit + // uses. + static dispatch_once_t onceToken; + static CGContextRef refCtxOpaque; + static CGContextRef refCtxTransparent; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 1); + refCtxOpaque = CGContextRetain(UIGraphicsGetCurrentContext()); + ASDisplayNodeCAssert(CGBitmapContextGetBytesPerRow(refCtxOpaque) == 32, @"Expected bytes per row to be aligned to 32. Has CGBitmapGetAlignedBytesPerRow implementation changed?"); + UIGraphicsEndImageContext(); + + // Make transparent ref context. + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1); + refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext()); + UIGraphicsEndImageContext(); + }); + + // These options are taken from UIGraphicsBeginImageContext. + CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent; + CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx); + + if (scale == 0) { + scale = ASScreenScale(); + } + size_t intWidth = (size_t)ceil(size.width * scale); + size_t intHeight = (size_t)ceil(size.height * scale); + size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx); + size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8; + bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow); + size_t bufferSize = bytesPerRow * intHeight; + CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx); + + // We create our own buffer, and wrap the context around that. This way we can prevent + // the copy that usually gets made when you form a CGImage from the context. + ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize]; + + CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo); + + // Transfer ownership of the data to the context. So that if the context + // is destroyed before we create an image from it, the data will be released. + objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // Set the CTM to account for iOS orientation & specified scale. + // If only we could use CGContextSetBaseCTM. It doesn't + // seem like there are any consequences for our use case + // but we'll be on the look out. The internet hinted that it + // affects shadowing but I tested and shadowing works. + CGContextTranslateCTM(context, 0, intHeight); + CGContextScaleCTM(context, scale, -scale); + + // Save the state so we can restore it and recover our scale in GetImageAndEnd + CGContextSaveGState(context); + + // Transfer context ownership to the UIKit stack. + UIGraphicsPushContext(context); + CGContextRelease(context); +} + +extern UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; + } + + // Pop the context and make sure we have one. + CGContextRef context = UIGraphicsGetCurrentContext(); + if (context == NULL) { + ASDisplayNodeCFailAssert(@"Can't end image context without having begun one."); + return nil; + } + + // Read the device-specific ICC-based color space to use for the image. + // For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage + // generates an image in a device-specific color space (for wide color support). + // We replicate that behavior, even though at this time CA does not + // require the image to be in this space. Plain DeviceRGB images seem + // to be treated exactly the same, but better safe than sorry. + static CGColorSpaceRef imageColorSpace; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); + UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext(); + imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage)); + ASDisplayNodeCAssertNotNil(imageColorSpace, nil); + UIGraphicsEndImageContext(); + }); + + // Retrieve our buffer and create a CGDataProvider from it. + ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey); + ASDisplayNodeCAssertNotNil(buffer, nil); + CGDataProviderRef provider = [buffer createDataProviderAndInvalidate]; + + // Create the CGImage. Options taken from CGBitmapContextCreateImage. + CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault); + CGDataProviderRelease(provider); + + // We saved our GState right after setting the CTM so that we could restore it + // here and get the original scale back. + CGContextRestoreGState(context); + CGFloat scale = CGContextGetCTM(context).a; + + // Note: popping from the UIKit stack will probably destroy the context. + context = NULL; + UIGraphicsPopContext(); + + UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp]; + CGImageRelease(cgImg); + return result; +} + +extern void ASGraphicsEndImageContext() +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIGraphicsEndImageContext(); + return; + } + + UIGraphicsPopContext(); +} diff --git a/Source/Details/ASImageProtocols.h b/Source/Details/ASImageProtocols.h index bc671866a..c99867219 100644 --- a/Source/Details/ASImageProtocols.h +++ b/Source/Details/ASImageProtocols.h @@ -37,7 +37,7 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i @param URL The URL of the image to retrieve from the cache. @param callbackQueue The queue to call `completion` on. @param completion The block to be called when the cache has either hit or missed. - @discussion If `URL` is nil, `completion` should be invoked immediately with a nil image. This method should not block + @discussion If `URL` is nil, `completion` will be invoked immediately with a nil image. This method should not block the calling thread as it is likely to be called from the main thread. */ - (void)cachedImageWithURL:(NSURL *)URL @@ -66,27 +66,15 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i */ - (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL; -/** - @abstract Attempts to fetch an image with the given URLs from the cache in reverse order. - @param URLs The URLs of the image to retrieve from the cache. - @param callbackQueue The queue to call `completion` on. - @param completion The block to be called when the cache has either hit or missed. - @discussion If `URLs` is nil or empty, `completion` should be invoked immediately with a nil image. This method should not block - the calling thread as it is likely to be called from the main thread. - @see downloadImageWithURLs:callbackQueue:downloadProgress:completion: - */ -- (void)cachedImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - completion:(ASImageCacherCompletion)completion; - @end /** @param image The image that was downloaded, if the image could be successfully downloaded; nil otherwise. @param error An error describing why the download of `URL` failed, if the download failed; nil otherwise. @param downloadIdentifier The identifier for the download task that completed. + @param userInfo Any additional info that your downloader would like to communicate through Texture. */ -typedef void(^ASImageDownloaderCompletion)(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier); +typedef void(^ASImageDownloaderCompletion)(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo); /** @param progress The progress of the download, in the range of (0.0, 1.0), inclusive. @@ -167,21 +155,6 @@ typedef NS_ENUM(NSUInteger, ASImageDownloaderPriority) { - (void)setPriority:(ASImageDownloaderPriority)priority withDownloadIdentifier:(id)downloadIdentifier; -/** - @abstract Downloads an image from a list of URLs depending on previously observed network speed conditions. - @param URLs An array of URLs ordered by the cost of downloading them, the URL at index 0 being the lowest cost. - @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. - @param downloadProgress The block to be invoked when the download of `URL` progresses. - @param completion The block to be invoked when the download has completed, or has failed. - @discussion This method is likely to be called on the main thread, so any custom implementations should make sure to background any expensive download operations. - @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must - retain the identifier if you wish to use it later. - */ -- (nullable id)downloadImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress - completion:(ASImageDownloaderCompletion)completion; - @end @protocol ASAnimatedImageProtocol diff --git a/Source/Details/ASIntegerMap.h b/Source/Details/ASIntegerMap.h index 2b94a8fce..86e288942 100644 --- a/Source/Details/ASIntegerMap.h +++ b/Source/Details/ASIntegerMap.h @@ -29,7 +29,7 @@ AS_SUBCLASSING_RESTRICTED */ + (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount deleted:(nullable NSIndexSet *)deleted - inserted:(nullable NSIndexSet *)inserted; + inserted:(nullable NSIndexSet *)inserted NS_RETURNS_RETAINED; /** * A singleton that maps each integer to itself. Its inverse is itself. @@ -37,6 +37,7 @@ AS_SUBCLASSING_RESTRICTED * Note: You cannot mutate this. */ @property (class, atomic, readonly) ASIntegerMap *identityMap; ++ (ASIntegerMap *)identityMap NS_RETURNS_RETAINED; /** * A singleton that returns NSNotFound for all keys. Its inverse is itself. @@ -44,6 +45,7 @@ AS_SUBCLASSING_RESTRICTED * Note: You cannot mutate this. */ @property (class, atomic, readonly) ASIntegerMap *emptyMap; ++ (ASIntegerMap *)emptyMap NS_RETURNS_RETAINED; /** * Retrieves the integer for a given key, or NSNotFound if the key is not found. diff --git a/Source/Details/ASIntegerMap.mm b/Source/Details/ASIntegerMap.mm index 4208b71a6..e80ab7bfa 100644 --- a/Source/Details/ASIntegerMap.mm +++ b/Source/Details/ASIntegerMap.mm @@ -13,7 +13,7 @@ #import "ASIntegerMap.h" #import #import -#import +#import #import /** @@ -31,7 +31,7 @@ @implementation ASIntegerMap { #pragma mark - Singleton -+ (ASIntegerMap *)identityMap ++ (ASIntegerMap *)identityMap NS_RETURNS_RETAINED { static ASIntegerMap *identityMap; static dispatch_once_t onceToken; @@ -43,7 +43,7 @@ + (ASIntegerMap *)identityMap return identityMap; } -+ (ASIntegerMap *)emptyMap ++ (ASIntegerMap *)emptyMap NS_RETURNS_RETAINED { static ASIntegerMap *emptyMap; static dispatch_once_t onceToken; @@ -55,7 +55,7 @@ + (ASIntegerMap *)emptyMap return emptyMap; } -+ (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount deleted:(NSIndexSet *)deletions inserted:(NSIndexSet *)insertions ++ (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount deleted:(NSIndexSet *)deletions inserted:(NSIndexSet *)insertions NS_RETURNS_RETAINED { if (oldCount == 0) { return ASIntegerMap.emptyMap; diff --git a/Source/Details/ASPINRemoteImageDownloader.h b/Source/Details/ASPINRemoteImageDownloader.h index 179104901..14b207dd9 100644 --- a/Source/Details/ASPINRemoteImageDownloader.h +++ b/Source/Details/ASPINRemoteImageDownloader.h @@ -28,12 +28,14 @@ NS_ASSUME_NONNULL_BEGIN @interface ASPINRemoteImageDownloader : NSObject /** - * A shared image downloader which can be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes + * A shared image downloader which can be used by @c ASNetworkImageNodes and @c ASMultiplexImageNodes. + * The userInfo provided by this downloader is an instance of `PINRemoteImageManagerResult`. * * This is the default downloader used by network backed image nodes if PINRemoteImage and PINCache are * available. It uses PINRemoteImage's features to provide caching and progressive image downloads. */ -+ (ASPINRemoteImageDownloader *)sharedDownloader; +@property (class, readonly) ASPINRemoteImageDownloader *sharedDownloader; ++ (ASPINRemoteImageDownloader *)sharedDownloader NS_RETURNS_RETAINED; /** diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 05107f8e0..eff1cb6fd 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -114,7 +114,7 @@ @interface ASPINRemoteImageDownloader () @implementation ASPINRemoteImageDownloader -+ (instancetype)sharedDownloader ++ (ASPINRemoteImageDownloader *)sharedDownloader NS_RETURNS_RETAINED { static dispatch_once_t onceToken = 0; @@ -202,32 +202,19 @@ - (void)cachedImageWithURL:(NSURL *)URL 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); - }); - } -} - -- (void)cachedImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - completion:(ASImageCacherCompletion)completion -{ - [self cachedImageWithURL:[URLs lastObject] - callbackQueue:callbackQueue - completion:^(id _Nullable imageFromCache) { - if (imageFromCache.asdk_image == nil && URLs.count > 1) { - [self cachedImageWithURLs:[URLs subarrayWithRange:NSMakeRange(0, URLs.count - 1)] - callbackQueue:callbackQueue - completion:completion]; - } else { - completion(imageFromCache); - } - }]; + [[self sharedPINRemoteImageManager] imageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ +#if PIN_ANIMATED_AVAILABLE + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation); + } else { + completion(result.image); + } +#else + completion(result.image); +#endif + }]; + }]; } - (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL @@ -244,63 +231,38 @@ - (nullable id)downloadImageWithURL:(NSURL *)URL downloadProgress:(ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion; { - NSArray *URLs = nil; - if (URL) { - URLs = @[URL]; - } - return [self downloadImageWithURLs:URLs callbackQueue:callbackQueue downloadProgress:downloadProgress completion:completion]; -} + PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { + if (downloadProgress == nil) { return; } -- (nullable id)downloadImageWithURLs:(NSArray *)URLs - callbackQueue:(dispatch_queue_t)callbackQueue - downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress - completion:(ASImageDownloaderCompletion)completion -{ - 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()) { - downloadProgress(completedBytes / (CGFloat)totalBytes); - } else { - dispatch_async(callbackQueue, ^{ - downloadProgress(completedBytes / (CGFloat)totalBytes); - }); - } - }; - - PINRemoteImageManagerImageCompletion imageCompletion = ^(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()) { -#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 - } else { - dispatch_async(callbackQueue, ^{ + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ + downloadProgress(completedBytes / (CGFloat)totalBytes); + }]; + }; + + PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) { + [ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{ #if PIN_ANIMATED_AVAILABLE - if (result.alternativeRepresentation) { - completion(result.alternativeRepresentation, result.error, result.UUID); - } else { - completion(result.image, result.error, result.UUID); - } + if (result.alternativeRepresentation) { + completion(result.alternativeRepresentation, result.error, result.UUID, result); + } else { + completion(result.image, result.error, result.UUID, result); + } #else - completion(result.image, result.error, result.UUID); + completion(result.image, result.error, result.UUID, result); #endif - }); - } - }; - - return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs - options:PINRemoteImageManagerDownloadOptionsSkipDecode - progressImage:nil - progressDownload:progressDownload - completion:imageCompletion]; + }]; + }; + + // 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 @@ -369,5 +331,29 @@ - (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageMana 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/ASPageTable.h b/Source/Details/ASPageTable.h index e295e0256..b512f0dc6 100644 --- a/Source/Details/ASPageTable.h +++ b/Source/Details/ASPageTable.h @@ -82,12 +82,12 @@ typedef ASPageTable *> AS /** * Creates a new page table with (NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) for values. */ -+ (ASPageTable *)pageTableForStrongObjectPointers; ++ (ASPageTable *)pageTableForStrongObjectPointers NS_RETURNS_RETAINED; /** * Creates a new page table with (NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) for values. */ -+ (ASPageTable *)pageTableForWeakObjectPointers; ++ (ASPageTable *)pageTableForWeakObjectPointers NS_RETURNS_RETAINED; /** * Builds a new page to layout attributes from the given layout attributes. @@ -98,7 +98,7 @@ typedef ASPageTable *> AS * * @param pageSize The size of each page. */ -+ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize NS_RETURNS_RETAINED; /** * Retrieves the object for a given page, or nil if the page is not found. diff --git a/Source/Details/ASPageTable.m b/Source/Details/ASPageTable.m index 5cbf758dd..6855b41b6 100644 --- a/Source/Details/ASPageTable.m +++ b/Source/Details/ASPageTable.m @@ -79,7 +79,7 @@ extern CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSiz @implementation NSMapTable (ASPageTableMethods) -+ (instancetype)pageTableWithValuePointerFunctions:(NSPointerFunctions *)valueFuncs ++ (instancetype)pageTableWithValuePointerFunctions:(NSPointerFunctions *)valueFuncs NS_RETURNS_RETAINED { static NSPointerFunctions *pageCoordinatesFuncs; static dispatch_once_t onceToken; @@ -90,7 +90,7 @@ + (instancetype)pageTableWithValuePointerFunctions:(NSPointerFunctions *)valueFu return [[NSMapTable alloc] initWithKeyPointerFunctions:pageCoordinatesFuncs valuePointerFunctions:valueFuncs capacity:0]; } -+ (ASPageTable *)pageTableForStrongObjectPointers ++ (ASPageTable *)pageTableForStrongObjectPointers NS_RETURNS_RETAINED { static NSPointerFunctions *strongObjectPointerFuncs; static dispatch_once_t onceToken; @@ -100,7 +100,7 @@ + (ASPageTable *)pageTableForStrongObjectPointers return [self pageTableWithValuePointerFunctions:strongObjectPointerFuncs]; } -+ (ASPageTable *)pageTableForWeakObjectPointers ++ (ASPageTable *)pageTableForWeakObjectPointers NS_RETURNS_RETAINED { static NSPointerFunctions *weakObjectPointerFuncs; static dispatch_once_t onceToken; @@ -110,7 +110,7 @@ + (ASPageTable *)pageTableForWeakObjectPointers return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; } -+ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize NS_RETURNS_RETAINED { ASPageToLayoutAttributesTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { diff --git a/Source/Details/ASPhotosFrameworkImageRequest.h b/Source/Details/ASPhotosFrameworkImageRequest.h index 1ecd07dec..54f55047a 100644 --- a/Source/Details/ASPhotosFrameworkImageRequest.h +++ b/Source/Details/ASPhotosFrameworkImageRequest.h @@ -26,6 +26,7 @@ extern NSString *const ASPhotosURLScheme; @abstract Use ASPhotosFrameworkImageRequest to encapsulate all the information needed to request an image from the Photos framework and store it in a URL. */ +API_AVAILABLE(ios(8.0), tvos(10.0)) @interface ASPhotosFrameworkImageRequest : NSObject - (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier NS_DESIGNATED_INITIALIZER; diff --git a/Source/Details/ASRangeController.h b/Source/Details/ASRangeController.h index 152c4e8b5..912027632 100644 --- a/Source/Details/ASRangeController.h +++ b/Source/Details/ASRangeController.h @@ -64,6 +64,11 @@ AS_SUBCLASSING_RESTRICTED */ - (void)updateIfNeeded; +/** + * Update the ranges immediately. + */ +- (void)updateRanges; + /** * Add the sized node for `indexPath` as a subview of `contentView`. * @@ -103,7 +108,7 @@ AS_SUBCLASSING_RESTRICTED /** * Property that indicates whether the scroll view for this range controller has ever changed its contentOffset. */ -@property (nonatomic, assign) BOOL contentOffsetHasChanged; +@property (nonatomic, assign) BOOL contentHasBeenScrolled; @end diff --git a/Source/Details/ASRangeController.mm b/Source/Details/ASRangeController.mm index 4d10838b7..898da690a 100644 --- a/Source/Details/ASRangeController.mm +++ b/Source/Details/ASRangeController.mm @@ -45,7 +45,7 @@ @interface ASRangeController () NSSet *_allPreviousIndexPaths; NSHashTable *_visibleNodes; ASLayoutRangeMode _currentRangeMode; - BOOL _contentOffsetHasChanged; + BOOL _contentHasBeenScrolled; BOOL _preserveCurrentRangeMode; BOOL _didRegisterForNodeDisplayNotifications; CFTimeInterval _pendingDisplayNodesTimestamp; @@ -78,7 +78,7 @@ - (instancetype)init _rangeIsValid = YES; _currentRangeMode = ASLayoutRangeModeUnspecified; - _contentOffsetHasChanged = NO; + _contentHasBeenScrolled = NO; _preserveCurrentRangeMode = NO; _previousScrollDirection = ASScrollDirectionDown | ASScrollDirectionRight; @@ -154,12 +154,16 @@ - (void)setNeedsUpdate - (void)updateIfNeeded { if (_needsRangeUpdate) { - _needsRangeUpdate = NO; - - [self _updateVisibleNodeIndexPaths]; + [self updateRanges]; } } +- (void)updateRanges +{ + _needsRangeUpdate = NO; + [self _updateVisibleNodeIndexPaths]; +} + - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode { _preserveCurrentRangeMode = YES; @@ -224,10 +228,6 @@ - (void)_updateVisibleNodeIndexPaths auto visibleElements = [_dataSource visibleElementsForRangeController:self]; NSHashTable *newVisibleNodes = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; - if (visibleElements.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... - [self _setVisibleNodes:newVisibleNodes]; - return; // don't do anything for this update, but leave _rangeIsValid == NO to make sure we update it later - } ASSignpostStart(ASSignpostRangeControllerUpdate); // Get the scroll direction. Default to using the previous one, if they're not scrolling. @@ -236,10 +236,22 @@ - (void)_updateVisibleNodeIndexPaths scrollDirection = _previousScrollDirection; } _previousScrollDirection = scrollDirection; - + + if (visibleElements.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... + // Verify the actual state by checking the layout with a "VisibleOnly" range. + // This allows us to avoid thrashing through -didExitVisibleState in the case of -reloadData, since that generates didEndDisplayingCell calls. + // Those didEndDisplayingCell calls result in items being removed from the visibleElements returned by the _dataSource, even though the layout remains correct. + visibleElements = [_layoutController elementsForScrolling:scrollDirection rangeMode:ASLayoutRangeModeVisibleOnly rangeType:ASLayoutRangeTypeDisplay map:map]; + for (ASCollectionElement *element in visibleElements) { + [newVisibleNodes addObject:element.node]; + } + [self _setVisibleNodes:newVisibleNodes]; + return; // don't do anything for this update, but leave _rangeIsValid == NO to make sure we update it later + } + ASInterfaceState selfInterfaceState = [self interfaceState]; ASLayoutRangeMode rangeMode = _currentRangeMode; - BOOL updateRangeMode = !_preserveCurrentRangeMode && _contentOffsetHasChanged; + BOOL updateRangeMode = (!_preserveCurrentRangeMode && _contentHasBeenScrolled); // If we've never scrolled before, we never update the range mode, so it doesn't jump into Full too early. // This can happen if we have multiple, noisy updates occurring from application code before the user has engaged. @@ -345,7 +357,7 @@ - (void)_updateVisibleNodeIndexPaths // instant we come onscreen. So, preload and display all of those things, but don't waste resources preloading yet. // We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:. - if ([allCurrentIndexPaths containsObject:indexPath]) { + if ([visibleIndexPaths containsObject:indexPath]) { //Propose changes // DO NOT set Visible: even though these elements are in the visible range / "viewport", // our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above @@ -358,7 +370,17 @@ - (void)_updateVisibleNodeIndexPaths // In that case we'll just set it back to MeasureLayout. Only set Display | Preload if in allCurrentIndexPaths. interfaceState |= ASInterfaceStateDisplay; } + } else if ([displayIndexPaths containsObject:indexPath]) { //Propose changes + interfaceState |= ASInterfaceStatePreload; + } else { + interfaceState = ASInterfaceStateNone; } + +// else { +// interfaceState |= ASInterfaceStatePreload; +// } + // we may not want this because preserve presload state can avoid loading data with thrash + } ASCellNode *node = [map elementForItemAtIndexPath:indexPath].nodeIfAllocated; @@ -368,7 +390,7 @@ - (void)_updateVisibleNodeIndexPaths [newVisibleNodes addObject:node]; } // Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState. - if (node.interfaceState != interfaceState) { + if (node.pendingInterfaceState != interfaceState) { #if ASRangeControllerLoggingEnabled [modifiedIndexPaths addObject:indexPath]; #endif diff --git a/Source/Details/ASRecursiveUnfairLock.h b/Source/Details/ASRecursiveUnfairLock.h new file mode 100644 index 000000000..fff42b59c --- /dev/null +++ b/Source/Details/ASRecursiveUnfairLock.h @@ -0,0 +1,59 @@ +// +// ASRecursiveUnfairLock.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +// Don't import C-only header if we're in a C++ file +#ifndef __cplusplus +#import +#endif + +// Note: We don't use ATOMIC_VAR_INIT here because C++ compilers don't like it, +// and it literally does absolutely nothing. +#define AS_RECURSIVE_UNFAIR_LOCK_INIT ((ASRecursiveUnfairLock){ OS_UNFAIR_LOCK_INIT, NULL, 0}) + +NS_ASSUME_NONNULL_BEGIN + +OS_UNFAIR_LOCK_AVAILABILITY +typedef struct { + os_unfair_lock _lock; + _Atomic(pthread_t) _thread; // Write-protected by lock + int _count; // Protected by lock +} ASRecursiveUnfairLock; + +CF_EXTERN_C_BEGIN + +/** + * Lock, blocking if needed. + */ +OS_UNFAIR_LOCK_AVAILABILITY +void ASRecursiveUnfairLockLock(ASRecursiveUnfairLock *l); + +/** + * Try to lock without blocking. Returns whether we took the lock. + */ +OS_UNFAIR_LOCK_AVAILABILITY +BOOL ASRecursiveUnfairLockTryLock(ASRecursiveUnfairLock *l); + +/** + * Unlock. Calling this on a thread that does not own + * the lock will result in an assertion failure, and undefined + * behavior if foundation assertions are disabled. + */ +OS_UNFAIR_LOCK_AVAILABILITY +void ASRecursiveUnfairLockUnlock(ASRecursiveUnfairLock *l); + +CF_EXTERN_C_END + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASRecursiveUnfairLock.m b/Source/Details/ASRecursiveUnfairLock.m new file mode 100644 index 000000000..a07b751c4 --- /dev/null +++ b/Source/Details/ASRecursiveUnfairLock.m @@ -0,0 +1,85 @@ +// +// ASRecursiveUnfairLock.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASRecursiveUnfairLock.h" + +/** + * For our atomic _thread, we use acquire/release memory order so that we can have + * the minimum possible constraint on the hardware. The default, `memory_order_seq_cst` + * demands that there be a total order of all such modifications as seen by all threads. + * Acquire/release only requires that modifications to this specific atomic are + * synchronized across acquire/release pairs. + * http://en.cppreference.com/w/cpp/atomic/memory_order + * + * Note also that the unfair_lock involves a thread fence as well, so we don't need to + * take care of synchronizing other values. Just the thread value. + */ +#define rul_set_thread(l, t) atomic_store_explicit(&l->_thread, t, memory_order_release) +#define rul_get_thread(l) atomic_load_explicit(&l->_thread, memory_order_acquire) + +void ASRecursiveUnfairLockLock(ASRecursiveUnfairLock *l) +{ + // Try to lock without blocking. If we fail, check what thread owns it. + // Note that the owning thread CAN CHANGE freely, but it can't become `self` + // because only we are `self`. And if it's already `self` then we already have + // the lock, because we reset it to NULL before we unlock. So (thread == self) is + // invariant. + + const pthread_t s = pthread_self(); + if (os_unfair_lock_trylock(&l->_lock)) { + // Owned by nobody. We now have the lock. Assign self. + rul_set_thread(l, s); + } else if (rul_get_thread(l) == s) { + // Owned by self (recursive lock). nop. + } else { + // Owned by other thread. Block and then set thread to self. + os_unfair_lock_lock(&l->_lock); + rul_set_thread(l, s); + } + + l->_count++; +} + +BOOL ASRecursiveUnfairLockTryLock(ASRecursiveUnfairLock *l) +{ + // Same as Lock above. See comments there. + + const pthread_t s = pthread_self(); + if (os_unfair_lock_trylock(&l->_lock)) { + // Owned by nobody. We now have the lock. Assign self. + rul_set_thread(l, s); + } else if (rul_get_thread(l) == s) { + // Owned by self (recursive lock). nop. + } else { + // Owned by other thread. Fail. + return NO; + } + + l->_count++; + return YES; +} + +void ASRecursiveUnfairLockUnlock(ASRecursiveUnfairLock *l) +{ + // Ensure we have the lock. This check may miss some pathological cases, + // but it'll catch 99.999999% of this serious programmer error. + NSCAssert(rul_get_thread(l) == pthread_self(), @"Unlocking from a different thread than locked."); + + if (0 == --l->_count) { + // Note that we have to clear this before unlocking because, if another thread + // succeeds in locking above, but hasn't managed to update _thread, and we + // try to re-lock, and fail the -tryLock, and read _thread, then we'll mistakenly + // think that we still own the lock and proceed without blocking. + rul_set_thread(l, NULL); + os_unfair_lock_unlock(&l->_lock); + } +} diff --git a/Source/Details/ASThread.h b/Source/Details/ASThread.h index 989db9a26..198a8c810 100644 --- a/Source/Details/ASThread.h +++ b/Source/Details/ASThread.h @@ -15,22 +15,82 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#pragma once +#import #import +#import #import #import #import #import +#import #import +#import +#import - -static inline BOOL ASDisplayNodeThreadIsMain() +ASDISPLAYNODE_INLINE BOOL ASDisplayNodeThreadIsMain() { return 0 != pthread_main_np(); } +/** + * Adds the lock to the current scope. + * + * A C version of the C++ lockers. Pass in any id. + * One benefit this has over C++ lockers is that the lock is retained. We + * had bugs in the past where an object would be deallocated while someone + * had locked its instanceLock, and we'd get a crash. This macro + * retains the locked object until it can be unlocked, which is nice. + */ +#define ASLockScope(nsLocking) \ + id __lockToken __attribute__((cleanup(_ASLockScopeCleanup))) NS_VALID_UNTIL_END_OF_SCOPE = nsLocking; \ + [__lockToken lock]; + +/// Same as ASLockScope(1) but lock isn't retained (be careful). +#define ASLockScopeUnowned(nsLocking) \ + __unsafe_unretained id __lockToken __attribute__((cleanup(_ASLockScopeUnownedCleanup))) = nsLocking; \ + [__lockToken lock]; + +ASDISPLAYNODE_INLINE void _ASLockScopeCleanup(id __strong * const lockPtr) { + [*lockPtr unlock]; +} + +ASDISPLAYNODE_INLINE void _ASLockScopeUnownedCleanup(id __unsafe_unretained * const lockPtr) { + [*lockPtr unlock]; +} + +/** + * Same as ASLockScope(1) but it uses self, so we can skip retain/release. + */ +#define ASLockScopeSelf() ASLockScopeUnowned(self) + +/// One-liner while holding the lock. +#define ASLocked(nsLocking, expr) ({ ASLockScope(nsLocking); expr; }) + +/// Faster self-version. +#define ASLockedSelf(expr) ({ ASLockScopeSelf(); expr; }) + +#define ASLockedSelfCompareAssign(lvalue, newValue) \ + ASLockedSelf(ASCompareAssign(lvalue, newValue)) + +#define ASLockedSelfCompareAssignObjects(lvalue, newValue) \ + ASLockedSelf(ASCompareAssignObjects(lvalue, newValue)) + +#define ASLockedSelfCompareAssignCustom(lvalue, newValue, isequal) \ + ASLockedSelf(ASCompareAssignCustom(lvalue, newValue, isequal)) + +#define ASLockedSelfCompareAssignCopy(lvalue, obj) \ + ASLockedSelf(ASCompareAssignCopy(lvalue, obj)) + +#define ASUnlockScope(nsLocking) \ + id __lockToken __attribute__((cleanup(_ASUnlockScopeCleanup))) NS_VALID_UNTIL_END_OF_SCOPE = nsLocking; \ + [__lockToken unlock]; + +ASDISPLAYNODE_INLINE void _ASUnlockScopeCleanup(id __strong *lockPtr) { + [*lockPtr lock]; +} + #ifdef __cplusplus #define TIME_LOCKER 0 @@ -55,13 +115,10 @@ static inline BOOL ASDisplayNodeThreadIsMain() // 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.) -#define ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(x_) do { \ - _Pragma("clang diagnostic push"); \ - _Pragma("clang diagnostic ignored \"-Wunused-variable\""); \ - volatile int res = (x_); \ - ASDisplayNodeCAssert(res == 0, @"Expected %@ to return 0, got %d instead", @#x_, res); \ - _Pragma("clang diagnostic pop"); \ -} while (0) +#define AS_POSIX_ASSERT_NOERR(x_) ({ \ + __unused int res = (x_); \ + ASDisplayNodeCAssert(res == 0, @"Expected %s to return 0, got %d instead. Error: %s", #x_, res, strerror(res)); \ +}) /** * Assert if the current thread owns a mutex. @@ -177,25 +234,26 @@ namespace ASDN { Unlocker(Unlocker&) = delete; Unlocker &operator=(Unlocker&) = delete; }; - - template - class SharedUnlocker - { - std::shared_ptr _l; - public: - SharedUnlocker (std::shared_ptr const& l) ASDISPLAYNODE_NOTHROW : _l (l) { _l->unlock (); } - ~SharedUnlocker () { _l->lock (); } - SharedUnlocker(SharedUnlocker&) = delete; - SharedUnlocker &operator=(SharedUnlocker&) = delete; - }; + // Set once in Mutex constructor. Linker fails if this is a member variable. ?? + static BOOL gMutex_unfair; + +// Silence unguarded availability warnings in here, because +// perf is critical and we will check availability once +// and not again. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" struct Mutex { /// Constructs a non-recursive mutex (the default). Mutex () : Mutex (false) {} ~Mutex () { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_destroy (&_m)); + if (gMutex_unfair) { + // nop + } else { + AS_POSIX_ASSERT_NOERR(pthread_mutex_destroy (&_m)); + } #if CHECK_LOCKING_SAFETY _owner = 0; _count = 0; @@ -205,8 +263,16 @@ namespace ASDN { Mutex (const Mutex&) = delete; Mutex &operator=(const Mutex&) = delete; - void lock () { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_lock (this->mutex())); + void lock() { + if (gMutex_unfair) { + if (_recursive) { + ASRecursiveUnfairLockLock(&_runfair); + } else { + os_unfair_lock_lock(&_unfair); + } + } else { + AS_POSIX_ASSERT_NOERR(pthread_mutex_lock(&_m)); + } #if CHECK_LOCKING_SAFETY mach_port_t thread_id = pthread_mach_thread_np(pthread_self()); if (thread_id != _owner) { @@ -235,7 +301,15 @@ namespace ASDN { _owner = 0; } #endif - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_unlock (this->mutex())); + if (gMutex_unfair) { + if (_recursive) { + ASRecursiveUnfairLockUnlock(&_runfair); + } else { + os_unfair_lock_unlock(&_unfair); + } + } else { + AS_POSIX_ASSERT_NOERR(pthread_mutex_unlock(&_m)); + } } pthread_mutex_t *mutex () { return &_m; } @@ -248,29 +322,57 @@ namespace ASDN { protected: explicit Mutex (bool recursive) { - if (!recursive) { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_init (&_m, NULL)); + + // Check if we can use unfair lock and store in static var. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + gMutex_unfair = ASActivateExperimentalFeature(ASExperimentalUnfairLock); + } + }); + + _recursive = recursive; + + if (gMutex_unfair) { + if (recursive) { + _runfair = AS_RECURSIVE_UNFAIR_LOCK_INIT; + } else { + _unfair = OS_UNFAIR_LOCK_INIT; + } } else { - pthread_mutexattr_t attr; - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutexattr_init (&attr)); - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE)); - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_init (&_m, &attr)); - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutexattr_destroy (&attr)); + if (!recursive) { + AS_POSIX_ASSERT_NOERR(pthread_mutex_init (&_m, NULL)); + } else { + // Fall back to recursive mutex. + static pthread_mutexattr_t attr; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + AS_POSIX_ASSERT_NOERR(pthread_mutexattr_init (&attr)); + AS_POSIX_ASSERT_NOERR(pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE)); + }); + AS_POSIX_ASSERT_NOERR(pthread_mutex_init(&_m, &attr)); + } } #if CHECK_LOCKING_SAFETY _owner = 0; _count = 0; #endif } - + private: - pthread_mutex_t _m; + BOOL _recursive; + union { + os_unfair_lock _unfair; + ASRecursiveUnfairLock _runfair; + pthread_mutex_t _m; + }; #if CHECK_LOCKING_SAFETY mach_port_t _owner; uint32_t _count; #endif }; - +#pragma clang diagnostic pop // ignored "-Wunguarded-availability" + /** Obj-C doesn't allow you to pass parameters to C++ ivar constructors. Provide a convenience to change the default from non-recursive to recursive. @@ -288,7 +390,6 @@ namespace ASDN { typedef Locker MutexLocker; typedef SharedLocker MutexSharedLocker; typedef Unlocker MutexUnlocker; - typedef SharedUnlocker MutexSharedUnlocker; /** If you are creating a static mutex, use StaticMutex. This avoids expensive constructor overhead at startup (or worse, ordering @@ -306,11 +407,11 @@ namespace ASDN { StaticMutex &operator=(const StaticMutex&) = delete; void lock () { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_lock (this->mutex())); + AS_POSIX_ASSERT_NOERR(pthread_mutex_lock (this->mutex())); } void unlock () { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_unlock (this->mutex())); + AS_POSIX_ASSERT_NOERR(pthread_mutex_unlock (this->mutex())); } pthread_mutex_t *mutex () { return &_m; } @@ -322,100 +423,6 @@ namespace ASDN { typedef Locker StaticMutexLocker; typedef Unlocker StaticMutexUnlocker; - struct Condition - { - Condition () { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_init(&_c, NULL)); - } - - ~Condition () { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_destroy(&_c)); - } - - // non-copyable. - Condition(const Condition&) = delete; - Condition &operator=(const Condition&) = delete; - - void signal() { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_signal(&_c)); - } - - void wait(Mutex &m) { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_wait(&_c, m.mutex())); - } - - pthread_cond_t *condition () { - return &_c; - } - - private: - pthread_cond_t _c; - }; - - struct ReadWriteLock - { - ReadWriteLock() { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_init(&_rwlock, NULL)); - } - - ~ReadWriteLock() { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_destroy(&_rwlock)); - } - - // non-copyable. - ReadWriteLock(const ReadWriteLock&) = delete; - ReadWriteLock &operator=(const ReadWriteLock&) = delete; - - void readlock() { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_rdlock(&_rwlock)); - } - - void writelock() { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_wrlock(&_rwlock)); - } - - void unlock() { - ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_unlock(&_rwlock)); - } - - private: - pthread_rwlock_t _rwlock; - }; - - class ReadWriteLockReadLocker - { - ReadWriteLock &_lock; - public: - ReadWriteLockReadLocker(ReadWriteLock &lock) ASDISPLAYNODE_NOTHROW : _lock(lock) { - _lock.readlock(); - } - - ~ReadWriteLockReadLocker() { - _lock.unlock(); - } - - // non-copyable. - ReadWriteLockReadLocker(const ReadWriteLockReadLocker&) = delete; - ReadWriteLockReadLocker &operator=(const ReadWriteLockReadLocker&) = delete; - }; - - class ReadWriteLockWriteLocker - { - ReadWriteLock &_lock; - public: - ReadWriteLockWriteLocker(ReadWriteLock &lock) ASDISPLAYNODE_NOTHROW : _lock(lock) { - _lock.writelock(); - } - - ~ReadWriteLockWriteLocker() { - _lock.unlock(); - } - - // non-copyable. - ReadWriteLockWriteLocker(const ReadWriteLockWriteLocker&) = delete; - ReadWriteLockWriteLocker &operator=(const ReadWriteLockWriteLocker&) = delete; - }; - } // namespace ASDN #endif /* __cplusplus */ diff --git a/Source/Details/ASTraitCollection.h b/Source/Details/ASTraitCollection.h index fdff5c0b1..287bf5e43 100644 --- a/Source/Details/ASTraitCollection.h +++ b/Source/Details/ASTraitCollection.h @@ -17,6 +17,7 @@ #import + #import @class ASTraitCollection; @@ -27,14 +28,59 @@ NS_ASSUME_NONNULL_BEGIN ASDISPLAYNODE_EXTERN_C_BEGIN +#pragma mark - ASPrimitiveContentSizeCategory + +/** + * ASPrimitiveContentSizeCategory is a UIContentSizeCategory that can be used inside a struct. + * + * We need an unretained pointer because ARC can't manage struct memory. + * + * WARNING: DO NOT cast UIContentSizeCategory values to ASPrimitiveContentSizeCategory directly. + * Use ASPrimitiveContentSizeCategoryMake(UIContentSizeCategory) instead. + * This is because we make some assumptions about the lifetime of the object it points to. + * Also note that cast from ASPrimitiveContentSizeCategory to UIContentSizeCategory is always safe. + */ +typedef __unsafe_unretained UIContentSizeCategory ASPrimitiveContentSizeCategory; + +/** + * Safely casts from UIContentSizeCategory to ASPrimitiveContentSizeCategory. + * + * The UIKit documentation doesn't specify if we can receive a copy of the UIContentSizeCategory constant. While getting + * copies is fine with ARC, usage of unretained pointers requires us to ensure the lifetime of the object it points to. + * Manual retain&release of the UIContentSizeCategory object is not an option because it would require us to do that + * everywhere ASPrimitiveTraitCollection is used. This is error-prone and can lead to crashes and memory leaks. So, we + * explicitly limit possible values of ASPrimitiveContentSizeCategory to the predetermined set of global constants with + * known lifetime. + * + * @return a pointer to one of the UIContentSizeCategory constants. + */ +extern ASPrimitiveContentSizeCategory ASPrimitiveContentSizeCategoryMake(UIContentSizeCategory sizeCategory); + #pragma mark - ASPrimitiveTraitCollection +/** + * @abstract This is an internal struct-representation of ASTraitCollection. + * + * @discussion This struct is for internal use only. Framework users should always use ASTraitCollection. + * + * If you use ASPrimitiveTraitCollection, please do make sure to initialize it with ASPrimitiveTraitCollectionMakeDefault() + * or ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection*). + */ typedef struct ASPrimitiveTraitCollection { - CGFloat displayScale; UIUserInterfaceSizeClass horizontalSizeClass; - UIUserInterfaceIdiom userInterfaceIdiom; UIUserInterfaceSizeClass verticalSizeClass; + + CGFloat displayScale; + UIDisplayGamut displayGamut; + + UIUserInterfaceIdiom userInterfaceIdiom; UIForceTouchCapability forceTouchCapability; + UITraitEnvironmentLayoutDirection layoutDirection; +#if TARGET_OS_TV + UIUserInterfaceStyle userInterfaceStyle; +#endif + + ASPrimitiveContentSizeCategory preferredContentSizeCategory; CGSize containerSize; } ASPrimitiveTraitCollection; @@ -42,7 +88,7 @@ typedef struct ASPrimitiveTraitCollection { /** * Creates ASPrimitiveTraitCollection with default values. */ -extern ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault(); +extern ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault(void); /** * Creates a ASPrimitiveTraitCollection from a given UITraitCollection. @@ -74,17 +120,23 @@ ASDISPLAYNODE_EXTERN_C_END @protocol ASTraitEnvironment /** - * Returns a struct-representation of the environment's ASEnvironmentDisplayTraits. This only exists as a internal - * convenience method. Users should access the trait collections through the NSObject based asyncTraitCollection API + * @abstract Returns a struct-representation of the environment's ASEnvironmentDisplayTraits. + * + * @discussion This only exists as an internal convenience method. Users should access the trait collections through + * the NSObject based asyncTraitCollection API */ - (ASPrimitiveTraitCollection)primitiveTraitCollection; /** - * Sets a trait collection on this environment state. + * @abstract Sets a trait collection on this environment state. + * + * @discussion This only exists as an internal convenience method. Users should not override trait collection using it. + * Use [ASViewController overrideDisplayTraitsWithTraitCollection] block instead. */ - (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection; /** + * @abstract Returns the thread-safe UITraitCollection equivalent. */ - (ASTraitCollection *)asyncTraitCollection; @@ -124,29 +176,79 @@ ASDISPLAYNODE_EXTERN_C_END AS_SUBCLASSING_RESTRICTED @interface ASTraitCollection : NSObject -@property (nonatomic, assign, readonly) CGFloat displayScale; @property (nonatomic, assign, readonly) UIUserInterfaceSizeClass horizontalSizeClass; -@property (nonatomic, assign, readonly) UIUserInterfaceIdiom userInterfaceIdiom; @property (nonatomic, assign, readonly) UIUserInterfaceSizeClass verticalSizeClass; + +@property (nonatomic, assign, readonly) CGFloat displayScale; +@property (nonatomic, assign, readonly) UIDisplayGamut displayGamut; + +@property (nonatomic, assign, readonly) UIUserInterfaceIdiom userInterfaceIdiom; @property (nonatomic, assign, readonly) UIForceTouchCapability forceTouchCapability; +@property (nonatomic, assign, readonly) UITraitEnvironmentLayoutDirection layoutDirection; +#if TARGET_OS_TV +@property (nonatomic, assign, readonly) UIUserInterfaceStyle userInterfaceStyle; +#endif + +@property (nonatomic, assign, readonly) UIContentSizeCategory preferredContentSizeCategory; + @property (nonatomic, assign, readonly) CGSize containerSize; -+ (ASTraitCollection *)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits; ++ (ASTraitCollection *)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection + containerSize:(CGSize)windowSize NS_RETURNS_RETAINED; + (ASTraitCollection *)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection - containerSize:(CGSize)windowSize; + containerSize:(CGSize)windowSize + fallbackContentSizeCategory:(UIContentSizeCategory)fallbackContentSizeCategory NS_RETURNS_RETAINED; + +#if TARGET_OS_TV ++ (ASTraitCollection *)traitCollectionWithHorizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass + verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass + displayScale:(CGFloat)displayScale + displayGamut:(UIDisplayGamut)displayGamut + userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom + forceTouchCapability:(UIForceTouchCapability)forceTouchCapability + layoutDirection:(UITraitEnvironmentLayoutDirection)layoutDirection + userInterfaceStyle:(UIUserInterfaceStyle)userInterfaceStyle + preferredContentSizeCategory:(UIContentSizeCategory)preferredContentSizeCategory + containerSize:(CGSize)windowSize NS_RETURNS_RETAINED; +#else ++ (ASTraitCollection *)traitCollectionWithHorizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass + verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass + displayScale:(CGFloat)displayScale + displayGamut:(UIDisplayGamut)displayGamut + userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom + forceTouchCapability:(UIForceTouchCapability)forceTouchCapability + layoutDirection:(UITraitEnvironmentLayoutDirection)layoutDirection + preferredContentSizeCategory:(UIContentSizeCategory)preferredContentSizeCategory + containerSize:(CGSize)windowSize NS_RETURNS_RETAINED; +#endif + +- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection; + +@end + +/** + * These are internal helper methods. Should never be called by the framework users. + */ +@interface ASTraitCollection (PrimitiveTraits) ++ (ASTraitCollection *)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits NS_RETURNS_RETAINED; + +- (ASPrimitiveTraitCollection)primitiveTraitCollection; + +@end + +@interface ASTraitCollection (Deprecated) + +- (instancetype)init ASDISPLAYNODE_DEPRECATED_MSG("The default constructor of this class is going to become unavailable. Use other constructors instead."); + (ASTraitCollection *)traitCollectionWithDisplayScale:(CGFloat)displayScale userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom horizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass forceTouchCapability:(UIForceTouchCapability)forceTouchCapability - containerSize:(CGSize)windowSize; - - -- (ASPrimitiveTraitCollection)primitiveTraitCollection; -- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection; + containerSize:(CGSize)windowSize + NS_RETURNS_RETAINED ASDISPLAYNODE_DEPRECATED_MSG("Use full version of this method instead."); @end diff --git a/Source/Details/ASTraitCollection.m b/Source/Details/ASTraitCollection.m index 0c845e9f6..9b1902c9c 100644 --- a/Source/Details/ASTraitCollection.m +++ b/Source/Details/ASTraitCollection.m @@ -20,6 +20,64 @@ #import #import +#pragma mark - ASPrimitiveContentSizeCategory + +// UIContentSizeCategoryUnspecified is available only in iOS 10.0 and later. +// This is used for compatibility with older iOS versions. +ASDISPLAYNODE_INLINE UIContentSizeCategory AS_UIContentSizeCategoryUnspecified() { + if (AS_AVAILABLE_IOS(10)) { + return UIContentSizeCategoryUnspecified; + } else { + return @"_UICTContentSizeCategoryUnspecified"; + } +} + +ASDISPLAYNODE_INLINE UIContentSizeCategory _Nonnull AS_safeContentSizeCategory(UIContentSizeCategory _Nullable sizeCategory) { + return sizeCategory ? sizeCategory : AS_UIContentSizeCategoryUnspecified(); +} + +ASPrimitiveContentSizeCategory ASPrimitiveContentSizeCategoryMake(UIContentSizeCategory sizeCategory) { + if ([sizeCategory isEqualToString:UIContentSizeCategoryExtraSmall]) { + return UIContentSizeCategoryExtraSmall; + } + if ([sizeCategory isEqualToString:UIContentSizeCategorySmall]) { + return UIContentSizeCategorySmall; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryMedium]) { + return UIContentSizeCategoryMedium; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryLarge]) { + return UIContentSizeCategoryLarge; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryExtraLarge]) { + return UIContentSizeCategoryExtraLarge; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { + return UIContentSizeCategoryExtraExtraLarge; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { + return UIContentSizeCategoryExtraExtraExtraLarge; + } + + if ([sizeCategory isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { + return UIContentSizeCategoryAccessibilityMedium; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { + return UIContentSizeCategoryAccessibilityLarge; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { + return UIContentSizeCategoryAccessibilityExtraLarge; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { + return UIContentSizeCategoryAccessibilityExtraExtraLarge; + } + if ([sizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { + return UIContentSizeCategoryAccessibilityExtraExtraExtraLarge; + } + + return AS_UIContentSizeCategoryUnspecified(); +} + #pragma mark - ASPrimitiveTraitCollection extern void ASTraitCollectionPropagateDown(id element, ASPrimitiveTraitCollection traitCollection) { @@ -32,36 +90,59 @@ extern void ASTraitCollectionPropagateDown(id element, ASPrimit } } -ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault() -{ +ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault() { return (ASPrimitiveTraitCollection) { // Default values can be defined in here + .displayGamut = UIDisplayGamutUnspecified, .userInterfaceIdiom = UIUserInterfaceIdiomUnspecified, + .layoutDirection = UITraitEnvironmentLayoutDirectionUnspecified, + .preferredContentSizeCategory = ASPrimitiveContentSizeCategoryMake(AS_UIContentSizeCategoryUnspecified()), .containerSize = CGSizeZero, }; } -ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection *traitCollection) -{ +ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection *traitCollection) { ASPrimitiveTraitCollection environmentTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); - environmentTraitCollection.displayScale = traitCollection.displayScale; environmentTraitCollection.horizontalSizeClass = traitCollection.horizontalSizeClass; environmentTraitCollection.verticalSizeClass = traitCollection.verticalSizeClass; + environmentTraitCollection.displayScale = traitCollection.displayScale; environmentTraitCollection.userInterfaceIdiom = traitCollection.userInterfaceIdiom; - if (AS_AT_LEAST_IOS9) { - environmentTraitCollection.forceTouchCapability = traitCollection.forceTouchCapability; + environmentTraitCollection.forceTouchCapability = traitCollection.forceTouchCapability; + if (AS_AVAILABLE_IOS(10)) { + environmentTraitCollection.displayGamut = traitCollection.displayGamut; + environmentTraitCollection.layoutDirection = traitCollection.layoutDirection; + + // preferredContentSizeCategory is also available on older iOS versions, but only via UIApplication class. + // It should be noted that [UIApplication sharedApplication] is unavailable because Texture is built with only extension-safe API. + environmentTraitCollection.preferredContentSizeCategory = ASPrimitiveContentSizeCategoryMake(traitCollection.preferredContentSizeCategory); + + #if TARGET_OS_TV + environmentTraitCollection.userInterfaceStyle = traitCollection.userInterfaceStyle; + #endif + } else { + environmentTraitCollection.displayGamut = UIDisplayGamutSRGB; // We're on iOS 9 or lower, so this is not a P3 device. } return environmentTraitCollection; } -BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTraitCollection lhs, ASPrimitiveTraitCollection rhs) -{ +BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTraitCollection lhs, ASPrimitiveTraitCollection rhs) { + UIContentSizeCategory leftSizeCategory = AS_safeContentSizeCategory(lhs.preferredContentSizeCategory); + UIContentSizeCategory rightSizeCategory = AS_safeContentSizeCategory(rhs.preferredContentSizeCategory); + return lhs.verticalSizeClass == rhs.verticalSizeClass && lhs.horizontalSizeClass == rhs.horizontalSizeClass && lhs.displayScale == rhs.displayScale && + lhs.displayGamut == rhs.displayGamut && lhs.userInterfaceIdiom == rhs.userInterfaceIdiom && lhs.forceTouchCapability == rhs.forceTouchCapability && + lhs.layoutDirection == rhs.layoutDirection && + #if TARGET_OS_TV + lhs.userInterfaceStyle == rhs.userInterfaceStyle && + #endif + + [leftSizeCategory isEqualToString:rightSizeCategory] && // Simple pointer comparison should be sufficient here + CGSizeEqualToSize(lhs.containerSize, rhs.containerSize); } @@ -105,14 +186,58 @@ BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTr } } -NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection traits) -{ +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIDisplayGamut(UIDisplayGamut displayGamut) { + switch (displayGamut) { + case UIDisplayGamutSRGB: + return @"sRGB"; + case UIDisplayGamutP3: + return @"P3"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUITraitEnvironmentLayoutDirection(UITraitEnvironmentLayoutDirection layoutDirection) { + switch (layoutDirection) { + case UITraitEnvironmentLayoutDirectionLeftToRight: + return @"LeftToRight"; + case UITraitEnvironmentLayoutDirectionRightToLeft: + return @"RightToLeft"; + default: + return @"Unspecified"; + } +} + +#if TARGET_OS_TV +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceStyle(UIUserInterfaceStyle userInterfaceStyle) { + switch (userInterfaceStyle) { + case UIUserInterfaceStyleLight: + return @"Light"; + case UIUserInterfaceStyleDark: + return @"Dark"; + default: + return @"Unspecified"; + } +} +#endif + +NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection traits) { NSMutableArray *props = [NSMutableArray array]; - [props addObject:@{ @"userInterfaceIdiom": AS_NSStringFromUIUserInterfaceIdiom(traits.userInterfaceIdiom) }]; - [props addObject:@{ @"containerSize": NSStringFromCGSize(traits.containerSize) }]; - [props addObject:@{ @"horizontalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.horizontalSizeClass) }]; [props addObject:@{ @"verticalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.verticalSizeClass) }]; + [props addObject:@{ @"horizontalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.horizontalSizeClass) }]; + [props addObject:@{ @"displayScale": [NSString stringWithFormat: @"%.0lf", (double)traits.displayScale] }]; + [props addObject:@{ @"displayGamut": AS_NSStringFromUIDisplayGamut(traits.displayGamut) }]; + [props addObject:@{ @"userInterfaceIdiom": AS_NSStringFromUIUserInterfaceIdiom(traits.userInterfaceIdiom) }]; [props addObject:@{ @"forceTouchCapability": AS_NSStringFromUIForceTouchCapability(traits.forceTouchCapability) }]; + [props addObject:@{ @"layoutDirection": AS_NSStringFromUITraitEnvironmentLayoutDirection(traits.layoutDirection) }]; + #if TARGET_OS_TV + [props addObject:@{ @"userInterfaceStyle": AS_NSStringFromUIUserInterfaceStyle(traits.userInterfaceStyle) }]; + #endif + [props addObject:@{ @"preferredContentSizeCategory": AS_safeContentSizeCategory(traits.preferredContentSizeCategory) }]; + [props addObject:@{ @"containerSize": NSStringFromCGSize(traits.containerSize) }]; return ASObjectDescriptionMakeWithoutObject(props); } @@ -120,86 +245,299 @@ BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTr @implementation ASTraitCollection -- (instancetype)initWithDisplayScale:(CGFloat)displayScale - userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom - horizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass - verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass - forceTouchCapability:(UIForceTouchCapability)forceTouchCapability - containerSize:(CGSize)windowSize +#if TARGET_OS_TV + +- (instancetype)initWithHorizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass + verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass + displayScale:(CGFloat)displayScale + displayGamut:(UIDisplayGamut)displayGamut + userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom + forceTouchCapability:(UIForceTouchCapability)forceTouchCapability + layoutDirection:(UITraitEnvironmentLayoutDirection)layoutDirection + userInterfaceStyle:(UIUserInterfaceStyle)userInterfaceStyle + preferredContentSizeCategory:(UIContentSizeCategory _Nonnull)preferredContentSizeCategory + containerSize:(CGSize)windowSize { self = [super init]; if (self) { - _displayScale = displayScale; - _userInterfaceIdiom = userInterfaceIdiom; _horizontalSizeClass = horizontalSizeClass; _verticalSizeClass = verticalSizeClass; + _displayScale = displayScale; + _displayGamut = displayGamut; + _userInterfaceIdiom = userInterfaceIdiom; _forceTouchCapability = forceTouchCapability; + _layoutDirection = layoutDirection; + _userInterfaceStyle = userInterfaceStyle; + _preferredContentSizeCategory = AS_safeContentSizeCategory(preferredContentSizeCategory); // guard against misuse _containerSize = windowSize; } return self; } -+ (instancetype)traitCollectionWithDisplayScale:(CGFloat)displayScale - userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom - horizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass - verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass - forceTouchCapability:(UIForceTouchCapability)forceTouchCapability - containerSize:(CGSize)windowSize ++ (instancetype)traitCollectionWithHorizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass + verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass + displayScale:(CGFloat)displayScale + displayGamut:(UIDisplayGamut)displayGamut + userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom + forceTouchCapability:(UIForceTouchCapability)forceTouchCapability + layoutDirection:(UITraitEnvironmentLayoutDirection)layoutDirection + userInterfaceStyle:(UIUserInterfaceStyle)userInterfaceStyle + preferredContentSizeCategory:(UIContentSizeCategory _Nonnull)preferredContentSizeCategory + containerSize:(CGSize)windowSize NS_RETURNS_RETAINED +{ + return [[self alloc] initWithHorizontalSizeClass:horizontalSizeClass + verticalSizeClass:verticalSizeClass + displayScale:displayScale + displayGamut:displayGamut + userInterfaceIdiom:userInterfaceIdiom + forceTouchCapability:forceTouchCapability + layoutDirection:layoutDirection + userInterfaceStyle:userInterfaceStyle + preferredContentSizeCategory:preferredContentSizeCategory + containerSize:windowSize]; +} + +#else + +- (instancetype)initWithHorizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass + verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass + displayScale:(CGFloat)displayScale + displayGamut:(UIDisplayGamut)displayGamut + userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom + forceTouchCapability:(UIForceTouchCapability)forceTouchCapability + layoutDirection:(UITraitEnvironmentLayoutDirection)layoutDirection + preferredContentSizeCategory:(UIContentSizeCategory _Nonnull)preferredContentSizeCategory + containerSize:(CGSize)windowSize +{ + self = [super init]; + if (self) { + _horizontalSizeClass = horizontalSizeClass; + _verticalSizeClass = verticalSizeClass; + _displayScale = displayScale; + _displayGamut = displayGamut; + _userInterfaceIdiom = userInterfaceIdiom; + _forceTouchCapability = forceTouchCapability; + _layoutDirection = layoutDirection; + _preferredContentSizeCategory = AS_safeContentSizeCategory(preferredContentSizeCategory); // guard against misuse + _containerSize = windowSize; + } + return self; +} + ++ (instancetype)traitCollectionWithHorizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass + verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass + displayScale:(CGFloat)displayScale + displayGamut:(UIDisplayGamut)displayGamut + userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom + forceTouchCapability:(UIForceTouchCapability)forceTouchCapability + layoutDirection:(UITraitEnvironmentLayoutDirection)layoutDirection + preferredContentSizeCategory:(UIContentSizeCategory _Nonnull)preferredContentSizeCategory + containerSize:(CGSize)windowSize NS_RETURNS_RETAINED { - return [[self alloc] initWithDisplayScale:displayScale - userInterfaceIdiom:userInterfaceIdiom - horizontalSizeClass:horizontalSizeClass - verticalSizeClass:verticalSizeClass - forceTouchCapability:forceTouchCapability - containerSize:windowSize]; + return [[self alloc] initWithHorizontalSizeClass:horizontalSizeClass + verticalSizeClass:verticalSizeClass + displayScale:displayScale + displayGamut:displayGamut + userInterfaceIdiom:userInterfaceIdiom + forceTouchCapability:forceTouchCapability + layoutDirection:layoutDirection + preferredContentSizeCategory:preferredContentSizeCategory + containerSize:windowSize]; } -+ (instancetype)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits +#endif + ++ (instancetype)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection + containerSize:(CGSize)windowSize NS_RETURNS_RETAINED { - return [self traitCollectionWithDisplayScale:traits.displayScale - userInterfaceIdiom:traits.userInterfaceIdiom - horizontalSizeClass:traits.horizontalSizeClass - verticalSizeClass:traits.verticalSizeClass - forceTouchCapability:traits.forceTouchCapability - containerSize:traits.containerSize]; + return [self traitCollectionWithUITraitCollection:traitCollection + containerSize:windowSize + fallbackContentSizeCategory:AS_UIContentSizeCategoryUnspecified()]; } + + (instancetype)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection - containerSize:(CGSize)windowSize + containerSize:(CGSize)windowSize + fallbackContentSizeCategory:(UIContentSizeCategory _Nonnull)fallbackContentSizeCategory NS_RETURNS_RETAINED +{ + UIDisplayGamut displayGamut; + UITraitEnvironmentLayoutDirection layoutDirection; + UIContentSizeCategory sizeCategory; + #if TARGET_OS_TV + UIUserInterfaceStyle userInterfaceStyle; + #endif + if (AS_AVAILABLE_IOS(10)) { + displayGamut = traitCollection.displayGamut; + layoutDirection = traitCollection.layoutDirection; + sizeCategory = traitCollection.preferredContentSizeCategory; + #if TARGET_OS_TV + userInterfaceStyle = traitCollection.userInterfaceStyle; + #endif + } else { + displayGamut = UIDisplayGamutSRGB; // We're on iOS 9 or lower, so this is not a P3 device. + layoutDirection = UITraitEnvironmentLayoutDirectionUnspecified; + sizeCategory = fallbackContentSizeCategory; + #if TARGET_OS_TV + userInterfaceStyle = UIUserInterfaceStyleUnspecified; + #endif + } + +#if TARGET_OS_TV + return [self traitCollectionWithHorizontalSizeClass:traitCollection.horizontalSizeClass + verticalSizeClass:traitCollection.verticalSizeClass + displayScale:traitCollection.displayScale + displayGamut:displayGamut + userInterfaceIdiom:traitCollection.userInterfaceIdiom + forceTouchCapability:traitCollection.forceTouchCapability + layoutDirection:layoutDirection + userInterfaceStyle:userInterfaceStyle + preferredContentSizeCategory:sizeCategory + containerSize:windowSize]; +#else + return [self traitCollectionWithHorizontalSizeClass:traitCollection.horizontalSizeClass + verticalSizeClass:traitCollection.verticalSizeClass + displayScale:traitCollection.displayScale + displayGamut:displayGamut + userInterfaceIdiom:traitCollection.userInterfaceIdiom + forceTouchCapability:traitCollection.forceTouchCapability + layoutDirection:layoutDirection + preferredContentSizeCategory:sizeCategory + containerSize:windowSize]; +#endif +} + +- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection { - UIForceTouchCapability forceTouch = AS_AT_LEAST_IOS9 ? traitCollection.forceTouchCapability : UIForceTouchCapabilityUnknown; - return [self traitCollectionWithDisplayScale:traitCollection.displayScale - userInterfaceIdiom:traitCollection.userInterfaceIdiom - horizontalSizeClass:traitCollection.horizontalSizeClass - verticalSizeClass:traitCollection.verticalSizeClass - forceTouchCapability:forceTouch - containerSize:windowSize]; + if (traitCollection == nil) { + return NO; + } + + if (self == traitCollection) { + return YES; + } + + return + self.horizontalSizeClass == traitCollection.horizontalSizeClass && + self.verticalSizeClass == traitCollection.verticalSizeClass && + self.displayScale == traitCollection.displayScale && + self.displayGamut == traitCollection.displayGamut && + self.userInterfaceIdiom == traitCollection.userInterfaceIdiom && + self.forceTouchCapability == traitCollection.forceTouchCapability && + self.layoutDirection == traitCollection.layoutDirection && + #if TARGET_OS_TV + self.userInterfaceStyle == traitCollection.userInterfaceStyle && + #endif + [self.preferredContentSizeCategory isEqualToString:traitCollection.preferredContentSizeCategory] && + CGSizeEqualToSize(self.containerSize, traitCollection.containerSize); +} + +@end + +@implementation ASTraitCollection (PrimitiveTraits) + ++ (instancetype)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits NS_RETURNS_RETAINED +{ +#if TARGET_OS_TV + return [self traitCollectionWithHorizontalSizeClass:traits.horizontalSizeClass + verticalSizeClass:traits.verticalSizeClass + displayScale:traits.displayScale + displayGamut:traits.displayGamut + userInterfaceIdiom:traits.userInterfaceIdiom + forceTouchCapability:traits.forceTouchCapability + layoutDirection:traits.layoutDirection + userInterfaceStyle:traits.userInterfaceStyle + preferredContentSizeCategory:AS_safeContentSizeCategory(traits.preferredContentSizeCategory) + containerSize:traits.containerSize]; +#else + return [self traitCollectionWithHorizontalSizeClass:traits.horizontalSizeClass + verticalSizeClass:traits.verticalSizeClass + displayScale:traits.displayScale + displayGamut:traits.displayGamut + userInterfaceIdiom:traits.userInterfaceIdiom + forceTouchCapability:traits.forceTouchCapability + layoutDirection:traits.layoutDirection + preferredContentSizeCategory:AS_safeContentSizeCategory(traits.preferredContentSizeCategory) + containerSize:traits.containerSize]; +#endif } - (ASPrimitiveTraitCollection)primitiveTraitCollection { return (ASPrimitiveTraitCollection) { - .displayScale = self.displayScale, .horizontalSizeClass = self.horizontalSizeClass, - .userInterfaceIdiom = self.userInterfaceIdiom, .verticalSizeClass = self.verticalSizeClass, + .displayScale = self.displayScale, + .displayGamut = self.displayGamut, + .userInterfaceIdiom = self.userInterfaceIdiom, .forceTouchCapability = self.forceTouchCapability, + .layoutDirection = self.layoutDirection, +#if TARGET_OS_TV + .userInterfaceStyle = self.userInterfaceStyle, +#endif + .preferredContentSizeCategory = ASPrimitiveContentSizeCategoryMake(self.preferredContentSizeCategory), .containerSize = self.containerSize, }; } -- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection +@end + +@implementation ASTraitCollection (Deprecated) + +- (instancetype)init { - if (self == traitCollection) { - return YES; - } +#if TARGET_OS_TV + return [self initWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified + verticalSizeClass:UIUserInterfaceSizeClassUnspecified + displayScale:0 + displayGamut:UIDisplayGamutUnspecified + userInterfaceIdiom:UIUserInterfaceIdiomUnspecified + forceTouchCapability:UIForceTouchCapabilityUnknown + layoutDirection:UITraitEnvironmentLayoutDirectionUnspecified + userInterfaceStyle:UIUserInterfaceStyleUnspecified + preferredContentSizeCategory:AS_UIContentSizeCategoryUnspecified() + containerSize:CGSizeZero]; +#else + return [self initWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified + verticalSizeClass:UIUserInterfaceSizeClassUnspecified + displayScale:0 + displayGamut:UIDisplayGamutUnspecified + userInterfaceIdiom:UIUserInterfaceIdiomUnspecified + forceTouchCapability:UIForceTouchCapabilityUnknown + layoutDirection:UITraitEnvironmentLayoutDirectionUnspecified + preferredContentSizeCategory:AS_UIContentSizeCategoryUnspecified() + containerSize:CGSizeZero]; +#endif +} - return self.displayScale == traitCollection.displayScale && - self.horizontalSizeClass == traitCollection.horizontalSizeClass && - self.verticalSizeClass == traitCollection.verticalSizeClass && - self.userInterfaceIdiom == traitCollection.userInterfaceIdiom && - CGSizeEqualToSize(self.containerSize, traitCollection.containerSize) && - self.forceTouchCapability == traitCollection.forceTouchCapability; ++ (ASTraitCollection *)traitCollectionWithDisplayScale:(CGFloat)displayScale + userInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom + horizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass + verticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass + forceTouchCapability:(UIForceTouchCapability)forceTouchCapability + containerSize:(CGSize)windowSize NS_RETURNS_RETAINED +{ +#if TARGET_OS_TV + return [self traitCollectionWithHorizontalSizeClass:horizontalSizeClass + verticalSizeClass:verticalSizeClass + displayScale:displayScale + displayGamut:UIDisplayGamutUnspecified + userInterfaceIdiom:userInterfaceIdiom + forceTouchCapability:forceTouchCapability + layoutDirection:UITraitEnvironmentLayoutDirectionUnspecified + userInterfaceStyle:UIUserInterfaceStyleUnspecified + preferredContentSizeCategory:AS_UIContentSizeCategoryUnspecified() + containerSize:windowSize]; +#else + return [self traitCollectionWithHorizontalSizeClass:horizontalSizeClass + verticalSizeClass:verticalSizeClass + displayScale:displayScale + displayGamut:UIDisplayGamutUnspecified + userInterfaceIdiom:userInterfaceIdiom + forceTouchCapability:forceTouchCapability + layoutDirection:UITraitEnvironmentLayoutDirectionUnspecified + preferredContentSizeCategory:AS_UIContentSizeCategoryUnspecified() + containerSize:windowSize]; +#endif } @end diff --git a/Source/Details/ASWeakProxy.h b/Source/Details/ASWeakProxy.h index 978204fbf..4a2490513 100644 --- a/Source/Details/ASWeakProxy.h +++ b/Source/Details/ASWeakProxy.h @@ -35,6 +35,6 @@ AS_SUBCLASSING_RESTRICTED * * @return an instance of ASWeakProxy */ -+ (instancetype)weakProxyWithTarget:(id)target; ++ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED; @end diff --git a/Source/Details/ASWeakProxy.m b/Source/Details/ASWeakProxy.m index 9db965541..721191bc0 100644 --- a/Source/Details/ASWeakProxy.m +++ b/Source/Details/ASWeakProxy.m @@ -29,7 +29,7 @@ - (instancetype)initWithTarget:(id)target return self; } -+ (instancetype)weakProxyWithTarget:(id)target ++ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED { return [[ASWeakProxy alloc] initWithTarget:target]; } diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 109bf7812..8c7687952 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -71,10 +71,13 @@ 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; +@property (nonatomic, assign) UIEdgeInsets layoutMargins; +@property (nonatomic, assign) BOOL preservesSuperviewLayoutMargins; +@property (nonatomic, assign) BOOL insetsLayoutMarginsFromSafeArea; /** Following properties of the UIAccessibility informal protocol are supported as well. diff --git a/Source/Details/_ASDisplayLayer.h b/Source/Details/_ASDisplayLayer.h index 11ed69578..97c1e9489 100644 --- a/Source/Details/_ASDisplayLayer.h +++ b/Source/Details/_ASDisplayLayer.h @@ -19,6 +19,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @class ASDisplayNode; @protocol _ASDisplayLayerDelegate; @@ -28,7 +30,7 @@ @discussion This property overrides the CALayer category method which implements this via associated objects. This should result in much better performance for _ASDisplayLayers. */ -@property (nonatomic, weak) ASDisplayNode *asyncdisplaykit_node; +@property (nullable, nonatomic, weak) ASDisplayNode *asyncdisplaykit_node; /** @summary Set to YES to enable asynchronous display for the receiver. @@ -57,7 +59,7 @@ @desc The asyncDelegate will have the opportunity to override the methods related to async display. */ -@property (atomic, weak) id<_ASDisplayLayerDelegate> asyncDelegate; +@property (nullable, atomic, weak) id<_ASDisplayLayerDelegate> asyncDelegate; /** @summary Suspends both asynchronous and synchronous display of the receiver if YES. @@ -109,7 +111,10 @@ @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. @param isRasterizing YES if the layer is being rasterized into another layer, in which case drawRect: probably wants to avoid doing things like filling its bounds with a zero-alpha color to clear the backing store. */ -+ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; ++ (void)drawRect:(CGRect)bounds + withParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock + isRasterizing:(BOOL)isRasterizing; /** @summary Delegate override to provide new layer contents as a UIImage. @@ -117,7 +122,8 @@ @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. @return A UIImage with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here. */ -+ (UIImage *)displayWithParameters:(id)parameters isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock; ++ (UIImage *)displayWithParameters:(nullable id)parameters + isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock; // Called on the main thread only @@ -147,3 +153,5 @@ - (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer; @end + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/_ASDisplayLayer.mm b/Source/Details/_ASDisplayLayer.mm index 7ceba1c79..ba8b02ab4 100644 --- a/Source/Details/_ASDisplayLayer.mm +++ b/Source/Details/_ASDisplayLayer.mm @@ -25,6 +25,7 @@ #import #import #import +#import @implementation _ASDisplayLayer { @@ -93,6 +94,7 @@ - (void)setContents:(id)contents - (void)setNeedsLayout { ASDisplayNodeAssertMainThread(); + as_log_verbose(ASNodeLog(), "%s on %@", sel_getName(_cmd), self); [super setNeedsLayout]; } #endif diff --git a/Source/Details/_ASDisplayView.h b/Source/Details/_ASDisplayView.h index c4968d183..36f3a00d6 100644 --- a/Source/Details/_ASDisplayView.h +++ b/Source/Details/_ASDisplayView.h @@ -17,11 +17,21 @@ #import +NS_ASSUME_NONNULL_BEGIN + // This class is only for use by ASDisplayNode and should never be subclassed or used directly. // Note that the "node" property is added to UIView directly via a category in ASDisplayNode. +@class ASDisplayNode; + @interface _ASDisplayView : UIView +/** + @discussion This property overrides the UIView category method which implements this via associated objects. + This should result in much better performance for _ASDisplayView. + */ +@property (nullable, nonatomic, weak) ASDisplayNode *asyncdisplaykit_node; + // These methods expose a way for ASDisplayNode touch events to let the view call super touch events // Some UIKit mechanisms, like UITableView and UICollectionView selection handling, require this to work - (void)__forwardTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; @@ -29,4 +39,14 @@ - (void)__forwardTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)__forwardTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; +// These methods expose a way for ASDisplayNode responder methods to let the view call super responder methods +// They are called from ASDisplayNode to pass through UIResponder methods to the view +- (BOOL)__canBecomeFirstResponder; +- (BOOL)__becomeFirstResponder; +- (BOOL)__canResignFirstResponder; +- (BOOL)__resignFirstResponder; +- (BOOL)__isFirstResponder; + @end + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 93f3187f6..9315fa7af 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -21,13 +21,59 @@ #import #import #import +#import #import #import -#import +#import #import +#import +#import + +#pragma mark - _ASDisplayViewMethodOverrides + +typedef NS_OPTIONS(NSUInteger, _ASDisplayViewMethodOverrides) +{ + _ASDisplayViewMethodOverrideNone = 0, + _ASDisplayViewMethodOverrideCanBecomeFirstResponder = 1 << 0, + _ASDisplayViewMethodOverrideBecomeFirstResponder = 1 << 1, + _ASDisplayViewMethodOverrideCanResignFirstResponder = 1 << 2, + _ASDisplayViewMethodOverrideResignFirstResponder = 1 << 3, + _ASDisplayViewMethodOverrideIsFirstResponder = 1 << 4, +}; + +/** + * Returns _ASDisplayViewMethodOverrides for the given class + * + * @param c the class, required. + * + * @return _ASDisplayViewMethodOverrides. + */ +static _ASDisplayViewMethodOverrides GetASDisplayViewMethodOverrides(Class c) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + _ASDisplayViewMethodOverrides overrides = _ASDisplayViewMethodOverrideNone; + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canBecomeFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideCanBecomeFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(becomeFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideBecomeFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canResignFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideCanResignFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(resignFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideResignFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(isFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideIsFirstResponder; + } + return overrides; +} + +#pragma mark - _ASDisplayView @interface _ASDisplayView () -@property (nullable, atomic, weak, readwrite) ASDisplayNode *asyncdisplaykit_node; // Keep the node alive while its view is active. If you create a view, add its layer to a layer hierarchy, then release // the view, the layer retains the view to prevent a crash. This replicates this behaviour for the node abstraction. @@ -41,6 +87,21 @@ @implementation _ASDisplayView NSArray *_accessibleElements; CGRect _lastAccessibleElementsFrame; + + _ASDisplayViewMethodOverrides _methodOverrides; +} + +#pragma mark - Class + ++ (void)initialize +{ + __unused Class initializeSelf = self; + IMP staticInitialize = imp_implementationWithBlock(^(_ASDisplayView *view) { + ASDisplayNodeAssert(view.class == initializeSelf, @"View class %@ does not have a matching _staticInitialize method; check to ensure [super initialize] is called within any custom +initialize implementations! Overridden methods will not be called unless they are also implemented by superclass %@", view.class, initializeSelf); + view->_methodOverrides = GetASDisplayViewMethodOverrides(view.class); + }); + + class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); } + (Class)layerClass @@ -50,6 +111,26 @@ + (Class)layerClass #pragma mark - NSObject Overrides +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + [self _initializeInstance]; + + return self; +} + +- (void)_initializeInstance +{ + [self _staticInitialize]; +} + +- (void)_staticInitialize +{ + ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden"); +} + // e.g. ; frame = ...> - (NSString *)description { @@ -155,6 +236,16 @@ - (void)didMoveToSuperview self.keepalive_node = nil; } +#if DEBUG + // This is only to help detect issues when a root-of-view-controller node is reused separately from its view controller. + // Avoid overhead in release. + if (superview && node.viewControllerRoot) { + UIViewController *vc = [node closestViewController]; + + ASDisplayNodeAssert(vc != nil && [vc isKindOfClass:[ASViewController class]] && ((ASViewController*)vc).node == node, @"This node was once used as a view controller's node. You should not reuse it without its view controller."); + } +#endif + ASDisplayNode *supernode = node.supernode; ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for superview's node to be layer-backed."); @@ -345,13 +436,11 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event } } -#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 { @@ -361,21 +450,63 @@ - (void)tintColorDidChange [node tintColorDidChange]; } -- (BOOL)canBecomeFirstResponder { +#pragma mark UIResponder Handling + +#define IMPLEMENT_RESPONDER_METHOD(__sel, __methodOverride) \ +- (BOOL)__sel\ +{\ + ASDisplayNode *node = _asyncdisplaykit_node; /* Create strong reference to weak ivar. */ \ + SEL sel = @selector(__sel); \ + /* Prevent an infinite loop in here if [super canBecomeFirstResponder] was called on a + / _ASDisplayView subclass */ \ + if (self->_methodOverrides & __methodOverride) { \ + /* Check if we can call through to ASDisplayNode subclass directly */ \ + if (ASDisplayNodeSubclassOverridesSelector([node class], sel)) { \ + return [node __sel]; \ + } else { \ + /* Call through to views superclass as we expect super was called from the + _ASDisplayView subclass and a node subclass does not overwrite canBecomeFirstResponder */ \ + return [self __##__sel]; \ + } \ + } else { \ + /* Call through to internal node __canBecomeFirstResponder that will consider the view in responding */ \ + return [node __##__sel]; \ + } \ +}\ +/* All __ prefixed methods are called from ASDisplayNode to let the view decide in what UIResponder state they \ +are not overridden by a ASDisplayNode subclass */ \ +- (BOOL)__##__sel \ +{ \ + return [super __sel]; \ +} \ + +IMPLEMENT_RESPONDER_METHOD(canBecomeFirstResponder, _ASDisplayViewMethodOverrideCanBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(becomeFirstResponder, _ASDisplayViewMethodOverrideBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(canResignFirstResponder, _ASDisplayViewMethodOverrideCanResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(resignFirstResponder, _ASDisplayViewMethodOverrideResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(isFirstResponder, _ASDisplayViewMethodOverrideIsFirstResponder); + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + // We forward responder-chain actions to our node if we can't handle them ourselves. See -targetForAction:withSender:. ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - return [node canBecomeFirstResponder]; + return ([super canPerformAction:action withSender:sender] || [node respondsToSelector:action]); } -- (BOOL)canResignFirstResponder { +- (void)layoutMarginsDidChange +{ ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - return [node canResignFirstResponder]; + [super layoutMarginsDidChange]; + + [node layoutMarginsDidChange]; } -- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +- (void)safeAreaInsetsDidChange { - // We forward responder-chain actions to our node if we can't handle them ourselves. See -targetForAction:withSender:. ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - return ([super canPerformAction:action withSender:sender] || [node respondsToSelector:action]); + [super safeAreaInsetsDidChange]; + + [node safeAreaInsetsDidChange]; } - (id)forwardingTargetForSelector:(SEL)aSelector diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 419901131..6b34c89a2 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -84,11 +84,13 @@ + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)containe accessibilityElement.accessibilityHint = node.accessibilityHint; accessibilityElement.accessibilityValue = node.accessibilityValue; accessibilityElement.accessibilityTraits = node.accessibilityTraits; - if (@available(iOS 11, *)) { - [accessibilityElement setValue:node.accessibilityAttributedLabel forKey:@"accessibilityAttributedLabel"]; - [accessibilityElement setValue:node.accessibilityAttributedHint forKey:@"accessibilityAttributedHint"]; - [accessibilityElement setValue:node.accessibilityAttributedValue forKey:@"accessibilityAttributedValue"]; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue; } +#endif return accessibilityElement; } @@ -176,7 +178,8 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, _ SortAccessibilityElements(labeledNodes); - if (AS_AT_LEAST_IOS11) { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { @@ -185,8 +188,10 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, _ } [attributedLabel appendAttributedString:(NSAttributedString *)obj]; }]; - [accessiblityElement setValue:attributedLabel forKey:@"accessibilityAttributedLabel"]; - } else { + accessiblityElement.accessibilityAttributedLabel = attributedLabel; + } else +#endif + { NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; } diff --git a/Source/Layout/ASAbsoluteLayoutSpec.h b/Source/Layout/ASAbsoluteLayoutSpec.h index 0c250ec68..1080980c9 100644 --- a/Source/Layout/ASAbsoluteLayoutSpec.h +++ b/Source/Layout/ASAbsoluteLayoutSpec.h @@ -41,12 +41,12 @@ NS_ASSUME_NONNULL_BEGIN @param sizing How much space the spec will take up @param children Children to be positioned at fixed positions */ -+ (instancetype)absoluteLayoutSpecWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray> *)children AS_WARN_UNUSED_RESULT; ++ (instancetype)absoluteLayoutSpecWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** @param children Children to be positioned at fixed positions */ -+ (instancetype)absoluteLayoutSpecWithChildren:(NSArray> *)children AS_WARN_UNUSED_RESULT; ++ (instancetype)absoluteLayoutSpecWithChildren:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASAbsoluteLayoutSpec.mm b/Source/Layout/ASAbsoluteLayoutSpec.mm index 96ab7a705..eede9ab76 100644 --- a/Source/Layout/ASAbsoluteLayoutSpec.mm +++ b/Source/Layout/ASAbsoluteLayoutSpec.mm @@ -28,12 +28,12 @@ @implementation ASAbsoluteLayoutSpec #pragma mark - Class -+ (instancetype)absoluteLayoutSpecWithChildren:(NSArray *)children ++ (instancetype)absoluteLayoutSpecWithChildren:(NSArray *)children NS_RETURNS_RETAINED { return [[self alloc] initWithChildren:children]; } -+ (instancetype)absoluteLayoutSpecWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray> *)children ++ (instancetype)absoluteLayoutSpecWithSizing:(ASAbsoluteLayoutSpecSizing)sizing children:(NSArray> *)children NS_RETURNS_RETAINED { return [[self alloc] initWithSizing:sizing children:children]; } diff --git a/Source/Layout/ASAsciiArtBoxCreator.m b/Source/Layout/ASAsciiArtBoxCreator.m index 293127a64..e018978bd 100644 --- a/Source/Layout/ASAsciiArtBoxCreator.m +++ b/Source/Layout/ASAsciiArtBoxCreator.m @@ -35,7 +35,7 @@ @interface NSString(PIDebugBox) @implementation NSString(PIDebugBox) -+ (instancetype)debugbox_stringWithString:(NSString *)stringToRepeat repeatedCount:(NSUInteger)repeatCount ++ (instancetype)debugbox_stringWithString:(NSString *)stringToRepeat repeatedCount:(NSUInteger)repeatCount NS_RETURNS_RETAINED { NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[stringToRepeat length] * repeatCount]; for (NSUInteger index = 0; index < repeatCount; index++) { diff --git a/Source/Layout/ASBackgroundLayoutSpec.h b/Source/Layout/ASBackgroundLayoutSpec.h index 882084d05..616d68fea 100644 --- a/Source/Layout/ASBackgroundLayoutSpec.h +++ b/Source/Layout/ASBackgroundLayoutSpec.h @@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN * @param child A child that is laid out to determine the size of this spec. * @param background A layoutElement object that is laid out behind the child. */ -+ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background AS_WARN_UNUSED_RESULT; ++ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASBackgroundLayoutSpec.mm b/Source/Layout/ASBackgroundLayoutSpec.mm index e8c50901d..1cdf270a7 100644 --- a/Source/Layout/ASBackgroundLayoutSpec.mm +++ b/Source/Layout/ASBackgroundLayoutSpec.mm @@ -28,7 +28,7 @@ @implementation ASBackgroundLayoutSpec #pragma mark - Class -+ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background; ++ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background NS_RETURNS_RETAINED { return [[self alloc] initWithChild:child background:background]; } diff --git a/Source/Layout/ASCenterLayoutSpec.h b/Source/Layout/ASCenterLayoutSpec.h index f51a44b3a..825b375fb 100644 --- a/Source/Layout/ASCenterLayoutSpec.h +++ b/Source/Layout/ASCenterLayoutSpec.h @@ -71,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)centerLayoutSpecWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions - child:(id)child AS_WARN_UNUSED_RESULT; + child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASCenterLayoutSpec.mm b/Source/Layout/ASCenterLayoutSpec.mm index 0017a2a8e..230768faa 100644 --- a/Source/Layout/ASCenterLayoutSpec.mm +++ b/Source/Layout/ASCenterLayoutSpec.mm @@ -42,7 +42,7 @@ - (instancetype)initWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)cen + (instancetype)centerLayoutSpecWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions - child:(id)child + child:(id)child NS_RETURNS_RETAINED { return [[self alloc] initWithCenteringOptions:centeringOptions sizingOptions:sizingOptions child:child]; } diff --git a/Source/Layout/ASCornerLayoutSpec.h b/Source/Layout/ASCornerLayoutSpec.h new file mode 100644 index 000000000..cb5406611 --- /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 NS_RETURNS_RETAINED 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 000000000..d2b88c70b --- /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 NS_RETURNS_RETAINED +{ + 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/ASInsetLayoutSpec.h b/Source/Layout/ASInsetLayoutSpec.h index f12e54ff1..61b37b5e6 100644 --- a/Source/Layout/ASInsetLayoutSpec.h +++ b/Source/Layout/ASInsetLayoutSpec.h @@ -44,7 +44,7 @@ NS_ASSUME_NONNULL_BEGIN @param insets The amount of space to inset on each side. @param child The wrapped child to inset. */ -+ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child AS_WARN_UNUSED_RESULT; ++ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASInsetLayoutSpec.mm b/Source/Layout/ASInsetLayoutSpec.mm index 5db1cf03e..23e0feace 100644 --- a/Source/Layout/ASInsetLayoutSpec.mm +++ b/Source/Layout/ASInsetLayoutSpec.mm @@ -59,7 +59,7 @@ - (instancetype)initWithInsets:(UIEdgeInsets)insets child:(id)c return self; } -+ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child ++ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child NS_RETURNS_RETAINED { return [[self alloc] initWithInsets:insets child:child]; } diff --git a/Source/Layout/ASLayout.h b/Source/Layout/ASLayout.h index fa1371985..81c88ddc0 100644 --- a/Source/Layout/ASLayout.h +++ b/Source/Layout/ASLayout.h @@ -108,7 +108,7 @@ ASDISPLAYNODE_EXTERN_C_END + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size position:(CGPoint)position - sublayouts:(nullable NSArray *)sublayouts AS_WARN_UNUSED_RESULT; + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** * Convenience initializer that has CGPointNull position. @@ -122,7 +122,7 @@ ASDISPLAYNODE_EXTERN_C_END */ + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size - sublayouts:(nullable NSArray *)sublayouts AS_WARN_UNUSED_RESULT; + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** * Convenience that has CGPointNull position and no sublayouts. @@ -133,11 +133,11 @@ ASDISPLAYNODE_EXTERN_C_END * @param size The size of this layout. */ + (instancetype)layoutWithLayoutElement:(id)layoutElement - size:(CGSize)size AS_WARN_UNUSED_RESULT; + size:(CGSize)size NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** * Traverses the existing layout tree and generates a new tree that represents only ASDisplayNode layouts */ -- (ASLayout *)filteredNodeLayoutTree AS_WARN_UNUSED_RESULT; +- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 4e8d2f502..1a1b419f4 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 @@ @interface ASLayout () */ @property (nonatomic, strong) NSMutableArray> *sublayoutLayoutElements; -@property (nonatomic, strong, readonly) ASRectTable, id> *elementToRectTable; +@property (nonatomic, strong, readonly) ASRectMap *elementToRectMap; @end @@ -142,9 +143,9 @@ - (instancetype)initWithLayoutElement:(id)layoutElement _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]; } } @@ -165,7 +166,7 @@ - (instancetype)init + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size position:(CGPoint)position - sublayouts:(nullable NSArray *)sublayouts + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED { return [[self alloc] initWithLayoutElement:layoutElement size:size @@ -175,7 +176,7 @@ + (instancetype)layoutWithLayoutElement:(id)layoutElement + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size - sublayouts:(nullable NSArray *)sublayouts + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED { return [self layoutWithLayoutElement:layoutElement size:size @@ -183,7 +184,7 @@ + (instancetype)layoutWithLayoutElement:(id)layoutElement sublayouts:sublayouts]; } -+ (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size ++ (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size NS_RETURNS_RETAINED { return [self layoutWithLayoutElement:layoutElement size:size @@ -215,7 +216,7 @@ - (void)setRetainSublayoutLayoutElements:(BOOL)retainSublayoutLayoutElements #pragma mark - Layout Flattening -- (ASLayout *)filteredNodeLayoutTree +- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED { if (ASLayoutIsFlattened(self)) { // All flattened layouts must have this flag enabled @@ -281,11 +282,12 @@ - (BOOL)isEqual:(id)object } if (!CGSizeEqualToSize(_size, layout.size)) return NO; - if (!CGPointEqualToPoint(_position, layout.position)) return NO; + + if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position)) + || CGPointEqualToPoint(self.position, layout.position))) return NO; if (_layoutElement != layout.layoutElement) return NO; - NSArray *sublayouts = layout.sublayouts; - if (sublayouts != _sublayouts && (sublayouts == nil || _sublayouts == nil || ![_sublayouts isEqual:sublayouts])) { + if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) { return NO; } @@ -301,7 +303,7 @@ - (ASLayoutElementType)type - (CGRect)frameForElement:(id)layoutElement { - return _elementToRectTable ? [_elementToRectTable rectForKey:layoutElement] : CGRectNull; + return _elementToRectMap ? [_elementToRectMap rectForKey:layoutElement] : CGRectNull; } - (CGRect)frame diff --git a/Source/Layout/ASLayoutElement.h b/Source/Layout/ASLayoutElement.h index ca56c6722..01af4353a 100644 --- a/Source/Layout/ASLayoutElement.h +++ b/Source/Layout/ASLayoutElement.h @@ -174,7 +174,7 @@ extern NSString * const ASLayoutElementStyleLayoutPositionProperty; - (void)style:(__kindof ASLayoutElementStyle *)style propertyDidChange:(NSString *)propertyName; @end -@interface ASLayoutElementStyle : NSObject +@interface ASLayoutElementStyle : NSObject /** * @abstract Initializes the layoutElement style with a specified delegate @@ -192,7 +192,7 @@ extern NSString * const ASLayoutElementStyleLayoutPositionProperty; #pragma mark - Sizing /** - * @abstract The width property specifies the height of the content area of an ASLayoutElement. + * @abstract The width property specifies the width of the content area of an ASLayoutElement. * The minWidth and maxWidth properties override width. * Defaults to ASDimensionAuto */ diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index 9fc52b3bf..8baba5326 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -50,38 +50,27 @@ - (instancetype)init int32_t const ASLayoutElementContextInvalidTransitionID = 0; int32_t const ASLayoutElementContextDefaultTransitionID = ASLayoutElementContextInvalidTransitionID + 1; -static void ASLayoutElementDestructor(void *p) { - if (p != NULL) { - ASDisplayNodeCFailAssert(@"Thread exited without clearing layout element context!"); - CFBridgingRelease(p); - } -}; - -pthread_key_t ASLayoutElementContextKey() -{ - return ASPthreadStaticKey(ASLayoutElementDestructor); -} +static _Thread_local __unsafe_unretained ASLayoutElementContext *tls_context; void ASLayoutElementPushContext(ASLayoutElementContext *context) { // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. - ASDisplayNodeCAssertNil(ASLayoutElementGetCurrentContext(), @"Nested ASLayoutElementContexts aren't supported."); - pthread_setspecific(ASLayoutElementContextKey(), CFBridgingRetain(context)); + ASDisplayNodeCAssertNil(tls_context, @"Nested ASLayoutElementContexts aren't supported."); + + tls_context = (__bridge ASLayoutElementContext *)(__bridge_retained CFTypeRef)context; } ASLayoutElementContext *ASLayoutElementGetCurrentContext() { // Don't retain here. Caller will retain if it wants to! - return (__bridge __unsafe_unretained ASLayoutElementContext *)pthread_getspecific(ASLayoutElementContextKey()); + return tls_context; } void ASLayoutElementPopContext() { - ASLayoutElementContextKey(); - ASDisplayNodeCAssertNotNil(ASLayoutElementGetCurrentContext(), @"Attempt to pop context when there wasn't a context!"); - auto key = ASLayoutElementContextKey(); - CFBridgingRelease(pthread_getspecific(key)); - pthread_setspecific(key, NULL); + ASDisplayNodeCAssertNotNil(tls_context, @"Attempt to pop context when there wasn't a context!"); + CFRelease((__bridge CFTypeRef)tls_context); + tls_context = nil; } #pragma mark - ASLayoutElementStyle @@ -187,6 +176,18 @@ - (instancetype)init return self; } +#pragma mark - NSLocking + +- (void)lock +{ + __instanceLock__.lock(); +} + +- (void)unlock +{ + __instanceLock__.unlock(); +} + #pragma mark - ASLayoutElementStyleSize - (ASLayoutElementSize)size diff --git a/Source/Layout/ASLayoutElementPrivate.h b/Source/Layout/ASLayoutElementPrivate.h index 9bc3101b0..96c520e5d 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 diff --git a/Source/Layout/ASLayoutSpec.h b/Source/Layout/ASLayoutSpec.h index ac02bacd0..181ea70b2 100644 --- a/Source/Layout/ASLayoutSpec.h +++ b/Source/Layout/ASLayoutSpec.h @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN /** * A layout spec is an immutable object that describes a layout, loosely inspired by React. */ -@interface ASLayoutSpec : NSObject +@interface ASLayoutSpec : NSObject /** * Creation of a layout spec should only happen by a user in layoutSpecThatFits:. During that method, a @@ -71,12 +71,12 @@ NS_ASSUME_NONNULL_BEGIN /* * Returns an ASWrapperLayoutSpec object with the given layoutElement as child. */ -+ (instancetype)wrapperWithLayoutElement:(id)layoutElement AS_WARN_UNUSED_RESULT; ++ (instancetype)wrapperWithLayoutElement:(id)layoutElement NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /* * Returns an ASWrapperLayoutSpec object with the given layoutElements as children. */ -+ (instancetype)wrapperWithLayoutElements:(NSArray> *)layoutElements AS_WARN_UNUSED_RESULT; ++ (instancetype)wrapperWithLayoutElements:(NSArray> *)layoutElements NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /* * Returns an ASWrapperLayoutSpec object initialized with the given layoutElement as child. diff --git a/Source/Layout/ASLayoutSpec.mm b/Source/Layout/ASLayoutSpec.mm index 76901d1ac..ff45add44 100644 --- a/Source/Layout/ASLayoutSpec.mm +++ b/Source/Layout/ASLayoutSpec.mm @@ -259,13 +259,25 @@ - (NSString *)asciiArtName return result; } +#pragma mark - NSLocking + +- (void)lock +{ + __instanceLock__.lock(); +} + +- (void)unlock +{ + __instanceLock__.unlock(); +} + @end #pragma mark - ASWrapperLayoutSpec @implementation ASWrapperLayoutSpec -+ (instancetype)wrapperWithLayoutElement:(id)layoutElement ++ (instancetype)wrapperWithLayoutElement:(id)layoutElement NS_RETURNS_RETAINED { return [[self alloc] initWithLayoutElement:layoutElement]; } @@ -279,7 +291,7 @@ - (instancetype)initWithLayoutElement:(id)layoutElement return self; } -+ (instancetype)wrapperWithLayoutElements:(NSArray> *)layoutElements ++ (instancetype)wrapperWithLayoutElements:(NSArray> *)layoutElements NS_RETURNS_RETAINED { return [[self alloc] initWithLayoutElements:layoutElements]; } diff --git a/Source/Layout/ASOverlayLayoutSpec.h b/Source/Layout/ASOverlayLayoutSpec.h index 54324f4ed..086252075 100644 --- a/Source/Layout/ASOverlayLayoutSpec.h +++ b/Source/Layout/ASOverlayLayoutSpec.h @@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN * @param child A child that is laid out to determine the size of this spec. * @param overlay A layoutElement object that is laid out over the child. */ -+ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay AS_WARN_UNUSED_RESULT; ++ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASOverlayLayoutSpec.mm b/Source/Layout/ASOverlayLayoutSpec.mm index 9ac5d1e56..77f8239da 100644 --- a/Source/Layout/ASOverlayLayoutSpec.mm +++ b/Source/Layout/ASOverlayLayoutSpec.mm @@ -26,7 +26,7 @@ @implementation ASOverlayLayoutSpec #pragma mark - Class -+ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay ++ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay NS_RETURNS_RETAINED { return [[self alloc] initWithChild:child overlay:overlay]; } diff --git a/Source/Layout/ASRatioLayoutSpec.h b/Source/Layout/ASRatioLayoutSpec.h index f5338d4da..b6f9987b5 100644 --- a/Source/Layout/ASRatioLayoutSpec.h +++ b/Source/Layout/ASRatioLayoutSpec.h @@ -43,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) CGFloat ratio; -+ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child AS_WARN_UNUSED_RESULT; ++ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASRatioLayoutSpec.mm b/Source/Layout/ASRatioLayoutSpec.mm index 294d11064..aec3a9c8f 100644 --- a/Source/Layout/ASRatioLayoutSpec.mm +++ b/Source/Layout/ASRatioLayoutSpec.mm @@ -35,7 +35,7 @@ @implementation ASRatioLayoutSpec #pragma mark - Lifecycle -+ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child ++ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child NS_RETURNS_RETAINED { return [[self alloc] initWithRatio:ratio child:child]; } diff --git a/Source/Layout/ASRelativeLayoutSpec.h b/Source/Layout/ASRelativeLayoutSpec.h index 27860b00e..cd3f14c9f 100644 --- a/Source/Layout/ASRelativeLayoutSpec.h +++ b/Source/Layout/ASRelativeLayoutSpec.h @@ -74,7 +74,7 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)relativePositionLayoutSpecWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption - child:(id)child AS_WARN_UNUSED_RESULT; + child:(id)child NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /*! * @discussion convenience initializer for a ASRelativeLayoutSpec diff --git a/Source/Layout/ASRelativeLayoutSpec.mm b/Source/Layout/ASRelativeLayoutSpec.mm index ea5f37df5..b14795d27 100644 --- a/Source/Layout/ASRelativeLayoutSpec.mm +++ b/Source/Layout/ASRelativeLayoutSpec.mm @@ -36,7 +36,7 @@ - (instancetype)initWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizon return self; } -+ (instancetype)relativePositionLayoutSpecWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption child:(id)child ++ (instancetype)relativePositionLayoutSpecWithHorizontalPosition:(ASRelativeLayoutSpecPosition)horizontalPosition verticalPosition:(ASRelativeLayoutSpecPosition)verticalPosition sizingOption:(ASRelativeLayoutSpecSizingOption)sizingOption child:(id)child NS_RETURNS_RETAINED { return [[self alloc] initWithHorizontalPosition:horizontalPosition verticalPosition:verticalPosition sizingOption:sizingOption child:child]; } diff --git a/Source/Layout/ASStackLayoutSpec.h b/Source/Layout/ASStackLayoutSpec.h index b4a27697e..44721f53d 100644 --- a/Source/Layout/ASStackLayoutSpec.h +++ b/Source/Layout/ASStackLayoutSpec.h @@ -88,7 +88,7 @@ NS_ASSUME_NONNULL_BEGIN spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems - children:(NSArray> *)children AS_WARN_UNUSED_RESULT; + children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** @param direction The direction of the stack view (horizontal or vertical) @@ -105,7 +105,7 @@ NS_ASSUME_NONNULL_BEGIN alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent - children:(NSArray> *)children AS_WARN_UNUSED_RESULT; + children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** @param direction The direction of the stack view (horizontal or vertical) @@ -124,17 +124,17 @@ NS_ASSUME_NONNULL_BEGIN flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent lineSpacing:(CGFloat)lineSpacing - children:(NSArray> *)children AS_WARN_UNUSED_RESULT; + children:(NSArray> *)children NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** * @return A stack layout spec with direction of ASStackLayoutDirectionVertical **/ -+ (instancetype)verticalStackLayoutSpec AS_WARN_UNUSED_RESULT; ++ (instancetype)verticalStackLayoutSpec NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** * @return A stack layout spec with direction of ASStackLayoutDirectionHorizontal **/ -+ (instancetype)horizontalStackLayoutSpec AS_WARN_UNUSED_RESULT; ++ (instancetype)horizontalStackLayoutSpec NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/Layout/ASStackLayoutSpec.mm b/Source/Layout/ASStackLayoutSpec.mm index fc89d6f97..756912d80 100644 --- a/Source/Layout/ASStackLayoutSpec.mm +++ b/Source/Layout/ASStackLayoutSpec.mm @@ -36,29 +36,29 @@ - (instancetype)init return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart lineSpacing:0.0 children:nil]; } -+ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children NS_RETURNS_RETAINED { return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart lineSpacing: 0.0 children:children]; } -+ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray> *)children ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray> *)children NS_RETURNS_RETAINED { return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent lineSpacing:0.0 children:children]; } -+ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent lineSpacing:(CGFloat)lineSpacing children:(NSArray> *)children ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent lineSpacing:(CGFloat)lineSpacing children:(NSArray> *)children NS_RETURNS_RETAINED { return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent lineSpacing:lineSpacing children:children]; } -+ (instancetype)verticalStackLayoutSpec ++ (instancetype)verticalStackLayoutSpec NS_RETURNS_RETAINED { ASStackLayoutSpec *stackLayoutSpec = [[self alloc] init]; stackLayoutSpec.direction = ASStackLayoutDirectionVertical; return stackLayoutSpec; } -+ (instancetype)horizontalStackLayoutSpec ++ (instancetype)horizontalStackLayoutSpec NS_RETURNS_RETAINED { ASStackLayoutSpec *stackLayoutSpec = [[self alloc] init]; stackLayoutSpec.direction = ASStackLayoutDirectionHorizontal; diff --git a/Source/Private/ASCellNode+Internal.h b/Source/Private/ASCellNode+Internal.h index 9135dd5fc..d23dc173d 100644 --- a/Source/Private/ASCellNode+Internal.h +++ b/Source/Private/ASCellNode+Internal.h @@ -63,7 +63,21 @@ NS_ASSUME_NONNULL_BEGIN @property (atomic, weak, nullable) id owningNode; -@property (nonatomic, assign) BOOL shouldUseUIKitCell; +@property (nonatomic, readonly) BOOL shouldUseUIKitCell; + +@end + +@class ASWrapperCellNode; + +typedef CGSize (^ASSizeForItemBlock)(ASWrapperCellNode *node, CGSize collectionSize); +typedef UICollectionViewCell * _Nonnull(^ASCellForItemBlock)(ASWrapperCellNode *node); +typedef UICollectionReusableView * _Nonnull(^ASViewForSupplementaryBlock)(ASWrapperCellNode *node); + +@interface ASWrapperCellNode : ASCellNode + +@property (nonatomic, copy, readonly) ASSizeForItemBlock sizeForItemBlock; +@property (nonatomic, copy, readonly) ASCellForItemBlock cellForItemBlock; +@property (nonatomic, copy, readonly) ASViewForSupplementaryBlock viewForSupplementaryBlock; @end diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 59273c975..eafc445a8 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -158,6 +158,21 @@ - (void)invalidateLayout } } +/** + * NOTE: It is suggested practice on the Web to override invalidationContextForInteractivelyMovingItems… and call out to the + * data source to move the item (so that if e.g. the item size depends on the data, you get the data you expect). However, as of iOS 11 this + * doesn't work, because UICV machinery will also call out to the data source to move the item after the interaction is done. The result is + * that your data source state will be incorrect due to this last move call. Plus it's just an API violation. + * + * Things tried: + * - Doing the speculative data source moves, and then UNDOING the last one in invalidationContextForEndingInteractiveMovementOfItems… + * but this does not work because the UICV machinery informs its data source before it calls that method on us, so we are too late. + * + * The correct practice is to use the UIDataSourceTranslating API introduced in iOS 11. Currently Texture does not support this API but we can + * build it if there is demand. We could add an id field onto the layout context object, and the layout client can + * use data source index paths when it reads nodes or other data source data. + */ + - (CGSize)collectionViewContentSize { ASDisplayNodeAssertMainThread(); diff --git a/Source/Private/ASCollectionView+Undeprecated.h b/Source/Private/ASCollectionView+Undeprecated.h index bc03fc6aa..bf037b16a 100644 --- a/Source/Private/ASCollectionView+Undeprecated.h +++ b/Source/Private/ASCollectionView+Undeprecated.h @@ -154,7 +154,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. @@ -165,7 +165,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. diff --git a/Source/tvOS/ASControlNode+tvOS.h b/Source/Private/ASControlNode+Private.h similarity index 89% rename from Source/tvOS/ASControlNode+tvOS.h rename to Source/Private/ASControlNode+Private.h index cd05078a7..f3249c11c 100644 --- a/Source/tvOS/ASControlNode+tvOS.h +++ b/Source/Private/ASControlNode+Private.h @@ -1,5 +1,5 @@ // -// ASControlNode+tvOS.h +// ASControlNode+Private.h // Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. @@ -15,10 +15,12 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#if TARGET_OS_TV #import -@interface ASControlNode (tvOS) +@interface ASControlNode (Private) -@end +#if TARGET_OS_TV +- (void)_pressDown; #endif + +@end diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index 986f3acf9..4d307439c 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -20,7 +20,8 @@ #import #import #import -#import +#import +#import #import #import #import @@ -218,15 +219,14 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro displayBlock = ^id{ CHECK_CANCELLED_AND_RETURN_NIL(); - UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); for (dispatch_block_t block in displayBlocks) { - CHECK_CANCELLED_AND_RETURN_NIL(UIGraphicsEndImageContext()); + CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext()); block(); } - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); ASDN_DELAY_FOR_DISPLAY(); return image; @@ -236,8 +236,8 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro CHECK_CANCELLED_AND_RETURN_NIL(); if (shouldCreateGraphicsContext) { - UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); - CHECK_CANCELLED_AND_RETURN_NIL( UIGraphicsEndImageContext(); ); + ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); } CGContextRef currentContext = UIGraphicsGetCurrentContext(); @@ -256,9 +256,8 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro [self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor]; if (shouldCreateGraphicsContext) { - CHECK_CANCELLED_AND_RETURN_NIL( UIGraphicsEndImageContext(); ); - image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); + image = ASGraphicsGetImageAndEndCurrentContext(); } ASDN_DELAY_FOR_DISPLAY(); @@ -332,7 +331,7 @@ - (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image: bounds.size.height *= contentsScale; CGFloat white = 0.0f, alpha = 0.0f; [backgroundColor getWhite:&white alpha:&alpha]; - UIGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale); + ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale); [*image drawInRect:bounds]; } else { bounds = CGContextGetClipBoundingBox(context); @@ -362,8 +361,7 @@ - (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image: [roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil. if (*image) { - *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + *image = ASGraphicsGetImageAndEndCurrentContext(); } } } diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index ebcaf23c1..987fdfa76 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 @@ -118,6 +141,10 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat // delegate to inform of ASInterfaceState changes (used by ASNodeController) @property (nonatomic, weak) id interfaceStateDelegate; +// The -pendingInterfaceState holds the value that will be applied to -interfaceState by the +// ASCATransactionQueue. If already applied, it matches -interfaceState. Thread-safe access. +@property (nonatomic, readonly) ASInterfaceState pendingInterfaceState; + // These methods are recursive, and either union or remove the provided interfaceState to all sub-elements. - (void)enterInterfaceState:(ASInterfaceState)interfaceState; - (void)exitInterfaceState:(ASInterfaceState)interfaceState; @@ -207,13 +234,32 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat * ASNetworkImageNode and ASMultiplexImageNode set this to YES, because they load data from a database or server, * and are expected to support a placeholder state given that display is often blocked on slow data fetching. */ -@property (nonatomic, assign) BOOL shouldBypassEnsureDisplay; +@property (atomic) BOOL shouldBypassEnsureDisplay; /** * @abstract Checks whether a node should be scheduled for display, considering its current and new interface states. */ - (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState; +/** + * @abstract safeAreaInsets will fallback to this value if the corresponding UIKit property is not available + * (due to an old iOS version). + * + * @discussion This should be set by the owning view controller based on it's layout guides. + * If this is not a view controllet's node the value will be calculated automatically by the parent node. + */ +@property (nonatomic, assign) UIEdgeInsets fallbackSafeAreaInsets; + +/** + * @abstract Indicates if this node is a view controller's root node. Defaults to NO. + * + * @discussion Set to YES in -[ASViewController initWithNode:]. + * + * YES here only means that this node is used as an ASViewController node. It doesn't mean that this node is a root of + * ASDisplayNode hierarchy, e.g. when its view controller is parented by another ASViewController. + */ +@property (nonatomic, assign, getter=isViewControllerRoot) BOOL viewControllerRoot; + @end @@ -225,7 +271,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 +283,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+FrameworkSubclasses.h b/Source/Private/ASDisplayNode+FrameworkSubclasses.h deleted file mode 100644 index 9b083c550..000000000 --- a/Source/Private/ASDisplayNode+FrameworkSubclasses.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// ASDisplayNode+FrameworkSubclasses.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 -// - -// -// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. -// These methods must never be called or overridden by other classes. -// - -#import -#import - -// These are included because most internal subclasses need it. -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface ASDisplayNode () -{ - // Protects access to _view, _layer, _pendingViewState, _subnodes, _supernode, and other properties which are accessed from multiple threads. - @package - ASDN::RecursiveMutex __instanceLock__; -} -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index 25c0efdf6..939ed642c 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -21,7 +21,6 @@ #import #import #import -#import #import /** @@ -96,16 +95,6 @@ ASDISPLAYNODE_INLINE BOOL ASDisplayNodeShouldApplyBridgedWriteToView(ASDisplayNo */ @implementation ASDisplayNode (UIViewBridge) -- (BOOL)canBecomeFirstResponder -{ - return NO; -} - -- (BOOL)canResignFirstResponder -{ - return YES; -} - #if TARGET_OS_TV // Focus Engine - (BOOL)canBecomeFocused @@ -146,23 +135,34 @@ - (UIView *)preferredFocusedView } #endif +- (BOOL)canBecomeFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __canBecomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __canResignFirstResponder]; +} + - (BOOL)isFirstResponder { ASDisplayNodeAssertMainThread(); - return _view != nil && [_view isFirstResponder]; + return [self __isFirstResponder]; } -// Note: this implicitly loads the view if it hasn't been loaded yet. - (BOOL)becomeFirstResponder { ASDisplayNodeAssertMainThread(); - return !self.layerBacked && [self canBecomeFirstResponder] && [self.view becomeFirstResponder]; + return [self __becomeFirstResponder]; } - (BOOL)resignFirstResponder { ASDisplayNodeAssertMainThread(); - return !self.layerBacked && [self canResignFirstResponder] && [_view resignFirstResponder]; + return [self __resignFirstResponder]; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender @@ -186,17 +186,12 @@ - (void)setAlpha:(CGFloat)newAlpha - (CGFloat)cornerRadius { ASDN::MutexLocker l(__instanceLock__); - if (_cornerRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { - return self.layerCornerRadius; - } else { - return _cornerRadius; - } + return _cornerRadius; } - (void)setCornerRadius:(CGFloat)newCornerRadius { - ASDN::MutexLocker l(__instanceLock__); - [self updateCornerRoundingWithType:_cornerRoundingType cornerRadius:newCornerRadius]; + [self updateCornerRoundingWithType:self.cornerRoundingType cornerRadius:newCornerRadius]; } - (ASCornerRoundingType)cornerRoundingType @@ -207,8 +202,7 @@ - (ASCornerRoundingType)cornerRoundingType - (void)setCornerRoundingType:(ASCornerRoundingType)newRoundingType { - ASDN::MutexLocker l(__instanceLock__); - [self updateCornerRoundingWithType:newRoundingType cornerRadius:_cornerRadius]; + [self updateCornerRoundingWithType:newRoundingType cornerRadius:self.cornerRadius]; } - (NSString *)contentsGravity @@ -854,23 +848,115 @@ - (void)setEdgeAntialiasingMask:(unsigned int)edgeAntialiasingMask - (UISemanticContentAttribute)semanticContentAttribute { _bridge_prologue_read; - if (AS_AT_LEAST_IOS9) { - return _getFromViewOnly(semanticContentAttribute); - } - return UISemanticContentAttributeUnspecified; + return _getFromViewOnly(semanticContentAttribute); } - (void)setSemanticContentAttribute:(UISemanticContentAttribute)semanticContentAttribute { _bridge_prologue_write; - if (AS_AT_LEAST_IOS9) { - _setToViewOnly(semanticContentAttribute, semanticContentAttribute); + _setToViewOnly(semanticContentAttribute, semanticContentAttribute); #if YOGA - [self semanticContentAttributeDidChange:semanticContentAttribute]; + [self semanticContentAttributeDidChange:semanticContentAttribute]; #endif +} + +- (UIEdgeInsets)layoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + UIEdgeInsets margins = _getFromViewOnly(layoutMargins); + + if (!AS_AT_LEAST_IOS11 && self.insetsLayoutMarginsFromSafeArea) { + UIEdgeInsets safeArea = self.safeAreaInsets; + margins = ASConcatInsets(margins, safeArea); + } + + return margins; +} + +- (void)setLayoutMargins:(UIEdgeInsets)layoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(layoutMargins, layoutMargins); +} + +- (BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(preservesSuperviewLayoutMargins); +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(preservesSuperviewLayoutMargins, preservesSuperviewLayoutMargins); +} + +- (void)layoutMarginsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnLayoutMarginsChanges) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)safeAreaInsets +{ + _bridge_prologue_read; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked && __loaded(self)) { + return self.view.safeAreaInsets; + } + } + return _fallbackSafeAreaInsets; +} + +- (BOOL)insetsLayoutMarginsFromSafeArea +{ + _bridge_prologue_read; + + return [self _locked_insetsLayoutMarginsFromSafeArea]; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea +{ + ASDisplayNodeAssertThreadAffinity(self); + BOOL shouldNotifyAboutUpdate; + { + _bridge_prologue_write; + + _fallbackInsetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + _setToViewOnly(insetsLayoutMarginsFromSafeArea, insetsLayoutMarginsFromSafeArea); + } + } + + shouldNotifyAboutUpdate = __loaded(self) && (!AS_AT_LEAST_IOS11 || _flags.layerBacked); + } + + if (shouldNotifyAboutUpdate) { + [self layoutMarginsDidChange]; } } +- (void)safeAreaInsetsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnSafeAreaChanges) { + [self setNeedsLayout]; + } + + [self _fallbackUpdateSafeAreaOnChildren]; +} + @end @implementation ASDisplayNode (InternalPropertyBridge) @@ -887,6 +973,16 @@ - (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius _setToLayer(cornerRadius, newLayerCornerRadius); } +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea +{ + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + return _getFromViewOnly(insetsLayoutMarginsFromSafeArea); + } + } + return _fallbackInsetsLayoutMarginsFromSafeArea; +} + @end #pragma mark - UIViewBridgeAccessibility @@ -904,22 +1000,19 @@ - (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius (_view ? _view.viewAndPendingViewStateProperty : nodeProperty )\ : ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty -// Attributed version of `_getAccessibilityFromViewOrProperty` macro -#define _getAttributedAccessibilityFromViewOrProperty(nodeProperty, viewAndPendingViewStatePropertyKey) __loaded(self) ? \ -(_view ? (NSAttributedString *)[_view valueForKey: viewAndPendingViewStatePropertyKey] : nodeProperty )\ -: (NSAttributedString *)[ASDisplayNodeGetPendingState(self) valueForKey: viewAndPendingViewStatePropertyKey] - // Helper function to set property values on pending state or view and property if loaded #define _setAccessibilityToViewAndProperty(nodeProperty, nodeValueExpr, viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) \ nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) -// Attributed version of `_setAccessibilityToViewAndProperty` macro -#define _setAttributedAccessibilityToViewAndProperty(nodeProperty, nodeValueExpr, viewAndPendingViewStatePropertyKey, viewAndPendingViewStateExpr) \ -nodeProperty = nodeValueExpr; BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ -if (shouldApply) { [_view setValue:(viewAndPendingViewStateExpr) forKey: viewAndPendingViewStatePropertyKey]; } else { [ASDisplayNodeGetPendingState(self) setValue:(viewAndPendingViewStateExpr) forKey:viewAndPendingViewStatePropertyKey]; } - @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; @@ -942,24 +1035,28 @@ - (void)setAccessibilityLabel:(NSString *)accessibilityLabel { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityLabel, accessibilityLabel, accessibilityLabel); - if (AS_AT_LEAST_IOS11) { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { NSAttributedString *accessibilityAttributedLabel = accessibilityLabel ? [[NSAttributedString alloc] initWithString:accessibilityLabel] : nil; - _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, @"accessibilityAttributedLabel", accessibilityAttributedLabel); + _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); } +#endif } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 - (NSAttributedString *)accessibilityAttributedLabel { _bridge_prologue_read; - return _getAttributedAccessibilityFromViewOrProperty(_accessibilityAttributedLabel, @"accessibilityAttributedLabel"); + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel); } - (void)setAccessibilityAttributedLabel:(NSAttributedString *)accessibilityAttributedLabel { _bridge_prologue_write; - { _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, @"accessibilityAttributedLabel", accessibilityAttributedLabel); } + { _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); } { _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityAttributedLabel.string, accessibilityLabel, accessibilityAttributedLabel.string); } } +#endif - (NSString *)accessibilityHint { @@ -971,24 +1068,29 @@ - (void)setAccessibilityHint:(NSString *)accessibilityHint { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityHint, accessibilityHint, accessibilityHint); - if (AS_AT_LEAST_IOS11) { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { NSAttributedString *accessibilityAttributedHint = accessibilityHint ? [[NSAttributedString alloc] initWithString:accessibilityHint] : nil; - _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, @"accessibilityAttributedHint", accessibilityAttributedHint); + _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); } +#endif } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 - (NSAttributedString *)accessibilityAttributedHint { _bridge_prologue_read; - return _getAttributedAccessibilityFromViewOrProperty(_accessibilityAttributedHint, @"accessibilityAttributedHint"); + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedHint, accessibilityAttributedHint); } - (void)setAccessibilityAttributedHint:(NSAttributedString *)accessibilityAttributedHint { _bridge_prologue_write; - { _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, @"accessibilityAttributedHint", accessibilityAttributedHint); } + { _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); } + { _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityAttributedHint.string, accessibilityHint, accessibilityAttributedHint.string); } } +#endif - (NSString *)accessibilityValue { @@ -1000,24 +1102,28 @@ - (void)setAccessibilityValue:(NSString *)accessibilityValue { _bridge_prologue_write; _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityValue, accessibilityValue, accessibilityValue); - if (AS_AT_LEAST_IOS11) { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { NSAttributedString *accessibilityAttributedValue = accessibilityValue ? [[NSAttributedString alloc] initWithString:accessibilityValue] : nil; - _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, @"accessibilityAttributedValue", accessibilityAttributedValue); + _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); } +#endif } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 - (NSAttributedString *)accessibilityAttributedValue { _bridge_prologue_read; - return _getAttributedAccessibilityFromViewOrProperty(_accessibilityAttributedValue, @"accessibilityAttributedValue"); + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedValue, accessibilityAttributedValue); } - (void)setAccessibilityAttributedValue:(NSAttributedString *)accessibilityAttributedValue { _bridge_prologue_write; - { _setAttributedAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, @"accessibilityAttributedValue", accessibilityAttributedValue); } + { _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); } { _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityAttributedValue.string, accessibilityValue, accessibilityAttributedValue.string); } } +#endif - (UIAccessibilityTraits)accessibilityTraits { diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 1be9e8c94..ab0e3faed 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -76,8 +76,10 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo @interface ASDisplayNode () <_ASTransitionContextCompletionDelegate> { @package - _ASPendingState *_pendingViewState; + ASDN::RecursiveMutex __instanceLock__; + _ASPendingState *_pendingViewState; + ASInterfaceState _pendingInterfaceState; UIView *_view; CALayer *_layer; @@ -202,6 +204,15 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo UIBezierPath *_accessibilityPath; BOOL _isAccessibilityContainer; + // These properties are used on iOS 10 and lower, where safe area is not supported by UIKit. + UIEdgeInsets _fallbackSafeAreaInsets; + BOOL _fallbackInsetsLayoutMarginsFromSafeArea; + + BOOL _automaticallyRelayoutOnSafeAreaChanges; + BOOL _automaticallyRelayoutOnLayoutMarginsChanges; + + BOOL _isViewControllerRoot; + // performance measurement ASDisplayNodePerformanceMeasurementOptions _measurementOptions; NSTimeInterval _layoutSpecTotalTime; @@ -275,6 +286,13 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo - (void)__incrementVisibilityNotificationsDisabled; - (void)__decrementVisibilityNotificationsDisabled; +// Helper methods for UIResponder forwarding +- (BOOL)__canBecomeFirstResponder; +- (BOOL)__becomeFirstResponder; +- (BOOL)__canResignFirstResponder; +- (BOOL)__resignFirstResponder; +- (BOOL)__isFirstResponder; + /// Helper method to summarize whether or not the node run through the display process - (BOOL)_implementsDisplay; @@ -326,12 +344,17 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo */ - (void)nodeViewDidAddGestureRecognizer; +// Recalculates fallbackSafeAreaInsets for the subnodes +- (void)_fallbackUpdateSafeAreaOnChildren; + @end @interface ASDisplayNode (InternalPropertyBridge) @property (nonatomic, assign) CGFloat layerCornerRadius; +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASIGListAdapterBasedDataSource.m b/Source/Private/ASIGListAdapterBasedDataSource.m index 31a6c4582..ab1863622 100644 --- a/Source/Private/ASIGListAdapterBasedDataSource.m +++ b/Source/Private/ASIGListAdapterBasedDataSource.m @@ -102,6 +102,14 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView [self.delegate scrollViewWillBeginDragging:scrollView]; } +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + // IGListAdapter doesn't implement scrollViewWillEndDragging yet (pending pull request), so we need this check for now. Doesn't hurt to have it anyways :) + if ([self.delegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { + [self.delegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + } +} + - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { [self.delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index 31766b366..d5c1e035b 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -25,6 +25,11 @@ NS_ASSUME_NONNULL_BEGIN ASDISPLAYNODE_EXTERN_C_BEGIN +void ASInitializeFrameworkMainThread(void); + +BOOL ASDefaultAllowsGroupOpacity(void); +BOOL ASDefaultAllowsEdgeAntialiasing(void); + BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector); BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector); @@ -32,15 +37,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 +85,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 { @@ -95,8 +100,21 @@ ASDISPLAYNODE_INLINE void ASBoundsAndPositionForFrame(CGRect rect, CGPoint origi rect.origin.y + rect.size.height * anchorPoint.y); } +ASDISPLAYNODE_INLINE UIEdgeInsets ASConcatInsets(UIEdgeInsets insetsA, UIEdgeInsets insetsB) +{ + insetsA.top += insetsB.top; + insetsA.left += insetsB.left; + insetsA.bottom += insetsB.bottom; + insetsA.right += insetsB.right; + return insetsA; +} + @interface NSIndexPath (ASInverseComparison) - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath; @end NS_ASSUME_NONNULL_END + +#ifndef AS_INITIALIZE_FRAMEWORK_MANUALLY +#define AS_INITIALIZE_FRAMEWORK_MANUALLY 0 +#endif diff --git a/Source/Private/ASInternalHelpers.m b/Source/Private/ASInternalHelpers.m index e0d757101..3912a66c7 100644 --- a/Source/Private/ASInternalHelpers.m +++ b/Source/Private/ASInternalHelpers.m @@ -25,6 +25,28 @@ #import #import +static BOOL defaultAllowsGroupOpacity = YES; +static BOOL defaultAllowsEdgeAntialiasing = NO; + +void ASInitializeFrameworkMainThread(void) +{ + ASDisplayNodeThreadIsMain(); + // Ensure these values are cached on the main thread before needed in the background. + CALayer *layer = [[[UIView alloc] init] layer]; + defaultAllowsGroupOpacity = layer.allowsGroupOpacity; + defaultAllowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; +} + +BOOL ASDefaultAllowsGroupOpacity(void) +{ + return defaultAllowsGroupOpacity; +} + +BOOL ASDefaultAllowsEdgeAntialiasing(void) +{ + return defaultAllowsEdgeAntialiasing; +} + BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector) { if (superclass == subclass) return NO; // Even if the class implements the selector, it doesn't override itself. @@ -60,7 +82,7 @@ IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block) } } -void ASPerformBlockOnMainThread(void (^block)()) +void ASPerformBlockOnMainThread(void (^block)(void)) { if (block == nil){ return; @@ -72,7 +94,7 @@ void ASPerformBlockOnMainThread(void (^block)()) } } -void ASPerformBlockOnBackgroundThread(void (^block)()) +void ASPerformBlockOnBackgroundThread(void (^block)(void)) { if (block == nil){ return; @@ -84,7 +106,7 @@ void ASPerformBlockOnBackgroundThread(void (^block)()) } } -void ASPerformBackgroundDeallocation(id object) +void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object) { [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object]; } @@ -143,8 +165,9 @@ CGFloat ASScreenScale() static CGFloat __scale = 0.0; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ASDisplayNodeCAssertMainThread(); - __scale = [[UIScreen mainScreen] scale]; + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); + __scale = CGContextGetCTM(UIGraphicsGetCurrentContext()).a; + UIGraphicsEndImageContext(); }); return __scale; } @@ -154,10 +177,23 @@ CGSize ASFloorSizeValues(CGSize s) return CGSizeMake(ASFloorPixelValue(s.width), ASFloorPixelValue(s.height)); } +// See ASCeilPixelValue for a more thoroguh explanation of (f + FLT_EPSILON), +// but here is some quick math: +// +// Imagine a layout that comes back with a height of 100.66666666663 +// for a 3x deice: +// 100.66666666663 * 3 = 301.99999999988995 +// floor(301.99999999988995) = 301 +// 301 / 3 = 100.333333333 +// +// If we add FLT_EPSILON to normalize the garbage at the end we get: +// po (100.66666666663 + FLT_EPSILON) * 3 = 302.00000035751782 +// floor(302.00000035751782) = 302 +// 302/3 = 100.66666666 CGFloat ASFloorPixelValue(CGFloat f) { CGFloat scale = ASScreenScale(); - return floor(f * scale) / scale; + return floor((f + FLT_EPSILON) * scale) / scale; } CGPoint ASCeilPointValues(CGPoint p) @@ -170,10 +206,29 @@ CGSize ASCeilSizeValues(CGSize s) return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height)); } +// With 3x devices layouts will often to compute to pixel bounds but +// include garbage values beyond the precision of a float/double. +// This garbage can result in a pixel value being rounded up when it isn't +// necessary. +// +// For example, imagine a layout that comes back with a height of 100.666666666669 +// for a 3x device: +// 100.666666666669 * 3 = 302.00000000000699 +// ceil(302.00000000000699) = 303 +// 303/3 = 101 +// +// If we use FLT_EPSILON to get rid of the garbage at the end of the value, +// things work as expected: +// (100.666666666669 - FLT_EPSILON) * 3 = 301.99999964237912 +// ceil(301.99999964237912) = 302 +// 302/3 = 100.666666666 +// +// For even more conversation around this, see: +// https://github.com/TextureGroup/Texture/issues/838 CGFloat ASCeilPixelValue(CGFloat f) { CGFloat scale = ASScreenScale(); - return ceil(f * scale) / scale; + return ceil((f - FLT_EPSILON) * scale) / scale; } CGFloat ASRoundPixelValue(CGFloat f) diff --git a/Source/Private/ASMutableElementMap.h b/Source/Private/ASMutableElementMap.h index 3b5c9bd7f..10225a455 100644 --- a/Source/Private/ASMutableElementMap.h +++ b/Source/Private/ASMutableElementMap.h @@ -37,10 +37,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 7ce12dca8..c7742bbec 100644 --- a/Source/Private/ASMutableElementMap.m +++ b/Source/Private/ASMutableElementMap.m @@ -48,7 +48,7 @@ - (id)copyWithZone:(NSZone *)zone return [[ASElementMap alloc] initWithSections:_sections items:_sectionsOfItems supplementaryElements:_supplementaryElements]; } -- (void)removeAllSectionContexts +- (void)removeAllSections { [_sections removeAllObjects]; } @@ -63,7 +63,7 @@ - (void)removeItemsAtIndexPaths:(NSArray *)indexPaths ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(_sectionsOfItems, indexPaths); } -- (void)removeSectionContextsAtIndexes:(NSIndexSet *)indexes +- (void)removeSectionsAtIndexes:(NSIndexSet *)indexes { [_sections removeObjectsAtIndexes:indexes]; } diff --git a/Source/Private/ASNetworkImageLoadInfo+Private.h b/Source/Private/ASNetworkImageLoadInfo+Private.h new file mode 100644 index 000000000..a77d48af6 --- /dev/null +++ b/Source/Private/ASNetworkImageLoadInfo+Private.h @@ -0,0 +1,26 @@ +// +// ASNetworkImageLoadInfo+Private.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASNetworkImageLoadInfo () + +- (instancetype)initWithURL:(NSURL *)url + sourceType:(ASNetworkImageSourceType)sourceType + downloadIdentifier:(nullable id)downloadIdentifier + userInfo:(nullable id)userInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASRectMap.h b/Source/Private/ASRectMap.h new file mode 100644 index 000000000..e4b05d7cb --- /dev/null +++ b/Source/Private/ASRectMap.h @@ -0,0 +1,52 @@ +// +// ASRectMap.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 + +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 NS_RETURNS_RETAINED; + +/** + * 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 000000000..2e4398c32 --- /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 NS_RETURNS_RETAINED +{ + 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 af47f59b6..000000000 --- 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 5a98a3f70..000000000 --- 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 e56ef03a8..2ba3d1d80 100644 --- a/Source/Private/ASSection.h +++ b/Source/Private/ASSection.h @@ -16,15 +16,29 @@ // #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 diff --git a/Source/Private/ASTableView+Undeprecated.h b/Source/Private/ASTableView+Undeprecated.h index bab43adf0..7ddc9c28e 100644 --- a/Source/Private/ASTableView+Undeprecated.h +++ b/Source/Private/ASTableView+Undeprecated.h @@ -146,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. diff --git a/Source/Private/TextExperiment/Component/ASTextDebugOption.m b/Source/Private/TextExperiment/Component/ASTextDebugOption.m index e823d328b..2e0a706ea 100755 --- a/Source/Private/TextExperiment/Component/ASTextDebugOption.m +++ b/Source/Private/TextExperiment/Component/ASTextDebugOption.m @@ -16,14 +16,14 @@ 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/Component/ASTextInput.h b/Source/Private/TextExperiment/Component/ASTextInput.h index 6b2aa79bb..061eef310 100755 --- a/Source/Private/TextExperiment/Component/ASTextInput.h +++ b/Source/Private/TextExperiment/Component/ASTextInput.h @@ -1,12 +1,18 @@ // // ASTextInput.h -// Modified from YYText +// Texture // -// Created by ibireme on 15/4/17. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -36,8 +42,8 @@ typedef NS_ENUM(NSInteger, ASTextAffinity) { @property (nonatomic, readonly) NSInteger offset; @property (nonatomic, readonly) ASTextAffinity affinity; -+ (instancetype)positionWithOffset:(NSInteger)offset; -+ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity) affinity; ++ (instancetype)positionWithOffset:(NSInteger)offset NS_RETURNS_RETAINED; ++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity) affinity NS_RETURNS_RETAINED; - (NSComparisonResult)compare:(id)otherPosition; @@ -57,10 +63,10 @@ typedef NS_ENUM(NSInteger, ASTextAffinity) { @property (nonatomic, readonly) ASTextPosition *end; @property (nonatomic, readonly, getter=isEmpty) BOOL empty; -+ (instancetype)rangeWithRange:(NSRange)range; -+ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity) affinity; -+ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end; -+ (instancetype)defaultRange; ///< <{0,0} Forward> ++ (instancetype)rangeWithRange:(NSRange)range NS_RETURNS_RETAINED; ++ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity) affinity NS_RETURNS_RETAINED; ++ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end NS_RETURNS_RETAINED; ++ (instancetype)defaultRange NS_RETURNS_RETAINED; ///< <{0,0} Forward> - (NSRange)asRange; diff --git a/Source/Private/TextExperiment/Component/ASTextInput.m b/Source/Private/TextExperiment/Component/ASTextInput.m index fa1c53b22..a56426b16 100755 --- a/Source/Private/TextExperiment/Component/ASTextInput.m +++ b/Source/Private/TextExperiment/Component/ASTextInput.m @@ -1,12 +1,18 @@ // // ASTextInput.m -// Modified from YYText +// Texture // -// Created by ibireme on 15/4/17. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -15,11 +21,11 @@ @implementation ASTextPosition -+ (instancetype)positionWithOffset:(NSInteger)offset { ++ (instancetype)positionWithOffset:(NSInteger)offset NS_RETURNS_RETAINED { return [self positionWithOffset:offset affinity:ASTextAffinityForward]; } -+ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity)affinity { ++ (instancetype)positionWithOffset:(NSInteger)offset affinity:(ASTextAffinity)affinity NS_RETURNS_RETAINED { ASTextPosition *p = [self new]; p->_offset = offset; p->_affinity = affinity; @@ -85,17 +91,17 @@ - (NSRange)asRange { return NSMakeRange(_start.offset, _end.offset - _start.offset); } -+ (instancetype)rangeWithRange:(NSRange)range { ++ (instancetype)rangeWithRange:(NSRange)range NS_RETURNS_RETAINED { return [self rangeWithRange:range affinity:ASTextAffinityForward]; } -+ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity)affinity { ++ (instancetype)rangeWithRange:(NSRange)range affinity:(ASTextAffinity)affinity NS_RETURNS_RETAINED { ASTextPosition *start = [ASTextPosition positionWithOffset:range.location affinity:affinity]; ASTextPosition *end = [ASTextPosition positionWithOffset:range.location + range.length affinity:affinity]; return [self rangeWithStart:start end:end]; } -+ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end { ++ (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end NS_RETURNS_RETAINED { if (!start || !end) return nil; if ([start compare:end] == NSOrderedDescending) { ASTEXT_SWAP(start, end); @@ -106,7 +112,7 @@ + (instancetype)rangeWithStart:(ASTextPosition *)start end:(ASTextPosition *)end return range; } -+ (instancetype)defaultRange { ++ (instancetype)defaultRange NS_RETURNS_RETAINED { return [self new]; } diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.h b/Source/Private/TextExperiment/Component/ASTextLayout.h index e2cd9873e..5de6f057c 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.h +++ b/Source/Private/TextExperiment/Component/ASTextLayout.h @@ -1,12 +1,18 @@ // // ASTextLayout.h -// Modified from YYText +// Texture // -// Created by ibireme on 15/3/3. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -53,13 +59,13 @@ extern const CGSize ASTextContainerMaxSize; @interface ASTextContainer : NSObject /// Creates a container with the specified size. @param size The size. -+ (instancetype)containerWithSize:(CGSize)size; ++ (instancetype)containerWithSize:(CGSize)size NS_RETURNS_RETAINED; /// Creates a container with the specified size and insets. @param size The size. @param insets The text insets. -+ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets; ++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets NS_RETURNS_RETAINED; /// Creates a container with the specified path. @param path The path. -+ (instancetype)containerWithPath:(nullable UIBezierPath *)path; ++ (instancetype)containerWithPath:(nullable UIBezierPath *)path NS_RETURNS_RETAINED; /// The constrained size. (if the size is larger than ASTextContainerMaxSize, it will be clipped) @property CGSize size; diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.m b/Source/Private/TextExperiment/Component/ASTextLayout.m index df8d356f6..3707e3d94 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.m +++ b/Source/Private/TextExperiment/Component/ASTextLayout.m @@ -1,12 +1,18 @@ // // ASTextLayout.m -// Modified from YYText +// Texture // -// Created by ibireme on 15/3/3. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -99,18 +105,18 @@ @implementation ASTextContainer { id _linePositionModifier; } -+ (instancetype)containerWithSize:(CGSize)size { ++ (instancetype)containerWithSize:(CGSize)size NS_RETURNS_RETAINED { return [self containerWithSize:size insets:UIEdgeInsetsZero]; } -+ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets { ++ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets NS_RETURNS_RETAINED { ASTextContainer *one = [self new]; one.size = ASTextClipCGSize(size); one.insets = insets; return one; } -+ (instancetype)containerWithPath:(UIBezierPath *)path { ++ (instancetype)containerWithPath:(UIBezierPath *)path NS_RETURNS_RETAINED { ASTextContainer *one = [self new]; one.path = path; return one; @@ -405,25 +411,9 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri container->_readonly = YES; maximumNumberOfRows = container.maximumNumberOfRows; - // CoreText bug when draw joined emoji since iOS 8.3. - // See -[NSMutableAttributedString setClearColorToJoinedEmoji] for more information. - static BOOL needFixJoinedEmojiBug = NO; // It may use larger constraint size when create CTFrame with // CTFramesetterCreateFrame in iOS 10. - static BOOL needFixLayoutSizeBug = NO; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - double systemVersionDouble = [UIDevice currentDevice].systemVersion.doubleValue; - if (8.3 <= systemVersionDouble && systemVersionDouble < 9) { - needFixJoinedEmojiBug = YES; - } - if (systemVersionDouble >= 10) { - needFixLayoutSizeBug = YES; - } - }); - if (needFixJoinedEmojiBug) { - [((NSMutableAttributedString *)text) as_setClearColorToJoinedEmoji]; - } + BOOL needFixLayoutSizeBug = AS_AT_LEAST_IOS10; layout = [[ASTextLayout alloc] _init]; layout.text = text; diff --git a/Source/Private/TextExperiment/Component/ASTextLine.h b/Source/Private/TextExperiment/Component/ASTextLine.h index 10d2685db..2befb4581 100755 --- a/Source/Private/TextExperiment/Component/ASTextLine.h +++ b/Source/Private/TextExperiment/Component/ASTextLine.h @@ -1,12 +1,18 @@ // // ASTextLine.h -// Modified from YYText +// Texture // -// Created by ibireme on 15/3/10. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -22,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN */ @interface ASTextLine : NSObject -+ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical; ++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical NS_RETURNS_RETAINED; @property (nonatomic) NSUInteger index; ///< line index @property (nonatomic) NSUInteger row; ///< line row @@ -73,7 +79,7 @@ typedef NS_ENUM(NSUInteger, ASTextRunGlyphDrawMode) { @interface ASTextRunGlyphRange : NSObject @property (nonatomic) NSRange glyphRangeInRun; @property (nonatomic) ASTextRunGlyphDrawMode drawMode; -+ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode; ++ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode NS_RETURNS_RETAINED; @end NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Component/ASTextLine.m b/Source/Private/TextExperiment/Component/ASTextLine.m index 14d9f7fd2..a0b8a173d 100755 --- a/Source/Private/TextExperiment/Component/ASTextLine.m +++ b/Source/Private/TextExperiment/Component/ASTextLine.m @@ -1,12 +1,18 @@ // -// ASYTextLine.m -// Modified from YYText +// ASTextLine.m +// Texture // -// Created by ibireme on 15/3/3. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -16,7 +22,7 @@ @implementation ASTextLine { CGFloat _firstGlyphPos; // first glyph position for baseline, typically 0. } -+ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical { ++ (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical NS_RETURNS_RETAINED { if (!CTLine) return nil; ASTextLine *line = [self new]; line->_position = position; @@ -157,7 +163,7 @@ - (NSString *)description { @implementation ASTextRunGlyphRange -+ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode { ++ (instancetype)rangeWithRange:(NSRange)range drawMode:(ASTextRunGlyphDrawMode)mode NS_RETURNS_RETAINED { ASTextRunGlyphRange *one = [self new]; one.glyphRangeInRun = range; one.drawMode = mode; diff --git a/Source/Private/TextExperiment/String/ASTextAttribute.h b/Source/Private/TextExperiment/String/ASTextAttribute.h index 80cdf64ec..87716e9ce 100755 --- a/Source/Private/TextExperiment/String/ASTextAttribute.h +++ b/Source/Private/TextExperiment/String/ASTextAttribute.h @@ -1,12 +1,18 @@ // // ASTextAttribute.h -// Modified from YYText +// Texture // -// Created by ibireme on 14/10/26. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -173,7 +179,7 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR Example: If :) is replace by a custom emoji (such as😊), the backed string can be set to @":)". */ @interface ASTextBackedString : NSObject -+ (instancetype)stringWithString:(nullable NSString *)string; ++ (instancetype)stringWithString:(nullable NSString *)string NS_RETURNS_RETAINED; @property (nullable, nonatomic, copy) NSString *string; ///< backed string @end @@ -188,7 +194,7 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR selection and edit. */ @interface ASTextBinding : NSObject -+ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm; ++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm NS_RETURNS_RETAINED; @property (nonatomic) BOOL deleteConfirm; ///< confirm the range when delete in ASTextView @end @@ -201,7 +207,7 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR It's similar to `NSShadow`, but offers more options. */ @interface ASTextShadow : NSObject -+ (instancetype)shadowWithColor:(nullable UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius; ++ (instancetype)shadowWithColor:(nullable UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius NS_RETURNS_RETAINED; @property (nullable, nonatomic, strong) UIColor *color; ///< shadow color @property (nonatomic) CGSize offset; ///< shadow offset @@ -209,7 +215,7 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR @property (nonatomic) CGBlendMode blendMode; ///< shadow blend mode @property (nullable, nonatomic, strong) ASTextShadow *subShadow; ///< a sub shadow which will be added above the parent shadow -+ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow; ///< convert NSShadow to ASTextShadow ++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow NS_RETURNS_RETAINED; ///< convert NSShadow to ASTextShadow - (NSShadow *)nsShadow; ///< convert ASTextShadow to NSShadow @end @@ -223,8 +229,8 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR when it's used as strikethrough, the line is drawn above text glyphs. */ @interface ASTextDecoration : NSObject -+ (instancetype)decorationWithStyle:(ASTextLineStyle)style; -+ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(nullable NSNumber *)width color:(nullable UIColor *)color; ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style NS_RETURNS_RETAINED; ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(nullable NSNumber *)width color:(nullable UIColor *)color NS_RETURNS_RETAINED; @property (nonatomic) ASTextLineStyle style; ///< line style @property (nullable, nonatomic, strong) NSNumber *width; ///< line width (nil means automatic width) @property (nullable, nonatomic, strong) UIColor *color; ///< line color (nil means automatic color) @@ -246,8 +252,8 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR ╰──────╯ */ @interface ASTextBorder : NSObject -+ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(nullable UIColor *)color; -+ (instancetype)borderWithFillColor:(nullable UIColor *)color cornerRadius:(CGFloat)cornerRadius; ++ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(nullable UIColor *)color NS_RETURNS_RETAINED; ++ (instancetype)borderWithFillColor:(nullable UIColor *)color cornerRadius:(CGFloat)cornerRadius NS_RETURNS_RETAINED; @property (nonatomic) ASTextLineStyle lineStyle; ///< border line style @property (nonatomic) CGFloat strokeWidth; ///< border line width @property (nullable, nonatomic, strong) UIColor *strokeColor; ///< border line color @@ -270,7 +276,7 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR then it will be added to the text container's view or layer. */ @interface ASTextAttachment : NSObject -+ (instancetype)attachmentWithContent:(nullable id)content; ++ (instancetype)attachmentWithContent:(nullable id)content NS_RETURNS_RETAINED; @property (nullable, nonatomic, strong) id content; ///< Supported type: UIImage, UIView, CALayer @property (nonatomic) UIViewContentMode contentMode; ///< Content display mode. @property (nonatomic) UIEdgeInsets contentInsets; ///< The insets when drawing content. @@ -303,14 +309,14 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR @param attributes The attributes which will replace original attributes when highlight, If the value is NSNull, it will removed when highlight. */ -+ (instancetype)highlightWithAttributes:(nullable NSDictionary *)attributes; ++ (instancetype)highlightWithAttributes:(nullable NSDictionary *)attributes NS_RETURNS_RETAINED; /** Convenience methods to create a default highlight with the specifeid background color. @param color The background border color. */ -+ (instancetype)highlightWithBackgroundColor:(nullable UIColor *)color; ++ (instancetype)highlightWithBackgroundColor:(nullable UIColor *)color NS_RETURNS_RETAINED; // Convenience methods below to set the `attributes`. - (void)setFont:(nullable UIFont *)font; diff --git a/Source/Private/TextExperiment/String/ASTextAttribute.m b/Source/Private/TextExperiment/String/ASTextAttribute.m index 084bdcf82..f5bb64224 100755 --- a/Source/Private/TextExperiment/String/ASTextAttribute.m +++ b/Source/Private/TextExperiment/String/ASTextAttribute.m @@ -1,12 +1,18 @@ // // ASTextAttribute.m -// Modified from YYText +// Texture // -// Created by ibireme on 14/10/26. -// Copyright (c) 2015 ibireme. +// 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. // -// This source code is licensed under the MIT-style license found in the -// LICENSE file in the root directory of this source tree. +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 "ASTextAttribute.h" @@ -63,7 +69,12 @@ ASTextAttributeType ASTextAttributeGetType(NSString *name){ dic[(id)kCTSuperscriptAttributeName] = UIKit; //it's a CoreText attrubite, but only supported by UIKit... dic[NSVerticalGlyphFormAttributeName] = All; dic[(id)kCTGlyphInfoAttributeName] = CoreText_ASText; +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" dic[(id)kCTCharacterShapeAttributeName] = CoreText_ASText; +#pragma clang diagnostic pop +#endif dic[(id)kCTRunDelegateAttributeName] = CoreText_ASText; dic[(id)kCTBaselineClassAttributeName] = CoreText_ASText; dic[(id)kCTBaselineInfoAttributeName] = CoreText_ASText; @@ -104,7 +115,7 @@ ASTextAttributeType ASTextAttributeGetType(NSString *name){ @implementation ASTextBackedString -+ (instancetype)stringWithString:(NSString *)string { ++ (instancetype)stringWithString:(NSString *)string NS_RETURNS_RETAINED { ASTextBackedString *one = [self new]; one.string = string; return one; @@ -131,7 +142,7 @@ - (id)copyWithZone:(NSZone *)zone { @implementation ASTextBinding -+ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm { ++ (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm NS_RETURNS_RETAINED { ASTextBinding *one = [self new]; one.deleteConfirm = deleteConfirm; return one; @@ -158,7 +169,7 @@ - (id)copyWithZone:(NSZone *)zone { @implementation ASTextShadow -+ (instancetype)shadowWithColor:(UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius { ++ (instancetype)shadowWithColor:(UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius NS_RETURNS_RETAINED { ASTextShadow *one = [self new]; one.color = color; one.offset = offset; @@ -166,7 +177,7 @@ + (instancetype)shadowWithColor:(UIColor *)color offset:(CGSize)offset radius:(C return one; } -+ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow { ++ (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow NS_RETURNS_RETAINED { if (!nsShadow) return nil; ASTextShadow *shadow = [self new]; shadow.offset = nsShadow.shadowOffset; @@ -227,12 +238,12 @@ - (instancetype)init { return self; } -+ (instancetype)decorationWithStyle:(ASTextLineStyle)style { ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style NS_RETURNS_RETAINED { ASTextDecoration *one = [self new]; one.style = style; return one; } -+ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(NSNumber *)width color:(UIColor *)color { ++ (instancetype)decorationWithStyle:(ASTextLineStyle)style width:(NSNumber *)width color:(UIColor *)color NS_RETURNS_RETAINED { ASTextDecoration *one = [self new]; one.style = style; one.width = width; @@ -267,7 +278,7 @@ - (id)copyWithZone:(NSZone *)zone { @implementation ASTextBorder -+ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(UIColor *)color { ++ (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(UIColor *)color NS_RETURNS_RETAINED { ASTextBorder *one = [self new]; one.lineStyle = lineStyle; one.strokeWidth = width; @@ -275,7 +286,7 @@ + (instancetype)borderWithLineStyle:(ASTextLineStyle)lineStyle lineWidth:(CGFloa return one; } -+ (instancetype)borderWithFillColor:(UIColor *)color cornerRadius:(CGFloat)cornerRadius { ++ (instancetype)borderWithFillColor:(UIColor *)color cornerRadius:(CGFloat)cornerRadius NS_RETURNS_RETAINED { ASTextBorder *one = [self new]; one.fillColor = color; one.cornerRadius = cornerRadius; @@ -331,7 +342,7 @@ - (id)copyWithZone:(NSZone *)zone { @implementation ASTextAttachment -+ (instancetype)attachmentWithContent:(id)content { ++ (instancetype)attachmentWithContent:(id)content NS_RETURNS_RETAINED { ASTextAttachment *one = [self new]; one.content = content; return one; @@ -368,13 +379,13 @@ - (id)copyWithZone:(NSZone *)zone { @implementation ASTextHighlight -+ (instancetype)highlightWithAttributes:(NSDictionary *)attributes { ++ (instancetype)highlightWithAttributes:(NSDictionary *)attributes NS_RETURNS_RETAINED { ASTextHighlight *one = [self new]; one.attributes = attributes; return one; } -+ (instancetype)highlightWithBackgroundColor:(UIColor *)color { ++ (instancetype)highlightWithBackgroundColor:(UIColor *)color NS_RETURNS_RETAINED { ASTextBorder *highlightBorder = [ASTextBorder new]; highlightBorder.insets = UIEdgeInsetsMake(-2, -1, -2, -1); highlightBorder.cornerRadius = 3; diff --git a/Source/Private/TextExperiment/Utility/ASTextUtilities.h b/Source/Private/TextExperiment/Utility/ASTextUtilities.h index c434cc901..024fb269c 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/TextExperiment/Utility/NSAttributedString+ASText.h b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h index 9fc8ec222..f7734347d 100755 --- a/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h +++ b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.h @@ -1270,7 +1270,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)as_setSuperscript:(nullable NSNumber *)superscript range:(NSRange)range; - (void)as_setGlyphInfo:(nullable CTGlyphInfoRef)glyphInfo range:(NSRange)range; -- (void)as_setCharacterShape:(nullable NSNumber *)characterShape range:(NSRange)range; +- (void)as_setCharacterShape:(nullable NSNumber *)characterShape range:(NSRange)range __TVOS_PROHIBITED; - (void)as_setRunDelegate:(nullable CTRunDelegateRef)runDelegate range:(NSRange)range; - (void)as_setBaselineClass:(nullable CFStringRef)baselineClass range:(NSRange)range; - (void)as_setBaselineInfo:(nullable CFDictionaryRef)baselineInfo range:(NSRange)range; @@ -1357,21 +1357,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)as_appendString:(NSString *)string; -/** - Set foreground color with [UIColor clearColor] in joined-emoji range. - Emoji drawing will not be affected by the foreground color. - - @discussion In iOS 8.3, Apple releases some new diversified emojis. - There's some single emoji which can be assembled to a new 'joined-emoji'. - The joiner is unicode character 'ZERO WIDTH JOINER' (U+200D). - For example: 👨👩👧👧 -> 👨‍👩‍👧‍👧. - - When there are more than 5 'joined-emoji' in a same CTLine, CoreText may render some - extra glyphs above the emoji. It's a bug in CoreText, try this method to avoid. - This bug is fixed in iOS 9. - */ -- (void)as_setClearColorToJoinedEmoji; - /** Removes all discontinuous attributes in a specified range. See `allDiscontinuousAttributeKeys`. diff --git a/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m index d67dd15d2..1b05bbf73 100755 --- a/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m +++ b/Source/Private/TextExperiment/Utility/NSAttributedString+ASText.m @@ -600,7 +600,12 @@ - (BOOL)as_canDrawWithUIKit { dispatch_once(&onceToken, ^{ failSet = [NSMutableSet new]; [failSet addObject:(id)kCTGlyphInfoAttributeName]; +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [failSet addObject:(id)kCTCharacterShapeAttributeName]; +#pragma clang diagnostic pop +#endif [failSet addObject:(id)kCTLanguageAttributeName]; [failSet addObject:(id)kCTRunDelegateAttributeName]; [failSet addObject:(id)kCTBaselineClassAttributeName]; @@ -1049,7 +1054,10 @@ - (void)as_setGlyphInfo:(CTGlyphInfoRef)glyphInfo range:(NSRange)range { } - (void)as_setCharacterShape:(NSNumber *)characterShape range:(NSRange)range { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self as_setAttribute:(id)kCTCharacterShapeAttributeName value:characterShape range:range]; +#pragma clang diagnostic pop } - (void)as_setRunDelegate:(CTRunDelegateRef)runDelegate range:(NSRange)range { @@ -1178,51 +1186,6 @@ - (void)as_appendString:(NSString *)string { [self as_removeDiscontinuousAttributesInRange:NSMakeRange(length, string.length)]; } -- (void)as_setClearColorToJoinedEmoji { - NSString *str = self.string; - if (str.length < 8) return; - - // Most string do not contains the joined-emoji, test the joiner first. - BOOL containsJoiner = NO; - { - CFStringRef cfStr = (__bridge CFStringRef)str; - BOOL needFree = NO; - UniChar *chars = NULL; - chars = (UniChar *)CFStringGetCharactersPtr(cfStr); - if (!chars) { - chars = (UniChar *)malloc(str.length * sizeof(UniChar)); - if (chars) { - needFree = YES; - CFStringGetCharacters(cfStr, CFRangeMake(0, str.length), chars); - } - } - if (!chars) { // fail to get unichar.. - containsJoiner = YES; - } else { - for (int i = 0, max = (int)str.length; i < max; i++) { - if (chars[i] == 0x200D) { // 'ZERO WIDTH JOINER' (U+200D) - containsJoiner = YES; - break; - } - } - if (needFree) free(chars); - } - } - if (!containsJoiner) return; - - // NSRegularExpression is designed to be immutable and thread safe. - static NSRegularExpression *regex; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - regex = [NSRegularExpression regularExpressionWithPattern:@"((👨‍👩‍👧‍👦|👨‍👩‍👦‍👦|👨‍👩‍👧‍👧|👩‍👩‍👧‍👦|👩‍👩‍👦‍👦|👩‍👩‍👧‍👧|👨‍👨‍👧‍👦|👨‍👨‍👦‍👦|👨‍👨‍👧‍👧)+|(👨‍👩‍👧|👩‍👩‍👦|👩‍👩‍👧|👨‍👨‍👦|👨‍👨‍👧))" options:kNilOptions error:nil]; - }); - - UIColor *clear = [UIColor clearColor]; - [regex enumerateMatchesInString:str options:kNilOptions range:NSMakeRange(0, str.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { - [self as_setColor:clear range:result.range]; - }]; -} - - (void)as_removeDiscontinuousAttributesInRange:(NSRange)range { NSArray *keys = [NSMutableAttributedString as_allDiscontinuousAttributeKeys]; for (NSString *key in keys) { diff --git a/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m b/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m index 3401fcc82..2374e4021 100755 --- a/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m +++ b/Source/Private/TextExperiment/Utility/NSParagraphStyle+ASText.m @@ -25,6 +25,7 @@ + (NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle { NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; +#if TARGET_OS_IOS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CGFloat lineSpacing; @@ -32,6 +33,7 @@ + (NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle { style.lineSpacing = lineSpacing; } #pragma clang diagnostic pop +#endif CGFloat paragraphSpacing; if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), ¶graphSpacing)) { @@ -114,6 +116,7 @@ - (CTParagraphStyleRef)as_CTStyle CF_RETURNS_RETAINED { CTParagraphStyleSetting set[kCTParagraphStyleSpecifierCount] = { }; int count = 0; +#if TARGET_OS_IOS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CGFloat lineSpacing = self.lineSpacing; @@ -122,6 +125,7 @@ - (CTParagraphStyleRef)as_CTStyle CF_RETURNS_RETAINED { set[count].value = &lineSpacing; count++; #pragma clang diagnostic pop +#endif CGFloat paragraphSpacing = self.paragraphSpacing; set[count].spec = kCTParagraphStyleSpecifierParagraphSpacing; diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index daeb4d6e6..a87707bb6 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -91,6 +91,9 @@ int setAccessibilityActivationPoint:1; int setAccessibilityPath:1; int setSemanticContentAttribute:1; + int setLayoutMargins:1; + int setPreservesSuperviewLayoutMargins:1; + int setInsetsLayoutMarginsFromSafeArea:1; } ASPendingStateFlags; @implementation _ASPendingState @@ -123,6 +126,9 @@ @implementation _ASPendingState CGFloat borderWidth; CGColorRef borderColor; BOOL asyncTransactionContainer; + UIEdgeInsets layoutMargins; + BOOL preservesSuperviewLayoutMargins; + BOOL insetsLayoutMarginsFromSafeArea; BOOL isAccessibilityElement; NSString *accessibilityLabel; NSAttributedString *accessibilityAttributedLabel; @@ -141,7 +147,7 @@ @implementation _ASPendingState NSArray *accessibilityHeaderElements; CGPoint accessibilityActivationPoint; UIBezierPath *accessibilityPath; - UISemanticContentAttribute semanticContentAttribute; + UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); ASPendingStateFlags _flags; } @@ -208,23 +214,12 @@ ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *sta @synthesize borderColor=borderColor; @synthesize asyncdisplaykit_asyncTransactionContainer=asyncTransactionContainer; @synthesize semanticContentAttribute=semanticContentAttribute; - +@synthesize layoutMargins=layoutMargins; +@synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins; +@synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea; static CGColorRef blackColorRef = NULL; static UIColor *defaultTintColor = nil; -static BOOL defaultAllowsGroupOpacity = YES; -static BOOL defaultAllowsEdgeAntialiasing = NO; - -+ (void)load -{ - // Create temporary view to read default values that are based on linked SDK and Info.plist values - // Ensure this values cached on the main thread before needed - ASDisplayNodeCAssertMainThread(); - UIView *view = [[UIView alloc] init]; - defaultAllowsGroupOpacity = view.layer.allowsGroupOpacity; - defaultAllowsEdgeAntialiasing = view.layer.allowsEdgeAntialiasing; -} - - (instancetype)init { @@ -251,8 +246,8 @@ - (instancetype)init tintColor = defaultTintColor; isHidden = NO; needsDisplayOnBoundsChange = NO; - allowsGroupOpacity = defaultAllowsGroupOpacity; - allowsEdgeAntialiasing = defaultAllowsEdgeAntialiasing; + allowsGroupOpacity = ASDefaultAllowsGroupOpacity(); + allowsEdgeAntialiasing = ASDefaultAllowsEdgeAntialiasing(); autoresizesSubviews = YES; alpha = 1.0f; cornerRadius = 0.0f; @@ -276,6 +271,9 @@ - (instancetype)init shadowRadius = 3; borderWidth = 0; borderColor = blackColorRef; + layoutMargins = UIEdgeInsetsMake(8, 8, 8, 8); + preservesSuperviewLayoutMargins = NO; + insetsLayoutMarginsFromSafeArea = YES; isAccessibilityElement = NO; accessibilityLabel = nil; accessibilityAttributedLabel = nil; @@ -573,7 +571,25 @@ - (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)flag _flags.setAsyncTransactionContainer = YES; } -- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute { +- (void)setLayoutMargins:(UIEdgeInsets)margins +{ + layoutMargins = margins; + _flags.setLayoutMargins = YES; +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)flag +{ + preservesSuperviewLayoutMargins = flag; + _flags.setPreservesSuperviewLayoutMargins = YES; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)flag +{ + insetsLayoutMarginsFromSafeArea = flag; + _flags.setInsetsLayoutMarginsFromSafeArea = YES; +} + +- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AVAILABLE(ios(9.0), tvos(9.0)) { semanticContentAttribute = attribute; _flags.setSemanticContentAttribute = YES; } @@ -1049,6 +1065,18 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr if (flags.setOpaque) ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + if (flags.setLayoutMargins) + view.layoutMargins = layoutMargins; + + if (flags.setPreservesSuperviewLayoutMargins) + view.preservesSuperviewLayoutMargins = preservesSuperviewLayoutMargins; + + if (AS_AVAILABLE_IOS(11.0)) { + if (flags.setInsetsLayoutMarginsFromSafeArea) { + view.insetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + } + } + if (flags.setSemanticContentAttribute) { view.semanticContentAttribute = semanticContentAttribute; } @@ -1212,15 +1240,22 @@ + (_ASPendingState *)pendingViewStateFromView:(UIView *)view pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; pendingState.semanticContentAttribute = view.semanticContentAttribute; + pendingState.layoutMargins = view.layoutMargins; + pendingState.preservesSuperviewLayoutMargins = view.preservesSuperviewLayoutMargins; + if (AS_AVAILABLE_IOS(11)) { + pendingState.insetsLayoutMarginsFromSafeArea = view.insetsLayoutMarginsFromSafeArea; + } pendingState.isAccessibilityElement = view.isAccessibilityElement; pendingState.accessibilityLabel = view.accessibilityLabel; pendingState.accessibilityHint = view.accessibilityHint; pendingState.accessibilityValue = view.accessibilityValue; - if (@available(iOS 11, *)) { - pendingState.accessibilityAttributedLabel = [view valueForKey: @"accessibilityAttributedLabel"]; - pendingState.accessibilityAttributedHint = [view valueForKey: @"accessibilityAttributedHint"]; - pendingState.accessibilityAttributedValue = [view valueForKey: @"accessibilityAttributedValue"]; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 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; @@ -1296,6 +1331,9 @@ - (BOOL)hasChanges || flags.setAsyncTransactionContainer || flags.setOpaque || flags.setSemanticContentAttribute + || flags.setLayoutMargins + || flags.setPreservesSuperviewLayoutMargins + || flags.setInsetsLayoutMarginsFromSafeArea || flags.setIsAccessibilityElement || flags.setAccessibilityLabel || flags.setAccessibilityAttributedLabel diff --git a/Source/TextKit/ASTextKitComponents.h b/Source/TextKit/ASTextKitComponents.h index 223abf5c1..c516fe69e 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 @@ -31,7 +37,7 @@ AS_SUBCLASSING_RESTRICTED @discussion The returned components will be hooked up together, so they are ready for use as a system upon return. */ + (instancetype)componentsWithAttributedSeedString:(nullable NSAttributedString *)attributedSeedString - textContainerSize:(CGSize)textContainerSize; + textContainerSize:(CGSize)textContainerSize NS_RETURNS_RETAINED; /** @abstract Creates the stack of TextKit components. @@ -43,7 +49,7 @@ AS_SUBCLASSING_RESTRICTED */ + (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage textContainerSize:(CGSize)textContainerSize - layoutManager:(NSLayoutManager *)layoutManager; + layoutManager:(NSLayoutManager *)layoutManager NS_RETURNS_RETAINED; /** @abstract Returns the bounding size for the text view's text. @@ -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 (nonatomic, strong, nullable) UITextView *textView; +@property (nonatomic, strong, nullable) ASTextKitComponentsTextView *textView; @end diff --git a/Source/TextKit/ASTextKitComponents.mm b/Source/TextKit/ASTextKitComponents.mm index 6b3955eb7..dbb611b78 100644 --- a/Source/TextKit/ASTextKitComponents.mm +++ b/Source/TextKit/ASTextKitComponents.mm @@ -20,6 +20,37 @@ #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 @@ -27,9 +58,6 @@ @interface ASTextKitComponents () @property (nonatomic, strong, readwrite) NSTextContainer *textContainer; @property (nonatomic, strong, readwrite) NSLayoutManager *layoutManager; -// Indicates whether or not this object must be deallocated on main thread. Defaults to YES. -@property (nonatomic, assign) BOOL requiresMainThreadDeallocation; - @end @implementation ASTextKitComponents @@ -37,7 +65,7 @@ @implementation ASTextKitComponents #pragma mark - Class + (instancetype)componentsWithAttributedSeedString:(NSAttributedString *)attributedSeedString - textContainerSize:(CGSize)textContainerSize + textContainerSize:(CGSize)textContainerSize NS_RETURNS_RETAINED { NSTextStorage *textStorage = attributedSeedString ? [[NSTextStorage alloc] initWithAttributedString:attributedSeedString] : [[NSTextStorage alloc] init]; @@ -48,7 +76,7 @@ + (instancetype)componentsWithAttributedSeedString:(NSAttributedString *)attribu + (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage textContainerSize:(CGSize)textContainerSize - layoutManager:(NSLayoutManager *)layoutManager + layoutManager:(NSLayoutManager *)layoutManager NS_RETURNS_RETAINED { ASTextKitComponents *components = [[self alloc] init]; @@ -61,8 +89,6 @@ + (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. [components.layoutManager addTextContainer:components.textContainer]; - components.requiresMainThreadDeallocation = YES; - return components; } @@ -70,11 +96,9 @@ + (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage - (void)dealloc { - if (_requiresMainThreadDeallocation) { - ASDisplayNodeAssertMainThread(); - } // Nil out all delegates to prevent crash if (_textView) { + ASDisplayNodeAssertMainThread(); _textView.delegate = nil; } _layoutManager.delegate = nil; @@ -88,10 +112,8 @@ - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth // 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)]; - // The temporary stack can be deallocated off main - components.requiresMainThreadDeallocation = NO; } // Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:). @@ -112,9 +134,7 @@ - (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth // Always use temporary stack in case of threading issues components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; - // The temporary stack can be deallocated off main - components.requiresMainThreadDeallocation = NO; - + // 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/ASTextKitCoreTextAdditions.h b/Source/TextKit/ASTextKitCoreTextAdditions.h index 94108961c..211250e8c 100644 --- a/Source/TextKit/ASTextKitCoreTextAdditions.h +++ b/Source/TextKit/ASTextKitCoreTextAdditions.h @@ -88,7 +88,7 @@ ASDISPLAYNODE_EXTERN_C_END @result An NSParagraphStyle initialized with as many of the paragraph specifiers from `coreTextParagraphStyle` as possible. */ -+ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle; ++ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle NS_RETURNS_RETAINED; @end diff --git a/Source/TextKit/ASTextKitCoreTextAdditions.m b/Source/TextKit/ASTextKitCoreTextAdditions.m index 05935db5c..272cacd3d 100644 --- a/Source/TextKit/ASTextKitCoreTextAdditions.m +++ b/Source/TextKit/ASTextKitCoreTextAdditions.m @@ -39,6 +39,7 @@ BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName) kCTBaselineInfoAttributeName, kCTBaselineReferenceInfoAttributeName, kCTUnderlineColorAttributeName, + kCTParagraphStyleAttributeName, nil]; }); return [coreTextAttributes containsObject:attributeName]; @@ -97,8 +98,13 @@ BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName) cleanAttributes[NSForegroundColorAttributeName] = [UIColor colorWithCGColor:(CGColorRef)coreTextValue]; } // kCTParagraphStyleAttributeName -> NSParagraphStyleAttributeName - else if ([coreTextKey isEqualToString:(NSString *)kCTParagraphStyleAttributeName] && ![coreTextValue isKindOfClass:[NSParagraphStyle class]]) { - cleanAttributes[NSParagraphStyleAttributeName] = [NSParagraphStyle paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextValue]; + else if ([coreTextKey isEqualToString:(NSString *)kCTParagraphStyleAttributeName]) { + if ([coreTextValue isKindOfClass:[NSParagraphStyle class]]) { + cleanAttributes[NSParagraphStyleAttributeName] = (NSParagraphStyle *)coreTextValue; + } + else { + cleanAttributes[NSParagraphStyleAttributeName] = [NSParagraphStyle paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextValue]; + } } // kCTStrokeWidthAttributeName -> NSStrokeWidthAttributeName else if ([coreTextKey isEqualToString:(NSString *)kCTStrokeWidthAttributeName]) { @@ -166,12 +172,13 @@ BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName) #pragma mark - @implementation NSParagraphStyle (ASTextKitCoreTextAdditions) -+ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle; ++ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle NS_RETURNS_RETAINED { NSMutableParagraphStyle *newParagraphStyle = [[NSMutableParagraphStyle alloc] init]; - if (!coreTextParagraphStyle) + if (!coreTextParagraphStyle) { return newParagraphStyle; + } // The following paragraph style specifiers are not supported on NSParagraphStyle. Should they become available, we should add them. /* @@ -190,67 +197,145 @@ + (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreText // kCTParagraphStyleSpecifierAlignment -> alignment CTTextAlignment coreTextAlignment; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierAlignment, sizeof(coreTextAlignment), &coreTextAlignment)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierAlignment, + sizeof(coreTextAlignment), + &coreTextAlignment)) { newParagraphStyle.alignment = NSTextAlignmentFromCTTextAlignment(coreTextAlignment); + } // kCTParagraphStyleSpecifierFirstLineHeadIndent -> firstLineHeadIndent CGFloat firstLineHeadIndent; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(firstLineHeadIndent), &firstLineHeadIndent)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierFirstLineHeadIndent, + sizeof(firstLineHeadIndent), + &firstLineHeadIndent)) { newParagraphStyle.firstLineHeadIndent = firstLineHeadIndent; + } // kCTParagraphStyleSpecifierHeadIndent -> headIndent CGFloat headIndent; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(headIndent), &headIndent)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierHeadIndent, + sizeof(headIndent), + &headIndent)) { newParagraphStyle.headIndent = headIndent; + } // kCTParagraphStyleSpecifierTailIndent -> tailIndent CGFloat tailIndent; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(tailIndent), &tailIndent)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierTailIndent, + sizeof(tailIndent), + &tailIndent)) { newParagraphStyle.tailIndent = tailIndent; + } // kCTParagraphStyleSpecifierLineBreakMode -> lineBreakMode CTLineBreakMode coreTextLineBreakMode; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineBreakMode, sizeof(coreTextLineBreakMode), &coreTextLineBreakMode)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineBreakMode, + sizeof(coreTextLineBreakMode), + &coreTextLineBreakMode)) { newParagraphStyle.lineBreakMode = (NSLineBreakMode)coreTextLineBreakMode; // They're the same enum. + } // kCTParagraphStyleSpecifierLineHeightMultiple -> lineHeightMultiple CGFloat lineHeightMultiple; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(lineHeightMultiple), &lineHeightMultiple)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineHeightMultiple, + sizeof(lineHeightMultiple), + &lineHeightMultiple)) { newParagraphStyle.lineHeightMultiple = lineHeightMultiple; + } // kCTParagraphStyleSpecifierMaximumLineHeight -> maximumLineHeight CGFloat maximumLineHeight; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(maximumLineHeight), &maximumLineHeight)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMaximumLineHeight, + sizeof(maximumLineHeight), + &maximumLineHeight)) { newParagraphStyle.maximumLineHeight = maximumLineHeight; + } // kCTParagraphStyleSpecifierMinimumLineHeight -> minimumLineHeight CGFloat minimumLineHeight; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(minimumLineHeight), &minimumLineHeight)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMinimumLineHeight, + sizeof(minimumLineHeight), + &minimumLineHeight)) { newParagraphStyle.minimumLineHeight = minimumLineHeight; - - // kCTParagraphStyleSpecifierLineSpacing -> lineSpacing - // Note that kCTParagraphStyleSpecifierLineSpacing is deprecated and will die soon. We should not be using it. + } + + CGFloat lineSpacing = 0; +#if TARGET_OS_IOS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - CGFloat lineSpacing; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(lineSpacing), &lineSpacing)) - newParagraphStyle.lineSpacing = lineSpacing; + // kCTParagraphStyleSpecifierLineSpacing -> lineSpacing + // Note that kCTParagraphStyleSpecifierLineSpacing is deprecated and will die soon. We should not be using it. + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineSpacing, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } #pragma clang diagnostic pop +#endif + + // Attempt to weakly map the following onto -[NSParagraphStyle lineSpacing]: + // - kCTParagraphStyleSpecifierMinimumLineSpacing + // - kCTParagraphStyleSpecifierMaximumLineSpacing + // - kCTParagraphStyleSpecifierLineSpacingAdjustment + if (fabs(lineSpacing) <= FLT_EPSILON && + CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMinimumLineSpacing, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } + + if (fabs(lineSpacing) <= FLT_EPSILON && + CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierMaximumLineSpacing, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } + + if (fabs(lineSpacing) <= FLT_EPSILON && + CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierLineSpacingAdjustment, + sizeof(lineSpacing), + &lineSpacing)) { + newParagraphStyle.lineSpacing = lineSpacing; + } // kCTParagraphStyleSpecifierParagraphSpacing -> paragraphSpacing CGFloat paragraphSpacing; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(paragraphSpacing), ¶graphSpacing)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierParagraphSpacing, + sizeof(paragraphSpacing), + ¶graphSpacing)) { newParagraphStyle.paragraphSpacing = paragraphSpacing; + } // kCTParagraphStyleSpecifierParagraphSpacingBefore -> paragraphSpacingBefore CGFloat paragraphSpacingBefore; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(paragraphSpacingBefore), ¶graphSpacingBefore)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierParagraphSpacingBefore, + sizeof(paragraphSpacingBefore), + ¶graphSpacingBefore)) { newParagraphStyle.paragraphSpacingBefore = paragraphSpacingBefore; + } // kCTParagraphStyleSpecifierBaseWritingDirection -> baseWritingDirection CTWritingDirection coreTextBaseWritingDirection; - if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(coreTextBaseWritingDirection), &coreTextBaseWritingDirection)) + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, + kCTParagraphStyleSpecifierBaseWritingDirection, + sizeof(coreTextBaseWritingDirection), + &coreTextBaseWritingDirection)) { newParagraphStyle.baseWritingDirection = (NSWritingDirection)coreTextBaseWritingDirection; // They're the same enum. + } return newParagraphStyle; } diff --git a/Source/UIImage+ASConvenience.h b/Source/UIImage+ASConvenience.h index e9612c8b7..915571b7c 100644 --- a/Source/UIImage+ASConvenience.h +++ b/Source/UIImage+ASConvenience.h @@ -40,7 +40,7 @@ NS_ASSUME_NONNULL_BEGIN * @param imageName The name of the image to load * @return The loaded image or nil */ -+ (UIImage *)as_imageNamed:(NSString *)imageName; ++ (UIImage *)as_imageNamed:(NSString *)imageName NS_RETURNS_RETAINED; /** * A version of imageNamed that caches results because loading an image is expensive. @@ -51,7 +51,7 @@ NS_ASSUME_NONNULL_BEGIN * @param traitCollection The traits associated with the intended environment for the image. * @return The loaded image or nil */ -+ (UIImage *)as_imageNamed:(NSString *)imageName compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection; ++ (UIImage *)as_imageNamed:(NSString *)imageName compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection NS_RETURNS_RETAINED; @end @@ -76,7 +76,7 @@ NS_ASSUME_NONNULL_BEGIN */ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(nullable UIColor *)cornerColor - fillColor:(UIColor *)fillColor AS_WARN_UNUSED_RESULT; + fillColor:(UIColor *)fillColor NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** * This generates a flat-color, rounded-corner resizeable image with a border @@ -91,7 +91,7 @@ NS_ASSUME_NONNULL_BEGIN cornerColor:(UIColor *)cornerColor fillColor:(UIColor *)fillColor borderColor:(nullable UIColor *)borderColor - borderWidth:(CGFloat)borderWidth AS_WARN_UNUSED_RESULT; + borderWidth:(CGFloat)borderWidth NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; /** * This generates a flat-color, rounded-corner resizeable image with a border @@ -110,7 +110,7 @@ NS_ASSUME_NONNULL_BEGIN borderColor:(nullable UIColor *)borderColor borderWidth:(CGFloat)borderWidth roundedCorners:(UIRectCorner)roundedCorners - scale:(CGFloat)scale AS_WARN_UNUSED_RESULT; + scale:(CGFloat)scale NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; @end diff --git a/Source/UIImage+ASConvenience.m b/Source/UIImage+ASConvenience.m index efc1e3536..c1d0751e3 100644 --- a/Source/UIImage+ASConvenience.m +++ b/Source/UIImage+ASConvenience.m @@ -16,6 +16,7 @@ // #import +#import #import #import @@ -23,7 +24,7 @@ @implementation UIImage (ASDKFastImageNamed) -UIImage *cachedImageNamed(NSString *imageName, UITraitCollection *traitCollection) +UIImage *cachedImageNamed(NSString *imageName, UITraitCollection *traitCollection) NS_RETURNS_RETAINED { static NSCache *imageCache = nil; static dispatch_once_t onceToken; @@ -54,12 +55,12 @@ @implementation UIImage (ASDKFastImageNamed) return image; } -+ (UIImage *)as_imageNamed:(NSString *)imageName ++ (UIImage *)as_imageNamed:(NSString *)imageName NS_RETURNS_RETAINED { return cachedImageNamed(imageName, nil); } -+ (UIImage *)as_imageNamed:(NSString *)imageName compatibleWithTraitCollection:(UITraitCollection *)traitCollection ++ (UIImage *)as_imageNamed:(NSString *)imageName compatibleWithTraitCollection:(UITraitCollection *)traitCollection NS_RETURNS_RETAINED { return cachedImageNamed(imageName, traitCollection); } @@ -72,7 +73,7 @@ @implementation UIImage (ASDKResizableRoundedRects) + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(UIColor *)cornerColor - fillColor:(UIColor *)fillColor + fillColor:(UIColor *)fillColor NS_RETURNS_RETAINED { return [self as_resizableRoundedImageWithCornerRadius:cornerRadius cornerColor:cornerColor @@ -87,7 +88,7 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(UIColor *)cornerColor fillColor:(UIColor *)fillColor borderColor:(UIColor *)borderColor - borderWidth:(CGFloat)borderWidth + borderWidth:(CGFloat)borderWidth NS_RETURNS_RETAINED { return [self as_resizableRoundedImageWithCornerRadius:cornerRadius cornerColor:cornerColor @@ -104,7 +105,7 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius borderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth roundedCorners:(UIRectCorner)roundedCorners - scale:(CGFloat)scale + scale:(CGFloat)scale NS_RETURNS_RETAINED { static NSCache *__pathCache = nil; static dispatch_once_t onceToken; @@ -138,7 +139,7 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius // We should probably check if the background color has any alpha component but that // might be expensive due to needing to check mulitple color spaces. - UIGraphicsBeginImageContextWithOptions(bounds.size, cornerColor != nil, scale); + ASGraphicsBeginImageContextWithOptions(bounds.size, cornerColor != nil, scale); BOOL contextIsClean = YES; if (cornerColor) { @@ -168,8 +169,7 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius [strokePath strokeWithBlendMode:(canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal) alpha:1]; } - UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIImage *result = ASGraphicsGetImageAndEndCurrentContext(); UIEdgeInsets capInsets = UIEdgeInsetsMake(cornerRadius, cornerRadius, cornerRadius, cornerRadius); result = [result resizableImageWithCapInsets:capInsets resizingMode:UIImageResizingModeStretch]; diff --git a/Source/_ASTransitionContext.h b/Source/_ASTransitionContext.h index 2a8a6f14f..68eae7daa 100644 --- a/Source/_ASTransitionContext.h +++ b/Source/_ASTransitionContext.h @@ -53,5 +53,5 @@ @interface _ASAnimatedTransitionContext : NSObject @property (nonatomic, strong, readonly) ASDisplayNode *node; @property (nonatomic, assign, readonly) CGFloat alpha; -+ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alphaValue; ++ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alphaValue NS_RETURNS_RETAINED; @end diff --git a/Source/_ASTransitionContext.m b/Source/_ASTransitionContext.m index da6350a4a..169e11eaf 100644 --- a/Source/_ASTransitionContext.m +++ b/Source/_ASTransitionContext.m @@ -101,7 +101,7 @@ @interface _ASAnimatedTransitionContext () @implementation _ASAnimatedTransitionContext -+ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alpha ++ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alpha NS_RETURNS_RETAINED { _ASAnimatedTransitionContext *context = [[_ASAnimatedTransitionContext alloc] init]; context.node = node; diff --git a/Source/tvOS/ASControlNode+tvOS.m b/Source/tvOS/ASControlNode+tvOS.m index 35acbc045..77e541862 100644 --- a/Source/tvOS/ASControlNode+tvOS.m +++ b/Source/tvOS/ASControlNode+tvOS.m @@ -16,20 +16,20 @@ // #import - #if TARGET_OS_TV -#import +#import +#import @implementation ASControlNode (tvOS) #pragma mark - tvOS -- (void)pressDown +- (void)_pressDown { - [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationCurveLinear animations:^{ + [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ [self setPressedState]; } completion:^(BOOL finished) { if (finished) { - [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationCurveLinear animations:^{ + [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ [self setFocusedState]; } completion:nil]; } diff --git a/Source/tvOS/ASImageNode+tvOS.h b/Source/tvOS/ASImageNode+tvOS.h deleted file mode 100644 index d808db10a..000000000 --- a/Source/tvOS/ASImageNode+tvOS.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// ASImageNode+tvOS.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 -// - -#if TARGET_OS_TV -#import - -@interface ASImageNode (tvOS) -@end -#endif - diff --git a/Source/tvOS/ASImageNode+tvOS.m b/Source/tvOS/ASImageNode+tvOS.m index d3da889c5..9482fdc8c 100644 --- a/Source/tvOS/ASImageNode+tvOS.m +++ b/Source/tvOS/ASImageNode+tvOS.m @@ -17,7 +17,8 @@ #import #if TARGET_OS_TV -#import +#import +#import #import #import diff --git a/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj b/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj index 814ad945a..e275becc1 100644 --- a/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj +++ b/SubspecWorkspaces/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj @@ -283,14 +283,14 @@ CC55321D1E16EB7A0011C01F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; CC55321E1E16EB7A0011C01F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; @@ -336,7 +336,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = ASDKListKitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -382,7 +382,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = ASDKListKitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = asyncdisplaykit.ASDKListKitTests; diff --git a/SubspecWorkspaces/ASDKListKit/Podfile b/SubspecWorkspaces/ASDKListKit/Podfile index d5b84556f..36498c247 100644 --- a/SubspecWorkspaces/ASDKListKit/Podfile +++ b/SubspecWorkspaces/ASDKListKit/Podfile @@ -1,6 +1,6 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'ASDKListKitTests' do pod 'Texture/IGListKit', :path => '../..' pod 'OCMock', '~> 3.4' diff --git a/Tests/ASBasicImageDownloaderTests.m b/Tests/ASBasicImageDownloaderTests.m index 2dd7d3375..6144b5c25 100644 --- a/Tests/ASBasicImageDownloaderTests.m +++ b/Tests/ASBasicImageDownloaderTests.m @@ -38,14 +38,14 @@ - (void)testAsynchronouslyDownloadTheSameURLTwice [downloader downloadImageWithURL:URL callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) downloadProgress:nil - completion:^(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier) { + completion:^(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { [firstExpectation fulfill]; }]; [downloader downloadImageWithURL:URL callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) downloadProgress:nil - completion:^(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier) { + completion:^(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { [secondExpectation fulfill]; }]; diff --git a/Tests/ASCollectionViewTests.mm b/Tests/ASCollectionViewTests.mm index 11ab711d6..9ea6fe557 100644 --- a/Tests/ASCollectionViewTests.mm +++ b/Tests/ASCollectionViewTests.mm @@ -23,6 +23,8 @@ #import #import #import +#import +#import "ASDisplayNodeTestsHelper.h" @interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode @@ -860,6 +862,7 @@ - (void)testThatDeletedItemsAreMarkedInvisible [cn waitUntilAllUpdatesAreProcessed]; [cn.view layoutIfNeeded]; ASCellNode *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASCATransactionQueueWait(nil); XCTAssertTrue(node.visible); testController.asyncDelegate->_itemCounts = {0}; [cn deleteItemsAtIndexPaths: @[[NSIndexPath indexPathForItem:0 inSection:0]]]; @@ -867,7 +870,7 @@ - (void)testThatDeletedItemsAreMarkedInvisible [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]; @@ -1047,7 +1050,7 @@ - (void)testInitialRangeBounds window.rootViewController = testController; [window makeKeyAndVisible]; - // Trigger the initial reload to start + // Trigger the initial reload to start [window layoutIfNeeded]; // Test the APIs that monitor ASCollectionNode update handling @@ -1071,7 +1074,7 @@ - (void)testInitialRangeBounds for (NSInteger i = 0; i < c; i++) { NSIndexPath *ip = [NSIndexPath indexPathForItem:i inSection:s]; ASCellNode *node = [cn nodeForItemAtIndexPath:ip]; - [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.001]]; + ASCATransactionQueueWait(nil); if (node.inPreloadState) { CGRect frame = [cn.view layoutAttributesForItemAtIndexPath:ip].frame; r = CGRectUnion(r, frame); @@ -1098,7 +1101,7 @@ - (void)testTraitCollectionChangesMidUpdate [window layoutIfNeeded]; // The initial reload is async, changing the trait collection here should be "mid-update" - ASPrimitiveTraitCollection traitCollection; + ASPrimitiveTraitCollection traitCollection = ASPrimitiveTraitCollectionMakeDefault(); traitCollection.displayScale = cn.primitiveTraitCollection.displayScale + 1; // Just a dummy change traitCollection.containerSize = screenBounds.size; cn.primitiveTraitCollection = traitCollection; diff --git a/Tests/ASConfigurationTests.m b/Tests/ASConfigurationTests.m new file mode 100644 index 000000000..41e0d0ed8 --- /dev/null +++ b/Tests/ASConfigurationTests.m @@ -0,0 +1,77 @@ +// +// ASConfigurationTests.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASTestCase.h" +#import "ASConfiguration.h" +#import "ASConfigurationDelegate.h" +#import "ASConfigurationInternal.h" + +@interface ASConfigurationTests : ASTestCase + +@end + +@implementation ASConfigurationTests { + void (^onActivate)(ASConfigurationTests *self, ASExperimentalFeatures feature); +} + +- (void)testExperimentalFeatureConfig +{ + // Set the config + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalGraphicsContexts; + config.delegate = self; + [ASConfigurationManager test_resetWithConfiguration:config]; + + // Set an expectation for a callback, and assert we only get one. + XCTestExpectation *e = [self expectationWithDescription:@"Callback 1 done."]; + onActivate = ^(ASConfigurationTests *self, ASExperimentalFeatures feature) { + XCTAssertEqual(feature, ASExperimentalGraphicsContexts); + [e fulfill]; + // Next time it's a fail. + self->onActivate = ^(ASConfigurationTests *self, ASExperimentalFeatures feature) { + XCTFail(@"Too many callbacks."); + }; + }; + + // Now activate the graphics experiment and expect it works. + XCTAssertTrue(ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)); + // We should get a callback here + // Now activate text node and expect it fails. + XCTAssertFalse(ASActivateExperimentalFeature(ASExperimentalTextNode)); + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)textureDidActivateExperimentalFeatures:(ASExperimentalFeatures)feature +{ + if (onActivate) { + onActivate(self, feature); + } +} + +- (void)testMappingNamesToFlags +{ + // Throw in a bad bit. + ASExperimentalFeatures features = ASExperimentalTextNode | ASExperimentalGraphicsContexts | (1 << 22); + NSArray *expectedNames = @[ @"exp_graphics_contexts", @"exp_text_node" ]; + XCTAssertEqualObjects(expectedNames, ASExperimentalFeaturesGetNames(features)); +} + +- (void)testMappingFlagsFromNames +{ + // Throw in a bad name. + NSArray *names = @[ @"exp_text_node", @"exp_graphics_contexts", @"__invalid_name" ]; + ASExperimentalFeatures expected = ASExperimentalGraphicsContexts | ASExperimentalTextNode; + XCTAssertEqual(expected, ASExperimentalFeaturesFromArray(names)); +} + +@end diff --git a/Tests/ASCornerLayoutSpecSnapshotTests.mm b/Tests/ASCornerLayoutSpecSnapshotTests.mm new file mode 100644 index 000000000..fcbbfe985 --- /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 1abcc7154..1e5b19aba 100644 --- a/Tests/ASDisplayLayerTests.m +++ b/Tests/ASDisplayLayerTests.m @@ -148,7 +148,7 @@ @interface _ASDisplayLayerTestDelegate : ASDisplayNode <_ASDisplayLayerDelegate> // 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 @@ - (void)DISABLED_testTransaction 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 a3a7e160f..9c39386df 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,49 @@ - (void)testInitialNodeInsertionWithOrdering 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 setHierarchyState:ASHierarchyStateRangeManaged]; + 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]; + + ASCATransactionQueueWait(nil); + // 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/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index 75346ff85..ba956321d 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -29,14 +29,17 @@ #import "ASDisplayNodeTestsHelper.h" #import #import +#import #import #import #import +#import #import #import #import #import #import +#import // Conveniences for making nodes named a certain way #define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.debugName = @#n @@ -87,11 +90,15 @@ XCTAssertFalse(n.nodeLoaded, @"%@ should not be loaded", n.debugName);\ } +@interface UIWindow (Testing) +// UIWindow has this handy method that is not public but great for testing +- (UIResponder *)firstResponder; +@end @interface ASDisplayNode (HackForTests) - (id)initWithViewClass:(Class)viewClass; - (id)initWithLayerClass:(Class)layerClass; - +- (void)setInterfaceState:(ASInterfaceState)state; // FIXME: Importing ASDisplayNodeInternal.h causes a heap of problems. - (void)enterInterfaceState:(ASInterfaceState)interfaceState; @end @@ -122,6 +129,12 @@ @interface ASTestResponderNode : ASTestDisplayNode @implementation ASTestDisplayNode +- (void)setInterfaceState:(ASInterfaceState)state +{ + [super setInterfaceState:state]; + ASCATransactionQueueWait(nil); +} + - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { return _calculateSizeBlock ? _calculateSizeBlock(self, constrainedSize) : CGSizeZero; @@ -173,6 +186,28 @@ - (void)displayWillStartAsynchronously:(BOOL)asynchronously @end +@interface ASSynchronousTestDisplayNodeViaViewClass : ASDisplayNode +@end + +@implementation ASSynchronousTestDisplayNodeViaViewClass + ++ (Class)viewClass { + return [UIView class]; +} + +@end + +@interface ASSynchronousTestDisplayNodeViaLayerClass : ASDisplayNode +@end + +@implementation ASSynchronousTestDisplayNodeViaLayerClass + ++ (Class)layerClass { + return [CALayer class]; +} + +@end + @interface UIDisplayNodeTestView : UIView @end @@ -228,6 +263,26 @@ - (BOOL)resignFirstResponder { @end +@interface ASTestViewController: ASViewController +@end +@implementation ASTestViewController +- (BOOL)prefersStatusBarHidden { return YES; } +@end + +@interface UIResponderNodeTestDisplayViewCallingSuper : _ASDisplayView +@end +@implementation UIResponderNodeTestDisplayViewCallingSuper +- (BOOL)canBecomeFirstResponder { return YES; } +- (BOOL)becomeFirstResponder { return [super becomeFirstResponder]; } +@end + +@interface UIResponderNodeTestViewCallingSuper : UIView +@end +@implementation UIResponderNodeTestViewCallingSuper +- (BOOL)canBecomeFirstResponder { return YES; } +- (BOOL)becomeFirstResponder { return [super becomeFirstResponder]; } +@end + @interface ASDisplayNodeTests : XCTestCase @end @@ -236,18 +291,77 @@ @implementation ASDisplayNodeTests dispatch_queue_t queue; } -- (void)testOverriddenFirstResponderBehavior { +- (void)testOverriddenNodeFirstResponderBehavior +{ ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; XCTAssertTrue([node canBecomeFirstResponder]); XCTAssertTrue([node becomeFirstResponder]); } -- (void)testDefaultFirstResponderBehavior { +- (void)testOverriddenDisplayViewFirstResponderBehavior +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewClass:[UIResponderNodeTestDisplayViewCallingSuper class]]; + + // We have to add the node to a window otherwise the super responder methods call responses are undefined + // This will also create the backing view of the node + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); +} + +- (void)testOverriddenViewFirstResponderBehavior +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewClass:[UIResponderNodeTestViewCallingSuper class]]; + + // We have to add the node to a window otherwise the super responder methods call responses are undefined + // This will also create the backing view of the node + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); +} + +- (void)testDefaultFirstResponderBehavior +{ ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; XCTAssertFalse([node canBecomeFirstResponder]); XCTAssertFalse([node becomeFirstResponder]); } +- (void)testResponderMethodsBehavior +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASEditableTextNode *textNode = [[ASEditableTextNode alloc] init]; + + // We have to add the text node to a window otherwise the responder methods responses are undefined + // This will also create the backing view of the node + [window addSubnode:textNode]; + [window makeKeyAndVisible]; + + XCTAssertTrue([textNode canBecomeFirstResponder]); + XCTAssertTrue([textNode becomeFirstResponder]); + XCTAssertTrue([window firstResponder] == textNode.textView); + XCTAssertTrue([textNode resignFirstResponder]); + + // If the textNode resigns it's first responder the view should not be the first responder + XCTAssertTrue([window firstResponder] == nil); + XCTAssertFalse([textNode.view isFirstResponder]); +} + +- (void)testUnsupportedResponderSetupWillThrow +{ + ASTestResponderNode *node = [[ASTestResponderNode alloc] init]; + [node setViewBlock:^UIView * _Nonnull{ + return [[UIView alloc] init]; + }]; + XCTAssertThrows([node view], @"Externally provided views should be synchronous"); +} + - (void)setUp { [super setUp]; @@ -370,6 +484,10 @@ - (void)checkValuesMatchDefaults:(ASDisplayNode *)node isLayerBacked:(BOOL)isLay XCTAssertEqual(NO, node.exclusiveTouch, @"default exclusiveTouch broken %@", hasLoadedView); XCTAssertEqual(YES, node.autoresizesSubviews, @"default autoresizesSubviews broken %@", hasLoadedView); XCTAssertEqual(UIViewAutoresizingNone, node.autoresizingMask, @"default autoresizingMask broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsMake(8, 8, 8, 8), node.layoutMargins), @"default layoutMargins broken %@", hasLoadedView); + XCTAssertEqual(NO, node.preservesSuperviewLayoutMargins, @"default preservesSuperviewLayoutMargins broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, node.safeAreaInsets), @"default safeAreaInsets broken %@", hasLoadedView); + XCTAssertEqual(YES, node.insetsLayoutMarginsFromSafeArea, @"default insetsLayoutMarginsFromSafeArea broken %@", hasLoadedView); } else { XCTAssertEqual(NO, node.userInteractionEnabled, @"layer-backed nodes do not support userInteractionEnabled %@", hasLoadedView); XCTAssertEqual(NO, node.exclusiveTouch, @"layer-backed nodes do not support exclusiveTouch %@", hasLoadedView); @@ -477,6 +595,9 @@ - (void)checkValuesMatchSetValues:(ASDisplayNode *)node isLayerBacked:(BOOL)isLa if (!isLayerBacked) { XCTAssertEqual(UIViewAutoresizingFlexibleLeftMargin, node.autoresizingMask, @"autoresizingMask %@", hasLoadedView); XCTAssertEqual(NO, node.autoresizesSubviews, @"autoresizesSubviews broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsMake(3, 5, 8, 11), node.layoutMargins), @"layoutMargins broken %@", hasLoadedView); + XCTAssertEqual(YES, node.preservesSuperviewLayoutMargins, @"preservesSuperviewLayoutMargins broken %@", hasLoadedView); + XCTAssertEqual(NO, node.insetsLayoutMarginsFromSafeArea, @"insetsLayoutMarginsFromSafeArea broken %@", hasLoadedView); } } @@ -545,6 +666,9 @@ - (void)checkSimpleBridgePropertiesSetPropagate:(BOOL)isLayerBacked node.exclusiveTouch = YES; node.autoresizesSubviews = NO; node.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + node.insetsLayoutMarginsFromSafeArea = NO; + node.layoutMargins = UIEdgeInsetsMake(3, 5, 8, 11); + node.preservesSuperviewLayoutMargins = YES; } }]; @@ -711,46 +835,54 @@ - (void)testDisplayNodePointConversionWithFrames // 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)); } @@ -764,7 +896,8 @@ - (void)testDisplayNodePointConversionWithNonZeroBounds // 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 @@ -773,12 +906,14 @@ - (void)testDisplayNodePointConversionWithNonZeroBounds 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 @@ -787,12 +922,14 @@ - (void)testDisplayNodePointConversionWithNonZeroBounds 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 @@ -801,12 +938,14 @@ - (void)testDisplayNodePointConversionWithNonZeroBounds 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 @@ -815,7 +954,8 @@ - (void)testDisplayNodePointConversionWithNonZeroBounds 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)); } @@ -828,7 +968,8 @@ - (void)testDisplayNodePointConversionWithNonZeroAnchorPoint // 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 @@ -836,12 +977,14 @@ - (void)testDisplayNodePointConversionWithNonZeroAnchorPoint 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 @@ -849,12 +992,14 @@ - (void)testDisplayNodePointConversionWithNonZeroAnchorPoint 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 @@ -862,12 +1007,14 @@ - (void)testDisplayNodePointConversionWithNonZeroAnchorPoint 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 @@ -875,7 +1022,8 @@ - (void)testDisplayNodePointConversionWithNonZeroAnchorPoint 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)); } @@ -2012,9 +2160,9 @@ - (void)testThatNodeGetsRenderedIfItGoesFromZeroSizeToRealSizeButOnlyOnce // Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2205 - (void)testThatRasterizedNodesGetInterfaceStateUpdatesWhenContainerEntersHierarchy { - ASDisplayNode *supernode = [[ASDisplayNode alloc] init]; + ASDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; [supernode enableSubtreeRasterization]; - ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + ASDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; ASSetDebugNames(supernode, subnode); UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; [supernode addSubnode:subnode]; @@ -2030,9 +2178,9 @@ - (void)testThatRasterizedNodesGetInterfaceStateUpdatesWhenContainerEntersHierar // Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2205 - (void)testThatRasterizedNodesGetInterfaceStateUpdatesWhenAddedToContainerThatIsInHierarchy { - ASDisplayNode *supernode = [[ASDisplayNode alloc] init]; + ASDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; [supernode enableSubtreeRasterization]; - ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + ASDisplayNode *subnode = [[ASTestDisplayNode alloc] init]; ASSetDebugNames(supernode, subnode); UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; @@ -2146,8 +2294,7 @@ - (void)testThatSubnodeGetsInterfaceStateSetIfRasterized [node view]; // Node needs to be loaded [node enterInterfaceState:ASInterfaceStatePreload]; - - + XCTAssertTrue((node.interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload); XCTAssertTrue((subnode.interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload); XCTAssertTrue(node.hasPreloaded); @@ -2245,6 +2392,46 @@ - (void)testThatHavingTheSameNodeTwiceInALayoutSpecCausesExceptionOnLayoutCalcul XCTAssertThrowsSpecificNamed([node calculateLayoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))], NSException, NSInternalInconsistencyException); } +- (void)testThatStackSpecOrdersSubnodesCorrectly +{ + // This test ensures that the z-order of nodes matches the stack spec, including after relayout / transition. + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareNodeNamed(d); + + NSArray *nodesForwardOrder = @[a, b, c, d]; + NSArray *nodesReverseOrder = @[d, c, b, a]; + __block BOOL flipItemOrder = NO; + + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange size) { + ASStackLayoutSpec *stack = [ASStackLayoutSpec verticalStackLayoutSpec]; + stack.children = flipItemOrder ? nodesReverseOrder : nodesForwardOrder; + return stack; + }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + + // Because automaticallyManagesSubnodes is used, the subnodes array is constructed from the layout spec's children. + XCTAssert([node.subnodes isEqualToArray:nodesForwardOrder], @"subnodes: %@, array: %@", node.subnodes, nodesForwardOrder); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + @"a,b,c,d", @"Forward order"); + + flipItemOrder = YES; + [node invalidateCalculatedLayout]; + [node.view layoutIfNeeded]; + + // In this case, it's critical that the items are in the new order so that event handling and apparent z-position are correct. + // FIXME: The reversal case is not currently passing. + // XCTAssert([node.subnodes isEqualToArray:nodesReverseOrder], @"subnodes: %@, array: %@", node.subnodes, nodesReverseOrder); + // XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + // @"d,c,b,a", @"Reverse order"); +} + - (void)testThatOverlaySpecOrdersSubnodesCorrectly { ASDisplayNode *node = [[ASDisplayNode alloc] init]; @@ -2325,4 +2512,65 @@ - (void)testThatItIsAllowedToRetrieveDebugDescriptionIncludingVCOffMainThread XCTAssert(hasVC); } +- (void)testThatSubnodeSafeAreaInsetsAreCalculatedCorrectly +{ + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + + rootNode.automaticallyManagesSubnodes = YES; + rootNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(1, 2, 3, 4) child:subnode]; + }; + + ASTestViewController *viewController = [[ASTestViewController alloc] initWithNode:rootNode]; + viewController.additionalSafeAreaInsets = UIEdgeInsetsMake(10, 10, 10, 10); + + // It looks like iOS 11 suppresses safeAreaInsets calculation for the views that are not on screen. + UIWindow *window = [[UIWindow alloc] init]; + window.rootViewController = viewController; + [window setHidden:NO]; + [window layoutIfNeeded]; + + UIEdgeInsets expectedRootNodeSafeArea = UIEdgeInsetsMake(10, 10, 10, 10); + UIEdgeInsets expectedSubnodeSafeArea = UIEdgeInsetsMake(9, 8, 7, 6); + + UIEdgeInsets windowSafeArea = UIEdgeInsetsZero; + if (AS_AVAILABLE_IOS(11.0)) { + windowSafeArea = window.safeAreaInsets; + } + + expectedRootNodeSafeArea = ASConcatInsets(expectedRootNodeSafeArea, windowSafeArea); + expectedSubnodeSafeArea = ASConcatInsets(expectedSubnodeSafeArea, windowSafeArea); + + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(expectedRootNodeSafeArea, rootNode.safeAreaInsets), + @"expected rootNode.safeAreaInsets to be %@ but got %@ (window.safeAreaInsets %@)", + NSStringFromUIEdgeInsets(expectedRootNodeSafeArea), + NSStringFromUIEdgeInsets(rootNode.safeAreaInsets), + NSStringFromUIEdgeInsets(windowSafeArea)); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(expectedSubnodeSafeArea, subnode.safeAreaInsets), + @"expected subnode.safeAreaInsets to be %@ but got %@ (window.safeAreaInsets %@)", + NSStringFromUIEdgeInsets(expectedSubnodeSafeArea), + NSStringFromUIEdgeInsets(subnode.safeAreaInsets), + NSStringFromUIEdgeInsets(windowSafeArea)); + + [window setHidden:YES]; +} + +- (void)testScreenScale +{ + XCTAssertEqual(ASScreenScale(), UIScreen.mainScreen.scale); +} + +- (void)testThatIfViewClassIsOverwrittenItsSynchronous +{ + ASSynchronousTestDisplayNodeViaViewClass *node = [[ASSynchronousTestDisplayNodeViaViewClass alloc] init]; + XCTAssertTrue([node isSynchronous], @"Node should be synchronous if viewClass is ovewritten and not a subclass of _ASDisplayView"); +} + +- (void)testThatIfLayerClassIsOverwrittenItsSynchronous +{ + ASSynchronousTestDisplayNodeViaLayerClass *node = [[ASSynchronousTestDisplayNodeViaLayerClass alloc] init]; + XCTAssertTrue([node isSynchronous], @"Node should be synchronous if viewClass is ovewritten and not a subclass of _ASDisplayView"); +} + @end diff --git a/Tests/ASDisplayNodeTestsHelper.h b/Tests/ASDisplayNodeTestsHelper.h index 447350004..3e0bea5fb 100644 --- a/Tests/ASDisplayNodeTestsHelper.h +++ b/Tests/ASDisplayNodeTestsHelper.h @@ -18,7 +18,7 @@ #import #import -@class ASDisplayNode; +@class ASCATransactionQueue, ASDisplayNode; typedef BOOL (^as_condition_block_t)(void); @@ -28,5 +28,6 @@ BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block); void ASDisplayNodeSizeToFitSize(ASDisplayNode *node, CGSize size); void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange); +void ASCATransactionQueueWait(ASCATransactionQueue *q); // nil means shared queue ASDISPLAYNODE_EXTERN_C_END diff --git a/Tests/ASDisplayNodeTestsHelper.m b/Tests/ASDisplayNodeTestsHelper.m index 05397b83b..6dbd9b794 100644 --- a/Tests/ASDisplayNodeTestsHelper.m +++ b/Tests/ASDisplayNodeTestsHelper.m @@ -18,6 +18,7 @@ #import "ASDisplayNodeTestsHelper.h" #import #import +#import #import @@ -62,3 +63,15 @@ void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange) CGSize sizeThatFits = [node layoutThatFits:sizeRange].size; node.bounds = (CGRect){.origin = CGPointZero, .size = sizeThatFits}; } + +void ASCATransactionQueueWait(ASCATransactionQueue *q) +{ + if (!q) { q = ASCATransactionQueue.sharedQueue; } + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:1]; + BOOL whileResult = YES; + while ([date timeIntervalSinceNow] > 0 && + (whileResult = ![q isEmpty])) { + [[NSRunLoop currentRunLoop] runUntilDate: + [NSDate dateWithTimeIntervalSinceNow:0.01]]; + } +} diff --git a/Tests/ASLayoutEngineTests.mm b/Tests/ASLayoutEngineTests.mm new file mode 100644 index 000000000..550dffa47 --- /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 56f76c5d4..fb04a9509 100644 --- a/Tests/ASLayoutFlatteningTests.m +++ b/Tests/ASLayoutFlatteningTests.m @@ -42,9 +42,9 @@ - (void)testThatFlattenedLayoutContainsOnlyDirectSubnodesInValidOrder 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 @@ - (void)testThatLayoutWithNullPositionAndFlattenedNodeSublayoutsIsReused @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 @@ - (void)testThatLayoutWithNullPositionAndUnflattenedSublayoutsIsNotReused 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/ASLayoutTestNode.h b/Tests/ASLayoutTestNode.h new file mode 100644 index 000000000..66fafee14 --- /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 000000000..3a112422a --- /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/ASMultiplexImageNodeTests.m b/Tests/ASMultiplexImageNodeTests.m index 9eb41874b..71b1fe05e 100644 --- a/Tests/ASMultiplexImageNodeTests.m +++ b/Tests/ASMultiplexImageNodeTests.m @@ -238,7 +238,7 @@ - (void)testUncachedDownload // Simulate completion. ASImageDownloaderCompletion completionBlock = [inv as_argumentAtIndexAsObject:5]; - completionBlock([self _testImage], nil, nil); + completionBlock([self _testImage], nil, nil, nil); }); NSNumber *imageIdentifier = @1; diff --git a/Tests/ASNavigationControllerTests.m b/Tests/ASNavigationControllerTests.m new file mode 100644 index 000000000..80f6ba87f --- /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/ASNetworkImageNodeTests.m b/Tests/ASNetworkImageNodeTests.m index 94dc668d5..498e79e8c 100644 --- a/Tests/ASNetworkImageNodeTests.m +++ b/Tests/ASNetworkImageNodeTests.m @@ -2,8 +2,12 @@ // ASNetworkImageNodeTests.m // Texture // -// Created by Adlai Holler on 10/14/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import diff --git a/Tests/ASPagerNodeTests.m b/Tests/ASPagerNodeTests.m index 05396387b..6f7157768 100644 --- a/Tests/ASPagerNodeTests.m +++ b/Tests/ASPagerNodeTests.m @@ -50,7 +50,8 @@ @interface ASPagerNodeTestController: UIViewController @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 @@ @interface ASPagerNodeTests : XCTestCase @implementation ASPagerNodeTests -- (void)testPagerReturnsIndexOfPages { +- (void)testPagerReturnsIndexOfPages +{ ASPagerNodeTestController *testController = [self testController]; ASCellNode *cellNode = [testController.pagerNode nodeForPageAtIndex:0]; @@ -82,7 +84,8 @@ - (void)testPagerReturnsIndexOfPages { XCTAssertEqual([testController.pagerNode indexOfPageWithNode:cellNode], 0); } -- (void)testPagerReturnsNotFoundForCellThatDontExistInPager { +- (void)testPagerReturnsNotFoundForCellThatDontExistInPager +{ ASPagerNodeTestController *testController = [self testController]; ASCellNode *badNode = [[ASCellNode alloc] init]; @@ -90,7 +93,17 @@ - (void)testPagerReturnsNotFoundForCellThatDontExistInPager { 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]; diff --git a/Tests/ASPerformanceTestContext.h b/Tests/ASPerformanceTestContext.h index 42075f5fc..d60f53b1a 100644 --- a/Tests/ASPerformanceTestContext.h +++ b/Tests/ASPerformanceTestContext.h @@ -2,8 +2,12 @@ // ASPerformanceTestContext.h // Texture // -// Created by Adlai Holler on 8/28/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import diff --git a/Tests/ASRectTableTests.m b/Tests/ASRectMapTests.m similarity index 78% rename from Tests/ASRectTableTests.m rename to Tests/ASRectMapTests.m index ca897a357..357f3879a 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)testThatItStoresRects - (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/ASRecursiveUnfairLockTests.m b/Tests/ASRecursiveUnfairLockTests.m new file mode 100644 index 000000000..56c0245b4 --- /dev/null +++ b/Tests/ASRecursiveUnfairLockTests.m @@ -0,0 +1,188 @@ +// +// ASRecursiveUnfairLockTests.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" +#import +#import +#import + +@interface ASRecursiveUnfairLockTests : ASTestCase + +@end + +@implementation ASRecursiveUnfairLockTests { + ASRecursiveUnfairLock lock; +} + +- (void)setUp +{ + [super setUp]; + lock = AS_RECURSIVE_UNFAIR_LOCK_INIT; +} + +- (void)testTheAtomicIsLockFree +{ + XCTAssertTrue(atomic_is_lock_free(&lock._thread)); +} + +- (void)testRelockingFromSameThread +{ + ASRecursiveUnfairLockLock(&lock); + ASRecursiveUnfairLockLock(&lock); + ASRecursiveUnfairLockUnlock(&lock); + // Now try locking from another thread. + XCTestExpectation *e1 = [self expectationWithDescription:@"Other thread tried lock."]; + [NSThread detachNewThreadWithBlock:^{ + XCTAssertFalse(ASRecursiveUnfairLockTryLock(&self->lock)); + [e1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + ASRecursiveUnfairLockUnlock(&lock); + + XCTestExpectation *e2 = [self expectationWithDescription:@"Other thread tried lock again"]; + [NSThread detachNewThreadWithBlock:^{ + XCTAssertTrue(ASRecursiveUnfairLockTryLock(&self->lock)); + ASRecursiveUnfairLockUnlock(&self->lock); + [e2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testThatUnlockingWithoutHoldingMakesAssertion +{ +#ifdef NS_BLOCK_ASSERTIONS +#warning Assertions should be on for `testThatUnlockingWithoutHoldingMakesAssertion` + NSLog(@"Passing because assertions are off."); +#else + ASRecursiveUnfairLockLock(&lock); + XCTestExpectation *e1 = [self expectationWithDescription:@"Other thread tried lock."]; + [NSThread detachNewThreadWithBlock:^{ + XCTAssertThrows(ASRecursiveUnfairLockUnlock(&lock)); + [e1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + ASRecursiveUnfairLockUnlock(&lock); +#endif +} + +#define CHAOS_TEST_BODY(contested, prefix, infix, postfix) \ +dispatch_group_t g = dispatch_group_create(); \ +for (int i = 0; i < (contested ? 16 : 2); i++) {\ +dispatch_group_enter(g);\ +[NSThread detachNewThreadWithBlock:^{\ + for (int i = 0; i < 20000; i++) {\ + prefix;\ + value += 150;\ + infix;\ + value -= 150;\ + postfix;\ + }\ + dispatch_group_leave(g);\ +}];\ +}\ +dispatch_group_wait(g, DISPATCH_TIME_FOREVER); + +#pragma mark - Correctness Tests + +- (void)testRecursiveUnfairLockContested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, ASRecursiveUnfairLockLock(&lock), {}, ASRecursiveUnfairLockUnlock(&lock)); + }]; + XCTAssertEqual(value, 0); +} + +- (void)testRecursiveUnfairLockUncontested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, ASRecursiveUnfairLockLock(&lock), {}, ASRecursiveUnfairLockUnlock(&lock)); + }]; + XCTAssertEqual(value, 0); +} + +#pragma mark - Lock performance tests + +#if RUN_LOCK_PERF_TESTS +- (void)testNoLockContested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, {}, {}, {}); + }]; + XCTAssertNotEqual(value, 0); +} + +- (void)testPlainUnfairLockContested +{ + __block int value = 0; + __block os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT; + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, os_unfair_lock_lock(&unfairLock), {}, os_unfair_lock_unlock(&unfairLock)); + }]; + XCTAssertEqual(value, 0); +} + +- (void)testRecursiveMutexContested +{ + __block int value = 0; + pthread_mutexattr_t attr; + pthread_mutexattr_init (&attr); + pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); + __block pthread_mutex_t m; + pthread_mutex_init (&m, &attr); + pthread_mutexattr_destroy (&attr); + + [self measureBlock:^{ + CHAOS_TEST_BODY(YES, pthread_mutex_lock(&m), {}, pthread_mutex_unlock(&m)); + }]; + pthread_mutex_destroy(&m); +} + +- (void)testNoLockUncontested +{ + __block int value = 0; + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, {}, {}, {}); + }]; + XCTAssertNotEqual(value, 0); +} + +- (void)testPlainUnfairLockUncontested +{ + __block int value = 0; + __block os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT; + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, os_unfair_lock_lock(&unfairLock), {}, os_unfair_lock_unlock(&unfairLock)); + }]; + XCTAssertEqual(value, 0); +} + +- (void)testRecursiveMutexUncontested +{ + __block int value = 0; + pthread_mutexattr_t attr; + pthread_mutexattr_init (&attr); + pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); + __block pthread_mutex_t m; + pthread_mutex_init (&m, &attr); + pthread_mutexattr_destroy (&attr); + + [self measureBlock:^{ + CHAOS_TEST_BODY(NO, pthread_mutex_lock(&m), {}, pthread_mutex_unlock(&m)); + }]; + pthread_mutex_destroy(&m); +} + +#endif // RUN_LOCK_PERF_TESTS +@end diff --git a/Tests/ASRunLoopQueueTests.m b/Tests/ASRunLoopQueueTests.m index ccf15ae7b..7889be9be 100644 --- a/Tests/ASRunLoopQueueTests.m +++ b/Tests/ASRunLoopQueueTests.m @@ -2,20 +2,33 @@ // ASRunLoopQueueTests.m // Texture // -// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 "ASTestCase.h" + #import +#import "ASDisplayNodeTestsHelper.h" static NSTimeInterval const kRunLoopRunTime = 0.001; // Allow the RunLoop to run for one millisecond each time. -@interface ASRunLoopQueueTests : XCTestCase +@interface QueueObject : NSObject +@property (nonatomic, assign) BOOL queueObjectProcessed; +@end + +@implementation QueueObject +- (void)prepareForCATransactionCommit +{ + self.queueObjectProcessed = YES; +} +@end + +@interface ASRunLoopQueueTests : ASTestCase @end @@ -157,4 +170,35 @@ - (void)testWeakQueueWithAllDeallocatedObjectsIsDrained XCTAssertTrue(queue.isEmpty); } +- (void)testASCATransactionQueueDisable +{ + // Disable coalescing. + ASConfiguration *config = [[ASConfiguration alloc] init]; + config.experimentalFeatures = kNilOptions; + [ASConfigurationManager test_resetWithConfiguration:config]; + + ASCATransactionQueue *queue = [[ASCATransactionQueue alloc] init]; + QueueObject *object = [[QueueObject alloc] init]; + XCTAssertFalse(object.queueObjectProcessed); + [queue enqueue:object]; + XCTAssertTrue(object.queueObjectProcessed); + XCTAssertTrue([queue isEmpty]); + XCTAssertFalse(queue.enabled); +} + +- (void)testASCATransactionQueueProcess +{ + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing; + [ASConfigurationManager test_resetWithConfiguration:config]; + + ASCATransactionQueue *queue = [[ASCATransactionQueue alloc] init]; + QueueObject *object = [[QueueObject alloc] init]; + [queue enqueue:object]; + XCTAssertFalse(object.queueObjectProcessed); + ASCATransactionQueueWait(queue); + XCTAssertTrue(object.queueObjectProcessed); + XCTAssertTrue(queue.enabled); +} + @end diff --git a/Tests/ASScrollNodeTests.m b/Tests/ASScrollNodeTests.m new file mode 100644 index 000000000..56b3fd731 --- /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 000000000..ef590220a --- /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 000000000..bdddbe5bf --- /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 000000000..0e6d9b3d3 --- /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 f26d96b11..fe54a4895 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -37,10 +37,10 @@ @interface ASTestDataController : ASDataController @implementation ASTestDataController -- (void)relayoutAllNodes +- (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock { _numberOfAllNodesRelayouts++; - [super relayoutAllNodes]; + [super relayoutAllNodesWithInvalidationBlock:invalidationBlock]; } @end @@ -139,6 +139,7 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize @interface ASTableViewFilledDataSource : NSObject @property (nonatomic) BOOL usesSectionIndex; +@property (nonatomic) NSInteger numberOfSections; @property (nonatomic) NSInteger rowsPerSection; @property (nonatomic, nullable, copy) ASCellNodeBlock(^nodeBlockForItem)(NSIndexPath *); @end @@ -149,6 +150,7 @@ - (instancetype)init { self = [super init]; if (self != nil) { + _numberOfSections = NumberOfSections; _rowsPerSection = 20; } return self; @@ -165,7 +167,7 @@ - (BOOL)respondsToSelector:(SEL)aSelector - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return NumberOfSections; + return _numberOfSections; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section @@ -833,6 +835,52 @@ - (void)testAutomaticallyAdjustingContentOffset XCTAssertEqual(node.contentOffset.y, 10); } +- (void)testTableViewReloadDoesReloadIfEditableTextNodeIsFirstResponder +{ + ASEditableTextNode *editableTextNode = [[ASEditableTextNode alloc] init]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 375, 667)]; + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + node.frame = window.bounds; + [window addSubnode:node]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + dataSource.rowsPerSection = 1; + dataSource.numberOfSections = 1; + // Currently this test requires that the text in the cell node fills the + // visible width, so we use the long description for the index path. + dataSource.nodeBlockForItem = ^(NSIndexPath *indexPath) { + return (ASCellNodeBlock)^{ + ASCellNode *cellNode = [[ASCellNode alloc] init]; + cellNode.automaticallyManagesSubnodes = YES; + cellNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) child:editableTextNode]; + }; + return cellNode; + }; + }; + node.delegate = dataSource; + node.dataSource = dataSource; + + // Reload the data for the initial load + [node reloadData]; + [node waitUntilAllUpdatesAreProcessed]; + [node setNeedsLayout]; + [node layoutIfNeeded]; + + // Set the textView as first responder + [editableTextNode.textView becomeFirstResponder]; + + // Change data source count and try to reload a second time + dataSource.rowsPerSection = 2; + [node reloadData]; + [node waitUntilAllUpdatesAreProcessed]; + + // Check that numberOfRows in section 0 is 2 + XCTAssertEqual([node numberOfRowsInSection:0], 2); + XCTAssertEqual([node.view numberOfRowsInSection:0], 2); +} + @end @implementation UITableView (Testing) diff --git a/Tests/ASTextKitTests.mm b/Tests/ASTextKitTests.mm index a39a333b1..687206178 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 @@ - (void)testRectsForRangeBeyondTruncationSizeReturnsNonZeroNumberOfRects 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/ASTextNodePerformanceTests.m b/Tests/ASTextNodePerformanceTests.m index aecf6d2fd..f5c62e53a 100644 --- a/Tests/ASTextNodePerformanceTests.m +++ b/Tests/ASTextNodePerformanceTests.m @@ -2,8 +2,12 @@ // ASTextNodePerformanceTests.m // Texture // -// Created by Adlai Holler on 8/28/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import diff --git a/Tests/ASTextNodeSnapshotTests.m b/Tests/ASTextNodeSnapshotTests.m index 0d86a7274..c81f5241a 100644 --- a/Tests/ASTextNodeSnapshotTests.m +++ b/Tests/ASTextNodeSnapshotTests.m @@ -120,4 +120,21 @@ - (void)testShadowing ASSnapshotVerifyNode(textNode, nil); } +/** + * https://github.com/TextureGroup/Texture/issues/822 + */ +- (void)DISABLED_testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.style.maxSize = CGSizeMake(20, 80); + NSMutableAttributedString *mas = [[NSMutableAttributedString alloc] initWithString:@"Quality is an important "]; + [mas appendAttributedString:[[NSAttributedString alloc] initWithString:@"thing" attributes:@{ NSBackgroundColorAttributeName : UIColor.yellowColor}]]; + textNode.attributedText = mas; + textNode.truncationMode = NSLineBreakByTruncatingTail; + + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:@{ NSBackgroundColorAttributeName: UIColor.greenColor }]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + @end diff --git a/Tests/ASTextNodeTests.m b/Tests/ASTextNodeTests.m index 5b1f2cf4d..8c90a6959 100644 --- a/Tests/ASTextNodeTests.m +++ b/Tests/ASTextNodeTests.m @@ -17,6 +17,8 @@ #import +#import "ASTestCase.h" + #import #import @@ -31,6 +33,10 @@ @interface ASTextNodeTestDelegate : NSObject @property (nonatomic, copy, readonly) NSString *tappedLinkAttribute; @property (nonatomic, assign, readonly) id tappedLinkValue; +@end +@interface ASTextNodeSubclass : ASTextNode +@end +@interface ASTextNodeSecondSubclass : ASTextNodeSubclass @end @implementation ASTextNodeTestDelegate @@ -235,4 +241,23 @@ - (void)testAddingExclusionPathsShouldInvalidateAndIncreaseTheSize XCTAssertGreaterThan(sizeWithExclusionPaths.height, sizeWithoutExclusionPaths.height, @"Setting exclusions paths should invalidate the calculated size and return a greater size"); } +- (void)testThatTheExperimentWorksCorrectly +{ + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTextNode; + [ASConfigurationManager test_resetWithConfiguration:config]; + + ASTextNode *plainTextNode = [[ASTextNode alloc] init]; + XCTAssertEqualObjects(plainTextNode.class, [ASTextNode2 class]); + + ASTextNodeSecondSubclass *sc2 = [[ASTextNodeSecondSubclass alloc] init]; + XCTAssertEqualObjects([ASTextNodeSubclass superclass], [ASTextNode2 class]); + XCTAssertEqualObjects(sc2.superclass, [ASTextNodeSubclass class]); +} + +@end + +@implementation ASTextNodeSubclass +@end +@implementation ASTextNodeSecondSubclass @end diff --git a/Tests/ASTraitCollectionTests.m b/Tests/ASTraitCollectionTests.m new file mode 100644 index 000000000..aa106d75e --- /dev/null +++ b/Tests/ASTraitCollectionTests.m @@ -0,0 +1,34 @@ +// +// ASTraitCollectionTests.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 + +@interface ASTraitCollectionTests : XCTestCase + +@end + +@implementation ASTraitCollectionTests + +- (void)testPrimitiveContentSizeCategoryLifetime +{ + ASPrimitiveContentSizeCategory primitiveContentSize; + @autoreleasepool { + // Make sure the compiler won't optimize string alloc/dealloc + NSString *contentSizeCategory = [NSString stringWithCString:"UICTContentSizeCategoryL" encoding:NSUTF8StringEncoding]; + primitiveContentSize = ASPrimitiveContentSizeCategoryMake(contentSizeCategory); + } + + XCTAssertEqual(primitiveContentSize, UIContentSizeCategoryLarge); +} + +@end diff --git a/Tests/ASVideoNodeTests.m b/Tests/ASVideoNodeTests.m index 278509efe..24bb5dd37 100644 --- a/Tests/ASVideoNodeTests.m +++ b/Tests/ASVideoNodeTests.m @@ -21,6 +21,7 @@ #import #import #import +#import "ASDisplayNodeTestsHelper.h" @interface ASVideoNodeTests : XCTestCase { @@ -351,9 +352,9 @@ - (void)testVideoResumedWhenBufferIsLikelyToKeepUp [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStatePreload]; [_videoNode prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + ASCATransactionQueueWait(nil); [_videoNode pause]; _videoNode.shouldBePlaying = YES; - XCTAssertFalse(_videoNode.isPlaying); [_videoNode observeValueForKeyPath:@"playbackLikelyToKeepUp" ofObject:[_videoNode currentItem] change:@{NSKeyValueChangeNewKey : @YES} context:NULL]; diff --git a/Tests/ArrayDiffingTests.m b/Tests/ArrayDiffingTests.m index 854b32a2e..5d25bfde7 100644 --- a/Tests/ArrayDiffingTests.m +++ b/Tests/ArrayDiffingTests.m @@ -65,10 +65,14 @@ - (void)testDiffingCommonIndexes NSIndexSet *indexSet = [test[0] _asdk_commonIndexesWithArray:test[1] compareBlock:^BOOL(id lhs, id rhs) { return [lhs isEqual:rhs]; }]; + NSMutableIndexSet *mutableIndexSet = [indexSet mutableCopy]; for (NSNumber *index in (NSArray *)test[2]) { XCTAssert([indexSet containsIndex:[index integerValue]]); + [mutableIndexSet removeIndex:[index integerValue]]; } + + XCTAssert([mutableIndexSet count] == 0, @"Unaccounted deletions: %@", mutableIndexSet); } } @@ -80,6 +84,12 @@ - (void)testDiffingInsertionsAndDeletions { @[@3], @[], ], + @[ + @[@"a", @"b", @"c", @"d"], + @[@"d", @"c", @"b", @"a"], + @[@1, @2, @3], + @[@0, @1, @2], + ], @[ @[@"bob", @"alice", @"dave"], @[@"bob", @"gary", @"alice", @"dave"], @@ -121,12 +131,20 @@ - (void)testDiffingInsertionsAndDeletions { for (NSArray *test in tests) { NSIndexSet *insertions, *deletions; [test[0] asdk_diffWithArray:test[1] insertions:&insertions deletions:&deletions]; + NSMutableIndexSet *mutableInsertions = [insertions mutableCopy]; + NSMutableIndexSet *mutableDeletions = [deletions mutableCopy]; + for (NSNumber *index in (NSArray *)test[2]) { - XCTAssert([insertions containsIndex:[index integerValue]]); + XCTAssert([mutableInsertions containsIndex:[index integerValue]]); + [mutableInsertions removeIndex:[index integerValue]]; } for (NSNumber *index in (NSArray *)test[3]) { - XCTAssert([deletions containsIndex:[index integerValue]]); + XCTAssert([mutableDeletions containsIndex:[index integerValue]]); + [mutableDeletions removeIndex:[index integerValue]]; } + + XCTAssert([mutableInsertions count] == 0, @"Unaccounted insertions: %@", mutableInsertions); + XCTAssert([mutableDeletions count] == 0, @"Unaccounted deletions: %@", mutableDeletions); } } diff --git a/Tests/Common/ASTestCase.h b/Tests/Common/ASTestCase.h index 4868f64d0..bbcf07336 100644 --- a/Tests/Common/ASTestCase.h +++ b/Tests/Common/ASTestCase.h @@ -2,8 +2,13 @@ // ASTestCase.h // Texture // -// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0 (the "License"); +// 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) through the 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 // @@ -12,6 +17,12 @@ #import +// Not strictly necessary, but convenient +#import +#import +#import "OCMockObject+ASAdditions.h" +#import "ASConfigurationInternal.h" + NS_ASSUME_NONNULL_BEGIN @interface ASTestCase : XCTestCase @@ -20,4 +31,10 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface ASConfigurationManager (Testing) + ++ (void)test_resetWithConfiguration:(nullable ASConfiguration *)configuration; + +@end + NS_ASSUME_NONNULL_END diff --git a/Tests/Common/ASTestCase.m b/Tests/Common/ASTestCase.m index 5bc720253..fb059e7ed 100644 --- a/Tests/Common/ASTestCase.m +++ b/Tests/Common/ASTestCase.m @@ -2,8 +2,13 @@ // ASTestCase.m // Texture // -// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0 (the "License"); +// 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) through the 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 // @@ -31,6 +36,8 @@ - (void)setUp - (void)tearDown { + [ASConfigurationManager test_resetWithConfiguration:nil]; + // Clear out all application windows. Note: the system will retain these sometimes on its // own but we'll do our best. for (UIWindow *window in [UIApplication sharedApplication].windows) { diff --git a/Tests/Common/OCMockObject+ASAdditions.h b/Tests/Common/OCMockObject+ASAdditions.h index a8e700491..f8617411b 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 86dcdbf9d..50fabd5eb 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 @@ + (void)load 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 @@ + (Class)protocolMockObjectClass 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 @@ - (instancetype)swizzled_init 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 000000000..5405e40de --- /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 000000000..3b78fb5e7 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 000000000..3b78fb5e7 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 000000000..34851067b 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 000000000..34851067b 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 000000000..aa4c3ee8d 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 000000000..aa4c3ee8d 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 000000000..23082ede8 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 000000000..23082ede8 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 000000000..18e211ae8 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 000000000..393143a21 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 000000000..18e211ae8 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 000000000..12498681e 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 000000000..18e211ae8 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 000000000..dc4f1ab2b 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 000000000..18e211ae8 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 000000000..fa7e15a55 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 000000000..90f411aff 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 000000000..6d49323c1 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 000000000..9d23e2b64 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 000000000..58257ffef 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 000000000..3503fd79d 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 000000000..263f50d29 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 000000000..492fc049b 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 000000000..9e39a3c5c Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASCornerLayoutSpecSnapshotTests/testCornerSpecForAllLocations_topRight_center_fullSize@2x.png differ diff --git a/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode@2x.png b/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode@2x.png new file mode 100644 index 000000000..40351ad62 Binary files /dev/null and b/Tests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testThatTruncationTokenAttributesPrecedeThoseInheritedFromTextWhenTruncateTailMode@2x.png differ diff --git a/Texture.podspec b/Texture.podspec index 6bc7c1621..7041e82f2 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Texture' - spec.version = '2.5' + 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' } @@ -11,13 +11,11 @@ Pod::Spec.new do |spec| spec.documentation_url = 'http://texturegroup.org/appledoc/' - spec.weak_frameworks = 'Photos','MapKit','AssetsLibrary' - spec.requires_arc = true + spec.ios.weak_frameworks = 'AssetsLibrary' + spec.weak_frameworks = 'Photos','MapKit' - spec.ios.deployment_target = '8.0' - - # Uncomment when fixed: issues with tvOS build for release 2.0 - # spec.tvos.deployment_target = '9.0' + spec.ios.deployment_target = '9.0' + spec.tvos.deployment_target = '9.0' # Subspecs spec.subspec 'Core' do |core| @@ -41,7 +39,6 @@ Pod::Spec.new do |spec| # See https://github.com/facebook/AsyncDisplayKit/issues/1153 'Source/TextKit/*.h', ] - core.xcconfig = { 'GCC_PRECOMPILE_PREFIX_HEADER' => 'YES' } end spec.subspec 'PINRemoteImage' do |pin| @@ -51,7 +48,7 @@ Pod::Spec.new do |spec| end spec.subspec 'IGListKit' do |igl| - igl.dependency 'IGListKit', '3.0.0' + igl.dependency 'IGListKit', '~> 3.0' igl.dependency 'Texture/Core' end @@ -66,9 +63,4 @@ Pod::Spec.new do |spec| spec.social_media_url = 'https://twitter.com/TextureiOS' spec.library = 'c++' - spec.pod_target_xcconfig = { - 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11', - 'CLANG_CXX_LIBRARY' => 'libc++' - } - end diff --git a/build.sh b/build.sh index bffaad4cc..ec4a62104 100755 --- a/build.sh +++ b/build.sh @@ -195,7 +195,7 @@ fi if [ "$MODE" = "cocoapods-lint" -o "$MODE" = "all" ]; then echo "Verifying that podspec lints." - set -o pipefail && pod env && pod lib lint + set -o pipefail && pod env && pod lib lint --allow-warnings success="1" fi diff --git a/docs/_docs/containers-asnodecontroller.md b/docs/_docs/containers-asnodecontroller.md index 7dd5034da..09b644440 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-layout-element-properties.md b/docs/_docs/layout2-layout-element-properties.md index 2efb71be6..21ed56b11 100755 --- a/docs/_docs/layout2-layout-element-properties.md +++ b/docs/_docs/layout2-layout-element-properties.md @@ -30,11 +30,11 @@ nextPage: layout2-api-sizing.html Additional space to place after this object in the stacking direction. - `BOOL .style.flexGrow` + `CGFloat .style.flexGrow` If the sum of childrens' stack dimensions is less than the minimum size, should this object grow? - `BOOL .style.flexShrink` + `CGFloat .style.flexShrink` If the sum of childrens' stack dimensions is greater than the maximum size, should this object shrink? diff --git a/docs/_docs/layout2-layoutspec-types.md b/docs/_docs/layout2-layoutspec-types.md index 42fad5a9b..f1bb432e3 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) } @@ -394,17 +394,17 @@ Within `ASAbsoluteLayoutSpec` you can specify exact locations (x/y coordinates) CGSize maxConstrainedSize = constrainedSize.max; // Layout all nodes absolute in a static layout spec - guitarVideoNode.layoutPosition = CGPointMake(0, 0); - guitarVideoNode.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width, maxConstrainedSize.height / 3.0)); + guitarVideoNode.style.layoutPosition = CGPointMake(0, 0); + guitarVideoNode.style.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width, maxConstrainedSize.height / 3.0)); - nicCageVideoNode.layoutPosition = CGPointMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0); - nicCageVideoNode.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0)); + nicCageVideoNode.style.layoutPosition = CGPointMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0); + nicCageVideoNode.style.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0)); - simonVideoNode.layoutPosition = CGPointMake(0.0, maxConstrainedSize.height - (maxConstrainedSize.height / 3.0)); - simonVideoNode.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width/2, maxConstrainedSize.height / 3.0)); + simonVideoNode.style.layoutPosition = CGPointMake(0.0, maxConstrainedSize.height - (maxConstrainedSize.height / 3.0)); + simonVideoNode.style.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width/2, maxConstrainedSize.height / 3.0)); - hlsVideoNode.layoutPosition = CGPointMake(0.0, maxConstrainedSize.height / 3.0); - hlsVideoNode.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0)); + hlsVideoNode.style.layoutPosition = CGPointMake(0.0, maxConstrainedSize.height / 3.0); + hlsVideoNode.style.size = ASSizeMakeFromCGSize(CGSizeMake(maxConstrainedSize.width / 2.0, maxConstrainedSize.height / 3.0)); return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[guitarVideoNode, nicCageVideoNode, simonVideoNode, hlsVideoNode]]; } diff --git a/docs/_docs/resources.md b/docs/_docs/resources.md index 62113da1e..51a388a7d 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/subclassing.md b/docs/_docs/subclassing.md index 44c3260ee..ea4cd24b9 100755 --- a/docs/_docs/subclassing.md +++ b/docs/_docs/subclassing.md @@ -51,7 +51,7 @@ An `ASViewController` is a regular `UIViewController` subclass that has special ### `-init` -This method is called once, at the very begining of an ASViewController's lifecycle. As with UIViewController initialization, it is best practice to **never access** `self.view` or `self.node.view` in this method as it will force the view to be created early. Instead, do any view access in -viewDidLoad. +This method is called once, at the very beginning of an ASViewController's lifecycle. As with UIViewController initialization, it is best practice to **never access** `self.view` or `self.node.view` in this method as it will force the view to be created early. Instead, do any view access in -viewDidLoad. ASViewController's designated initializer is `initWithNode:`. A typical initializer will look something like the code below. Note how the ASViewController's node is created _before_ calling super. An ASViewController manages a node similarly to how a UIViewController manages a view, but the initialization is slightly different. diff --git a/docs/_docs/subtree-rasterization.md b/docs/_docs/subtree-rasterization.md index 8671a524f..20aa0761b 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 68e9aa457..d4430b751 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 3a5531509..9e98ce2f4 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -210,7 +210,31 @@ permalink: /showcase.html
    Sorted: Master Your Day + + + +
    + Vingle +
    + Improvement feed performance with Texture + + + + + + + +
    + Blendle + + + + +
    + MensXP + + 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 000000000..2a3ac0210 Binary files /dev/null and b/docs/static/images/example-app-screenshots/ASCornerLayoutSpec.png differ diff --git a/examples/ASCollectionView/Podfile b/examples/ASCollectionView/Podfile index 922ff50ec..08d1b7add 100644 --- a/examples/ASCollectionView/Podfile +++ b/examples/ASCollectionView/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj index 634184297..967d1f7cd 100644 --- a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj @@ -240,13 +240,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 */ @@ -302,7 +305,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -338,7 +341,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -353,7 +356,6 @@ ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; INFOPLIST_FILE = Sample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 1; @@ -367,7 +369,6 @@ ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; INFOPLIST_FILE = Sample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 1; diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 3755b01d6..10c974776 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -23,10 +23,13 @@ #define ASYNC_COLLECTION_LAYOUT 0 +static CGSize const kItemSize = (CGSize){180, 90}; + @interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; -@property (nonatomic, strong) NSArray *data; +@property (nonatomic, strong) NSMutableArray *> *data; +@property (nonatomic, strong) UILongPressGestureRecognizer *moveRecognizer; @end @@ -34,18 +37,13 @@ @implementation ViewController #pragma mark - Lifecycle -- (void)dealloc -{ - self.collectionNode.dataSource = nil; - self.collectionNode.delegate = nil; - - NSLog(@"ViewController is deallocing"); -} - - (void)viewDidLoad { [super viewDidLoad]; + self.moveRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress)]; + [self.view addGestureRecognizer:self.moveRecognizer]; + #if ASYNC_COLLECTION_LAYOUT ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections]; layoutDelegate.propertiesProvider = self; @@ -54,6 +52,7 @@ - (void)viewDidLoad UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; layout.headerReferenceSize = CGSizeMake(50.0, 50.0); layout.footerReferenceSize = CGSizeMake(50.0, 50.0); + layout.itemSize = kItemSize; self.collectionNode = [[ASCollectionNode alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; [self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; [self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; @@ -73,34 +72,37 @@ - (void)viewDidLoad self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reloadTapped)]; -#endif - -#if SIMULATE_WEB_RESPONSE + [self loadData]; +#else __weak typeof(self) weakSelf = self; - void(^mockWebService)() = ^{ - NSLog(@"ViewController \"got data from a web service\""); - ViewController *strongSelf = weakSelf; - if (strongSelf != nil) - { - NSLog(@"ViewController is not nil"); - strongSelf->_data = [[NSArray alloc] init]; - [strongSelf->_collectionNode performBatchUpdates:^{ - [strongSelf->_collectionNode insertSections:[[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, 100)]]; - } completion:nil]; - NSLog(@"ViewController finished updating collectionNode"); - } - else { - NSLog(@"ViewController is nil - won't update collectionNode"); - } - }; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), mockWebService); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self.navigationController popViewControllerAnimated:YES]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [weakSelf handleSimulatedWebResponse]; }); #endif } +- (void)handleSimulatedWebResponse +{ + [self.collectionNode performBatchUpdates:^{ + [self loadData]; + [self.collectionNode insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.data.count)]]; + } completion:nil]; +} + +- (void)loadData +{ + // Form our data array + typeof(self.data) data = [NSMutableArray array]; + for (NSInteger s = 0; s < 100; s++) { + NSMutableArray *items = [NSMutableArray array]; + for (NSInteger i = 0; i < 10; i++) { + items[i] = [NSString stringWithFormat:@"[%zd.%zd] says hi", s, i]; + } + data[s] = items; + } + self.data = data; +} + #pragma mark - Button Actions - (void)reloadTapped @@ -115,14 +117,42 @@ - (void)reloadTapped - (CGSize)galleryLayoutDelegate:(ASCollectionGalleryLayoutDelegate *)delegate sizeForElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); - return CGSizeMake(180, 90); + return kItemSize; +} + +- (void)handleLongPress +{ + UICollectionView *collectionView = self.collectionNode.view; + CGPoint location = [self.moveRecognizer locationInView:collectionView]; + switch (self.moveRecognizer.state) { + case UIGestureRecognizerStateBegan: { + NSIndexPath *indexPath = [collectionView indexPathForItemAtPoint:location]; + if (indexPath) { + [collectionView beginInteractiveMovementForItemAtIndexPath:indexPath]; + } + break; + } + case UIGestureRecognizerStateChanged: + [collectionView updateInteractiveMovementTargetPosition:location]; + break; + case UIGestureRecognizerStateEnded: + [collectionView endInteractiveMovement]; + break; + case UIGestureRecognizerStateFailed: + case UIGestureRecognizerStateCancelled: + [collectionView cancelInteractiveMovement]; + break; + case UIGestureRecognizerStatePossible: + // nop + break; + } } -#pragma mark - ASCollectionView Data Source +#pragma mark - ASCollectionDataSource - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath; { - NSString *text = [NSString stringWithFormat:@"[%zd.%zd] says hi", indexPath.section, indexPath.item]; + NSString *text = self.data[indexPath.section][indexPath.item]; return ^{ return [[ItemNode alloc] initWithString:text]; }; @@ -139,18 +169,29 @@ - (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplem - (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section { - return 10; + return self.data[section].count; } - (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode { -#if SIMULATE_WEB_RESPONSE - return _data == nil ? 0 : 100; -#else - return 100; -#endif + return self.data.count; +} + +- (BOOL)collectionNode:(ASCollectionNode *)collectionNode canMoveItemWithNode:(ASCellNode *)node +{ + return YES; } +- (void)collectionNode:(ASCollectionNode *)collectionNode moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath +{ + __auto_type sectionArray = self.data[sourceIndexPath.section]; + __auto_type object = sectionArray[sourceIndexPath.item]; + [sectionArray removeObjectAtIndex:sourceIndexPath.item]; + [self.data[destinationIndexPath.section] insertObject:object atIndex:destinationIndexPath.item]; +} + +#pragma mark - ASCollectionDelegate + - (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context { NSLog(@"fetch additional content"); diff --git a/examples/ASDKLayoutTransition/Podfile b/examples/ASDKLayoutTransition/Podfile index 90c6ad7ea..0fbf4c952 100644 --- a/examples/ASDKLayoutTransition/Podfile +++ b/examples/ASDKLayoutTransition/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' pod 'Texture/Yoga', :path => '../..' diff --git a/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj b/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj index 3b4f1e6f1..17537d093 100644 --- a/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj +++ b/examples/ASDKLayoutTransition/Sample.xcodeproj/project.pbxproj @@ -196,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 # 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; }; F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { @@ -271,7 +274,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -306,7 +309,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/ASDKTube/Podfile b/examples/ASDKTube/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/ASDKTube/Podfile +++ b/examples/ASDKTube/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/ASDKTube/Sample.xcodeproj/project.pbxproj b/examples/ASDKTube/Sample.xcodeproj/project.pbxproj index 0e6c890fa..1b8c982be 100644 --- a/examples/ASDKTube/Sample.xcodeproj/project.pbxproj +++ b/examples/ASDKTube/Sample.xcodeproj/project.pbxproj @@ -277,13 +277,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; }; F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { @@ -356,7 +359,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -391,7 +394,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/ASDKgram/Podfile b/examples/ASDKgram/Podfile index eb879f3f1..4aebd0493 100644 --- a/examples/ASDKgram/Podfile +++ b/examples/ASDKgram/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture/IGListKit', :path => '../..' pod 'Texture/PINRemoteImage', :path => '../..' diff --git a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj index 9cfb0e254..a4b5b7722 100644 --- a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj +++ b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ CC5532171E15CC1E0011C01F /* ASCollectionSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532161E15CC1E0011C01F /* ASCollectionSectionController.m */; }; CC6350BB1E1C482D002BC613 /* TailLoadingNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */; }; CC85250F1E36B392008EABE6 /* FeedHeaderNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */; }; + CCEDDDD7200C4C0E00FFCD0A /* TextureConfigDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDD6200C4C0E00FFCD0A /* TextureConfigDelegate.m */; }; E5F128F01E09625400B4335F /* PhotoFeedBaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = E5F128EF1E09625400B4335F /* PhotoFeedBaseController.m */; }; /* End PBXBuildFile section */ @@ -98,6 +99,7 @@ CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TailLoadingNode.m; sourceTree = ""; }; CC85250D1E36B392008EABE6 /* FeedHeaderNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedHeaderNode.h; sourceTree = ""; }; CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedHeaderNode.m; sourceTree = ""; }; + CCEDDDD6200C4C0E00FFCD0A /* TextureConfigDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TextureConfigDelegate.m; sourceTree = ""; }; D09B5DF0BFB37583DE8F3142 /* 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 = ""; }; E5F128EE1E09612700B4335F /* PhotoFeedBaseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoFeedBaseController.h; sourceTree = ""; }; E5F128EF1E09625400B4335F /* PhotoFeedBaseController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoFeedBaseController.m; sourceTree = ""; }; @@ -141,6 +143,7 @@ children = ( 768843511CAA37EF00D8629E /* AppDelegate.h */, 768843681CAA37EF00D8629E /* AppDelegate.m */, + CCEDDDD6200C4C0E00FFCD0A /* TextureConfigDelegate.m */, 76229A761CBB79E000B62CEF /* WindowWithStatusBarUnderlay.h */, 76229A771CBB79E000B62CEF /* WindowWithStatusBarUnderlay.m */, 767A5F141CAA3D8A004CDA8D /* Controller */, @@ -381,13 +384,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; }; F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { @@ -424,6 +430,7 @@ CC5532171E15CC1E0011C01F /* ASCollectionSectionController.m in Sources */, 768843801CAA37EF00D8629E /* AppDelegate.m in Sources */, 768843811CAA37EF00D8629E /* CommentFeedModel.m in Sources */, + CCEDDDD7200C4C0E00FFCD0A /* TextureConfigDelegate.m in Sources */, 7688438E1CAA37EF00D8629E /* PhotoFeedNodeController.m in Sources */, CC6350BB1E1C482D002BC613 /* TailLoadingNode.m in Sources */, CC85250F1E36B392008EABE6 /* FeedHeaderNode.m in Sources */, @@ -477,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -512,7 +519,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/ASDKgram/Sample/AppDelegate.m b/examples/ASDKgram/Sample/AppDelegate.m index 16484467f..e1dd507ba 100644 --- a/examples/ASDKgram/Sample/AppDelegate.m +++ b/examples/ASDKgram/Sample/AppDelegate.m @@ -7,7 +7,7 @@ // 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, +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 @@ -22,6 +22,8 @@ #import "WindowWithStatusBarUnderlay.h" #import "Utilities.h" +#import + #define WEAVER 0 #if WEAVER diff --git a/examples/ASDKgram/Sample/PhotoCellNode.m b/examples/ASDKgram/Sample/PhotoCellNode.m index d424587c6..a094ad560 100644 --- a/examples/ASDKgram/Sample/PhotoCellNode.m +++ b/examples/ASDKgram/Sample/PhotoCellNode.m @@ -44,6 +44,9 @@ #define InsetForHeader UIEdgeInsetsMake(0, HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER) #define InsetForFooter UIEdgeInsetsMake(VERTICAL_BUFFER, HORIZONTAL_BUFFER, VERTICAL_BUFFER, HORIZONTAL_BUFFER) +@interface PhotoCellNode () +@end + @implementation PhotoCellNode { PhotoModel *_photoModel; @@ -77,6 +80,7 @@ - (instancetype)initWithPhotoObject:(PhotoModel *)photo; }]; _photoImageNode = [[ASNetworkImageNode alloc] init]; + _photoImageNode.delegate = self; _photoImageNode.URL = photo.URL; _photoImageNode.layerBacked = YES; @@ -284,6 +288,19 @@ - (void)didEnterPreloadState }]; } +#pragma mark - Network Image Delegate + +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageLoadInfo *)info +{ + // Docs say method is called from bg but right now it's called from main. + // Save main thread time by shunting this. + if (info.sourceType == ASNetworkImageSourceDownload) { + ASPerformBlockOnBackgroundThread(^{ + NSLog(@"Received image %@ from %@ with userInfo %@", image, info.url.path, ASObjectDescriptionMakeTiny(info.userInfo)); + }); + } +} + #pragma mark - Helper Methods - (ASTextNode *)createLayerBackedTextNodeWithString:(NSAttributedString *)attributedString diff --git a/examples/ASDKgram/Sample/PhotoFeedModel.m b/examples/ASDKgram/Sample/PhotoFeedModel.m index 27cf185e9..afac8ca9f 100644 --- a/examples/ASDKgram/Sample/PhotoFeedModel.m +++ b/examples/ASDKgram/Sample/PhotoFeedModel.m @@ -1,20 +1,18 @@ // // PhotoFeedModel.m -// Sample -// -// Created by Hannah Troisi on 2/28/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. +// +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import "PhotoFeedModel.h" @@ -184,9 +182,11 @@ - (void)fetchPageWithCompletionBlock:(void (^)(NSArray *))block numResultsToRetu // early return if reached end of pages if (_totalPages) { if (_currentPage == _totalPages) { - if (block){ - block(@[]); - } + dispatch_async(dispatch_get_main_queue(), ^{ + if (block) { + block(@[]); + } + }); return; } } diff --git a/examples/ASDKgram/Sample/PhotoModel.m b/examples/ASDKgram/Sample/PhotoModel.m index 575a7049a..4c852d596 100644 --- a/examples/ASDKgram/Sample/PhotoModel.m +++ b/examples/ASDKgram/Sample/PhotoModel.m @@ -1,20 +1,18 @@ // // PhotoModel.m -// Sample -// -// Created by Hannah Troisi on 2/26/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. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import "PhotoModel.h" @@ -53,7 +51,7 @@ - (instancetype)initWith500pxPhoto:(NSDictionary *)photoDictionary _commentsCount = [[photoDictionary objectForKey:@"comments_count"] integerValue]; _likesCount = [[photoDictionary objectForKey:@"positive_votes_count"] integerValue]; - NSString *urlString = [photoDictionary objectForKey:@"image_url"]; + NSString *urlString = [[photoDictionary objectForKey:@"image_url"] firstObject]; _URL = urlString ? [NSURL URLWithString:urlString] : nil; _location = [[LocationModel alloc] initWith500pxPhoto:photoDictionary]; diff --git a/examples/ASDKgram/Sample/TextureConfigDelegate.m b/examples/ASDKgram/Sample/TextureConfigDelegate.m new file mode 100644 index 000000000..0905a646e --- /dev/null +++ b/examples/ASDKgram/Sample/TextureConfigDelegate.m @@ -0,0 +1,41 @@ +// +// TextureConfigDelegate.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface TextureConfigDelegate : NSObject + +@end + +@implementation ASConfiguration (UserProvided) + ++ (ASConfiguration *)textureConfiguration +{ + ASConfiguration *config = [[ASConfiguration alloc] init]; + config.experimentalFeatures = ASExperimentalGraphicsContexts | ASExperimentalTextNode; + config.delegate = [[TextureConfigDelegate alloc] init]; + return config; +} + +@end + +@implementation TextureConfigDelegate + +- (void)textureDidActivateExperimentalFeatures:(ASExperimentalFeatures)features +{ + if (features & ASExperimentalGraphicsContexts) { + NSLog(@"Texture activated experimental graphics contexts."); + } +} + +@end + diff --git a/examples/ASMapNode/Podfile b/examples/ASMapNode/Podfile index 922ff50ec..08d1b7add 100644 --- a/examples/ASMapNode/Podfile +++ b/examples/ASMapNode/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/ASMapNode/Sample.xcodeproj/project.pbxproj b/examples/ASMapNode/Sample.xcodeproj/project.pbxproj index 529b58783..48093ffda 100644 --- a/examples/ASMapNode/Sample.xcodeproj/project.pbxproj +++ b/examples/ASMapNode/Sample.xcodeproj/project.pbxproj @@ -214,13 +214,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 */ @@ -288,7 +291,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -325,7 +328,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/ASViewController/Podfile b/examples/ASViewController/Podfile index 922ff50ec..08d1b7add 100644 --- a/examples/ASViewController/Podfile +++ b/examples/ASViewController/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/ASViewController/Sample.xcodeproj/project.pbxproj b/examples/ASViewController/Sample.xcodeproj/project.pbxproj index 200b3c225..e304fdb8e 100644 --- a/examples/ASViewController/Sample.xcodeproj/project.pbxproj +++ b/examples/ASViewController/Sample.xcodeproj/project.pbxproj @@ -219,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 # 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 */ @@ -294,7 +297,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -331,7 +334,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/ASViewController/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASViewController/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples/ASViewController/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/AnimatedGIF/Podfile b/examples/AnimatedGIF/Podfile index e784c52d1..c998fa0a8 100644 --- a/examples/AnimatedGIF/Podfile +++ b/examples/AnimatedGIF/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' pod 'PINRemoteImage/WebP' diff --git a/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj b/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj index 7498a38dd..453c359c0 100644 --- a/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj +++ b/examples/AnimatedGIF/Sample.xcodeproj/project.pbxproj @@ -214,13 +214,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 */ diff --git a/examples/AsyncDisplayKitOverview/Podfile b/examples/AsyncDisplayKitOverview/Podfile index 84d7dee82..ff6cb63a3 100644 --- a/examples/AsyncDisplayKitOverview/Podfile +++ b/examples/AsyncDisplayKitOverview/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '8.0' +platform :ios, '9.0' # Uncomment this line if you're using Swift # use_frameworks! diff --git a/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.pbxproj b/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.pbxproj index 8d5abb4bb..3683dbd63 100644 --- a/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.pbxproj +++ b/examples/AsyncDisplayKitOverview/Sample.xcodeproj/project.pbxproj @@ -217,13 +217,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; }; 84F93825AFB1CA7FBB116BA4 /* [CP] Copy Pods Resources */ = { @@ -309,7 +312,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -348,7 +351,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/examples/CatDealsCollectionView/Podfile b/examples/CatDealsCollectionView/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/CatDealsCollectionView/Podfile +++ b/examples/CatDealsCollectionView/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj b/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj index 5a9485def..29fb32d34 100644 --- a/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/CatDealsCollectionView/Sample.xcodeproj/project.pbxproj @@ -242,13 +242,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 */ @@ -308,7 +311,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -344,7 +347,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/examples/CatDealsCollectionView/Sample/BlurbNode.h b/examples/CatDealsCollectionView/Sample/BlurbNode.h index e6574bcd0..e40f99d8a 100644 --- a/examples/CatDealsCollectionView/Sample/BlurbNode.h +++ b/examples/CatDealsCollectionView/Sample/BlurbNode.h @@ -1,18 +1,18 @@ // // BlurbNode.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. // -// 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) through the 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 diff --git a/examples/CatDealsCollectionView/Sample/BlurbNode.m b/examples/CatDealsCollectionView/Sample/BlurbNode.m index 7dbe86a8b..0b358273c 100644 --- a/examples/CatDealsCollectionView/Sample/BlurbNode.m +++ b/examples/CatDealsCollectionView/Sample/BlurbNode.m @@ -1,18 +1,18 @@ // // BlurbNode.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. // -// 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) through the 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 "BlurbNode.h" @@ -57,16 +57,15 @@ - (instancetype)init NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; [string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue-Light" size:16.0f] range:NSMakeRange(0, blurb.length)]; [string addAttributes:@{ - NSLinkAttributeName: [NSURL URLWithString:@"http://lorempixel.com/"], - NSForegroundColorAttributeName: [UIColor blueColor], - NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), - } - range:[blurb rangeOfString:@"lorempixel.com"]]; + NSLinkAttributeName: [NSURL URLWithString:@"http://lorempixel.com/"], + NSForegroundColorAttributeName: [UIColor blueColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } range:[blurb rangeOfString:@"lorempixel.com"]]; [string addAttributes:@{ - NSLinkAttributeName: [NSURL URLWithString:@"http://www.catipsum.com/"], - NSForegroundColorAttributeName: [UIColor blueColor], - NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), - } range:[blurb rangeOfString:@"catipsum.com"]]; + NSLinkAttributeName: [NSURL URLWithString:@"http://www.catipsum.com/"], + NSForegroundColorAttributeName: [UIColor blueColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } range:[blurb rangeOfString:@"catipsum.com"]]; _textNode.attributedText = string; // add it as a subnode, and we're done @@ -90,7 +89,7 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; centerSpec.child = _textNode; - UIEdgeInsets padding =UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + UIEdgeInsets padding = UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; } diff --git a/examples/CatDealsCollectionView/Sample/ItemNode.h b/examples/CatDealsCollectionView/Sample/ItemNode.h index 678327e11..bb0f06857 100644 --- a/examples/CatDealsCollectionView/Sample/ItemNode.h +++ b/examples/CatDealsCollectionView/Sample/ItemNode.h @@ -20,7 +20,6 @@ @interface ItemNode : ASCellNode -- initWithViewModel:(ItemViewModel *)viewModel; + (CGSize)sizeForWidth:(CGFloat)width; + (CGSize)preferredViewSize; diff --git a/examples/CatDealsCollectionView/Sample/ItemNode.m b/examples/CatDealsCollectionView/Sample/ItemNode.m index 400f5b79e..4fd76e8a0 100644 --- a/examples/CatDealsCollectionView/Sample/ItemNode.m +++ b/examples/CatDealsCollectionView/Sample/ItemNode.m @@ -20,6 +20,16 @@ #import "PlaceholderNetworkImageNode.h" #import +static CGFloat ASIsRTL() +{ + static BOOL __isRTL = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __isRTL = [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; + }); + return __isRTL; +} + const CGFloat kFixedLabelsAreaHeight = 96.0; const CGFloat kDesignWidth = 320.0; const CGFloat kDesignHeight = 299.0; @@ -28,7 +38,7 @@ @interface ItemNode() -@property (nonatomic, strong) ItemViewModel *viewModel; +@property (nonatomic, strong) ItemViewModel *nodeModel; @property (nonatomic, strong) PlaceholderNetworkImageNode *dealImageView; @@ -46,28 +56,35 @@ @interface ItemNode() @end @implementation ItemNode -@dynamic viewModel; +// Defined on ASCellNode +@dynamic nodeModel; -- (instancetype)initWithViewModel:(ItemViewModel *)viewModel ++ (void)load +{ + // Need to happen on main thread. + ASIsRTL(); +} + +- (instancetype)init { self = [super init]; if (self != nil) { - self.viewModel = viewModel; - [self setup]; - [self updateLabels]; + [self setupNodes]; [self updateBackgroundColor]; - - ASSetDebugName(self, @"Item #%zd", viewModel.identifier); - self.accessibilityIdentifier = viewModel.titleText; } return self; } -+ (BOOL)isRTL { - return [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; +- (void)setNodeModel:(ItemViewModel *)nodeModel +{ + [super setNodeModel:nodeModel]; + + [self updateLabels]; + [self updateAccessibilityIdentifier]; } -- (void)setup { +- (void)setupNodes +{ self.dealImageView = [[PlaceholderNetworkImageNode alloc] init]; self.dealImageView.delegate = self; self.dealImageView.placeholderEnabled = YES; @@ -138,7 +155,7 @@ - (void)setup { self.soldOutLabelBackground.hidden = YES; self.soldOutLabelFlat.hidden = YES; - if ([ItemNode isRTL]) { + if (ASIsRTL()) { self.titleLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; self.firstInfoLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; self.distanceLabel.style.alignSelf = ASStackLayoutAlignSelfEnd; @@ -154,49 +171,56 @@ - (void)setup { } } -- (void)updateLabels { +- (void)updateLabels +{ // Set Title text - if (self.viewModel.titleText) { - self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:self.viewModel.titleText attributes:[ItemStyles titleStyle]]; + if (self.nodeModel.titleText) { + self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.titleText attributes:[ItemStyles titleStyle]]; } - if (self.viewModel.firstInfoText) { - self.firstInfoLabel.attributedText = [[NSAttributedString alloc] initWithString:self.viewModel.firstInfoText attributes:[ItemStyles subtitleStyle]]; + if (self.nodeModel.firstInfoText) { + self.firstInfoLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.firstInfoText attributes:[ItemStyles subtitleStyle]]; } - if (self.viewModel.secondInfoText) { - self.secondInfoLabel.attributedText = [[NSAttributedString alloc] initWithString:self.viewModel.secondInfoText attributes:[ItemStyles secondInfoStyle]]; + if (self.nodeModel.secondInfoText) { + self.secondInfoLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.secondInfoText attributes:[ItemStyles secondInfoStyle]]; } - if (self.viewModel.originalPriceText) { - self.originalPriceLabel.attributedText = [[NSAttributedString alloc] initWithString:self.viewModel.originalPriceText attributes:[ItemStyles originalPriceStyle]]; + if (self.nodeModel.originalPriceText) { + self.originalPriceLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.originalPriceText attributes:[ItemStyles originalPriceStyle]]; } - if (self.viewModel.finalPriceText) { - self.finalPriceLabel.attributedText = [[NSAttributedString alloc] initWithString:self.viewModel.finalPriceText attributes:[ItemStyles finalPriceStyle]]; + if (self.nodeModel.finalPriceText) { + self.finalPriceLabel.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.finalPriceText attributes:[ItemStyles finalPriceStyle]]; } - if (self.viewModel.distanceLabelText) { - NSString *format = [ItemNode isRTL] ? @"%@ •" : @"• %@"; - NSString *distanceText = [NSString stringWithFormat:format, self.viewModel.distanceLabelText]; + if (self.nodeModel.distanceLabelText) { + NSString *format = ASIsRTL() ? @"%@ •" : @"• %@"; + NSString *distanceText = [NSString stringWithFormat:format, self.nodeModel.distanceLabelText]; self.distanceLabel.attributedText = [[NSAttributedString alloc] initWithString:distanceText attributes:[ItemStyles distanceStyle]]; } - BOOL isSoldOut = self.viewModel.soldOutText != nil; + BOOL isSoldOut = self.nodeModel.soldOutText != nil; if (isSoldOut) { - NSString *soldOutText = self.viewModel.soldOutText; + NSString *soldOutText = self.nodeModel.soldOutText; self.soldOutLabelFlat.attributedText = [[NSAttributedString alloc] initWithString:soldOutText attributes:[ItemStyles soldOutStyle]]; } self.soldOutOverlay.hidden = !isSoldOut; self.soldOutLabelFlat.hidden = !isSoldOut; self.soldOutLabelBackground.hidden = !isSoldOut; - BOOL hasBadge = self.viewModel.badgeText != nil; + BOOL hasBadge = self.nodeModel.badgeText != nil; if (hasBadge) { - self.badge.attributedText = [[NSAttributedString alloc] initWithString:self.viewModel.badgeText attributes:[ItemStyles badgeStyle]]; + self.badge.attributedText = [[NSAttributedString alloc] initWithString:self.nodeModel.badgeText attributes:[ItemStyles badgeStyle]]; self.badge.backgroundColor = [ItemStyles badgeColor]; } self.badge.hidden = !hasBadge; } +- (void)updateAccessibilityIdentifier +{ + ASSetDebugName(self, @"Item #%zd", self.nodeModel.identifier); + self.accessibilityIdentifier = self.nodeModel.titleText; +} + - (void)updateBackgroundColor { if (self.highlighted) { @@ -208,9 +232,6 @@ - (void)updateBackgroundColor } } -- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image { -} - - (void)setSelected:(BOOL)selected { [super setSelected:selected]; @@ -223,21 +244,22 @@ - (void)setHighlighted:(BOOL)highlighted [self updateBackgroundColor]; } -#pragma mark - superclass +#pragma mark - ASDisplayNode -- (void)displayWillStart { +- (void)displayWillStart +{ [super displayWillStart]; [self didEnterPreloadState]; } -- (void)didEnterPreloadState { +- (void)didEnterPreloadState +{ [super didEnterPreloadState]; - if (self.viewModel) { + if (self.nodeModel) { [self loadImage]; } } - - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { ASLayoutSpec *textSpec = [self textSpec]; @@ -253,7 +275,8 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { return soldOutOverlay; } -- (ASLayoutSpec *)textSpec { +- (ASLayoutSpec *)textSpec +{ CGFloat kInsetHorizontal = 16.0; CGFloat kInsetTop = 6.0; CGFloat kInsetBottom = 0.0; @@ -271,7 +294,7 @@ - (ASLayoutSpec *)textSpec { NSArray *info1Children = @[self.firstInfoLabel, self.distanceLabel, horizontalSpacer1, self.originalPriceLabel]; NSArray *info2Children = @[self.secondInfoLabel, horizontalSpacer2, self.finalPriceLabel]; - if ([ItemNode isRTL]) { + if (ASIsRTL()) { info1Children = [[info1Children reverseObjectEnumerator] allObjects]; info2Children = [[info2Children reverseObjectEnumerator] allObjects]; } @@ -288,7 +311,8 @@ - (ASLayoutSpec *)textSpec { return textWrapper; } -- (ASLayoutSpec *)imageSpecWithSize:(ASSizeRange)constrainedSize { +- (ASLayoutSpec *)imageSpecWithSize:(ASSizeRange)constrainedSize +{ CGFloat imageRatio = [self imageRatioFromSize:constrainedSize.max]; ASRatioLayoutSpec *imagePlace = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:imageRatio child:self.dealImageView]; @@ -310,34 +334,38 @@ - (ASLayoutSpec *)soldOutLabelSpec { return soldOutLabelOverBackground; } - -+ (CGSize)sizeForWidth:(CGFloat)width { ++ (CGSize)sizeForWidth:(CGFloat)width +{ CGFloat height = [self scaledHeightForPreferredSize:[self preferredViewSize] scaledWidth:width]; return CGSizeMake(width, height); } -+ (CGSize)preferredViewSize { ++ (CGSize)preferredViewSize +{ return CGSizeMake(kDesignWidth, kDesignHeight); } -+ (CGFloat)scaledHeightForPreferredSize:(CGSize)preferredSize scaledWidth:(CGFloat)scaledWidth { ++ (CGFloat)scaledHeightForPreferredSize:(CGSize)preferredSize scaledWidth:(CGFloat)scaledWidth +{ CGFloat scale = scaledWidth / kDesignWidth; CGFloat scaledHeight = ceilf(scale * (kDesignHeight - kFixedLabelsAreaHeight)) + kFixedLabelsAreaHeight; return scaledHeight; } -#pragma mark - view operations +#pragma mark - Image -- (CGFloat)imageRatioFromSize:(CGSize)size { +- (CGFloat)imageRatioFromSize:(CGSize)size +{ CGFloat imageHeight = size.height - kFixedLabelsAreaHeight; CGFloat imageRatio = imageHeight / size.width; return imageRatio; } -- (CGSize)imageSize { +- (CGSize)imageSize +{ if (!CGSizeEqualToSize(self.dealImageView.frame.size, CGSizeZero)) { return self.dealImageView.frame.size; } else if (!CGSizeEqualToSize(self.calculatedSize, CGSizeZero)) { @@ -349,13 +377,14 @@ - (CGSize)imageSize { } } -- (void)loadImage { +- (void)loadImage +{ CGSize imageSize = [self imageSize]; if (CGSizeEqualToSize(CGSizeZero, imageSize)) { return; } - NSURL *url = [self.viewModel imageURLWithSize:imageSize]; + NSURL *url = [self.nodeModel imageURLWithSize:imageSize]; // if we're trying to set the deal image to what it already was, skip the work if ([[url absoluteString] isEqualToString:[self.dealImageView.URL absoluteString]]) { diff --git a/examples/CatDealsCollectionView/Sample/ItemStyles.h b/examples/CatDealsCollectionView/Sample/ItemStyles.h index a5af90a01..09d3b81c8 100644 --- a/examples/CatDealsCollectionView/Sample/ItemStyles.h +++ b/examples/CatDealsCollectionView/Sample/ItemStyles.h @@ -1,20 +1,18 @@ // // ItemStyles.h -// Sample -// -// Created by Samuel Stow on 12/30/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. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import diff --git a/examples/CatDealsCollectionView/Sample/ItemStyles.m b/examples/CatDealsCollectionView/Sample/ItemStyles.m index 12871e3be..690980d49 100644 --- a/examples/CatDealsCollectionView/Sample/ItemStyles.m +++ b/examples/CatDealsCollectionView/Sample/ItemStyles.m @@ -1,20 +1,18 @@ // // ItemStyles.m -// Sample -// -// Created by Samuel Stow on 12/30/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. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import "ItemStyles.h" @@ -95,9 +93,10 @@ + (UIColor *)badgeColor { + (UIImage *)placeholderImage { static UIImage *__catFace = nil; - if (!__catFace) { + static dispatch_once_t onceToken; + dispatch_once (&onceToken, ^{ __catFace = [UIImage imageNamed:@"cat_face"]; - } + }); return __catFace; } diff --git a/examples/CatDealsCollectionView/Sample/ItemViewModel.m b/examples/CatDealsCollectionView/Sample/ItemViewModel.m index 4cf925e5d..6206e399c 100644 --- a/examples/CatDealsCollectionView/Sample/ItemViewModel.m +++ b/examples/CatDealsCollectionView/Sample/ItemViewModel.m @@ -35,7 +35,8 @@ + (ItemViewModel *)randomItem { return [[ItemViewModel alloc] init]; } -- (instancetype)init { +- (instancetype)init +{ self = [super init]; if (self) { static _Atomic(NSInteger) nextID = ATOMIC_VAR_INIT(1); @@ -45,11 +46,9 @@ - (instancetype)init { _secondInfoText = [NSString stringWithFormat:@"%zd+ bought", [self randomNumberInRange:5 to:6000]]; _originalPriceText = [NSString stringWithFormat:@"$%zd", [self randomNumberInRange:40 to:90]]; _finalPriceText = [NSString stringWithFormat:@"$%zd", [self randomNumberInRange:5 to:30]]; - BOOL isSoldOut = arc4random() % 5 == 0; - _soldOutText = isSoldOut ? @"SOLD OUT" : nil; + _soldOutText = (arc4random() % 5 == 0) ? @"SOLD OUT" : nil; _distanceLabelText = [NSString stringWithFormat:@"%zd mi", [self randomNumberInRange:1 to:20]]; - BOOL isBadged = arc4random() % 2 == 0; - if (isBadged) { + if (arc4random() % 2 == 0) { _badgeText = [self randomObjectFromArray:badges]; } _catNumber = [self randomNumberInRange:1 to:10]; @@ -58,18 +57,20 @@ - (instancetype)init { return self; } -- (NSURL *)imageURLWithSize:(CGSize)size { +- (NSURL *)imageURLWithSize:(CGSize)size +{ NSString *imageText = [NSString stringWithFormat:@"Fun cat pic %zd", self.labelNumber]; NSString *urlString = [NSString stringWithFormat:@"http://lorempixel.com/%zd/%zd/cats/%zd/%@", (NSInteger)roundl(size.width), (NSInteger)roundl(size.height), self.catNumber, imageText]; - urlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + + urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]]; return [NSURL URLWithString:urlString]; } // titles courtesy of http://www.catipsum.com/ -+ (void)initialize { ++ (void)initialize +{ titles = @[@"Leave fur on owners clothes intrigued by the shower", @"Meowwww", @"Immediately regret falling into bathtub stare out the window", diff --git a/examples/CatDealsCollectionView/Sample/LoadingNode.h b/examples/CatDealsCollectionView/Sample/LoadingNode.h index d144de01a..6c0715726 100644 --- a/examples/CatDealsCollectionView/Sample/LoadingNode.h +++ b/examples/CatDealsCollectionView/Sample/LoadingNode.h @@ -1,20 +1,18 @@ // // LoadingNode.h -// Sample -// -// Created by Samuel Stow on 1/9/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. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import diff --git a/examples/CatDealsCollectionView/Sample/LoadingNode.m b/examples/CatDealsCollectionView/Sample/LoadingNode.m index 4fa29d6e3..03a6825c6 100644 --- a/examples/CatDealsCollectionView/Sample/LoadingNode.m +++ b/examples/CatDealsCollectionView/Sample/LoadingNode.m @@ -1,41 +1,29 @@ // // LoadingNode.m -// Sample -// -// Created by Samuel Stow on 1/9/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. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import "LoadingNode.h" -#import -#import -#import #import -@interface LoadingNode () -{ +@implementation LoadingNode { ASDisplayNode *_loadingSpinner; } -@end - -@implementation LoadingNode - - -#pragma mark - -#pragma mark ASCellNode. +#pragma mark - ASCellNode - (instancetype)init { @@ -61,7 +49,6 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize centerSpec.centeringOptions = ASCenterLayoutSpecCenteringXY; centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionDefault; centerSpec.child = _loadingSpinner; - return centerSpec; } diff --git a/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h index 53ba71544..31470610b 100644 --- a/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h +++ b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.h @@ -1,20 +1,18 @@ // // PlaceholderNetworkImageNode.h -// Sample -// -// Created by Samuel Stow on 1/14/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. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import diff --git a/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m index cb1b7e81b..e7f0de47f 100644 --- a/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m +++ b/examples/CatDealsCollectionView/Sample/PlaceholderNetworkImageNode.m @@ -1,29 +1,27 @@ // // PlaceholderNetworkImageNode.m -// Sample -// -// Created by Samuel Stow on 1/14/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. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) through the 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // #import "PlaceholderNetworkImageNode.h" @implementation PlaceholderNetworkImageNode -- (UIImage *)placeholderImage { +- (UIImage *)placeholderImage +{ return self.placeholderImageOverride; } - @end diff --git a/examples/CatDealsCollectionView/Sample/ViewController.m b/examples/CatDealsCollectionView/Sample/ViewController.m index 6e63dabe0..78b58d67a 100644 --- a/examples/CatDealsCollectionView/Sample/ViewController.m +++ b/examples/CatDealsCollectionView/Sample/ViewController.m @@ -50,7 +50,6 @@ - (instancetype)init self = [super initWithNode:_collectionNode]; if (self) { - self.title = @"Cat Deals"; _collectionNode.dataSource = self; @@ -89,7 +88,8 @@ - (void)viewDidLoad [self fetchMoreCatsWithCompletion:nil]; } -- (void)fetchMoreCatsWithCompletion:(void (^)(BOOL))completion { +- (void)fetchMoreCatsWithCompletion:(void (^)(BOOL))completion +{ if (kSimulateWebResponse) { __weak typeof(self) weakSelf = self; void(^mockWebService)() = ^{ @@ -110,7 +110,8 @@ - (void)fetchMoreCatsWithCompletion:(void (^)(BOOL))completion { } } -- (void)appendMoreItems:(NSInteger)numberOfNewItems completion:(void (^)(BOOL))completion { +- (void)appendMoreItems:(NSInteger)numberOfNewItems completion:(void (^)(BOOL))completion +{ NSArray *newData = [self getMoreData:numberOfNewItems]; [_collectionNode performBatchAnimated:YES updates:^{ [_data addObjectsFromArray:newData]; @@ -119,7 +120,8 @@ - (void)appendMoreItems:(NSInteger)numberOfNewItems completion:(void (^)(BOOL))c } completion:completion]; } -- (NSArray *)getMoreData:(NSInteger)count { +- (NSArray *)getMoreData:(NSInteger)count +{ NSMutableArray *data = [NSMutableArray array]; for (int i = 0; i < count; i++) { [data addObject:[ItemViewModel randomItem]]; @@ -127,7 +129,8 @@ - (NSArray *)getMoreData:(NSInteger)count { return data; } -- (NSArray *)indexPathsForObjects:(NSArray *)data { +- (NSArray *)indexPathsForObjects:(NSArray *)data +{ NSMutableArray *indexPaths = [NSMutableArray array]; NSInteger section = 0; for (ItemViewModel *viewModel in data) { @@ -138,7 +141,8 @@ - (NSArray *)indexPathsForObjects:(NSArray *)data { return indexPaths; } -- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ [_collectionNode.view.collectionViewLayout invalidateLayout]; } @@ -151,12 +155,16 @@ - (void)reloadTapped - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { - ItemViewModel *viewModel = _data[indexPath.item]; return ^{ - return [[ItemNode alloc] initWithViewModel:viewModel]; + return [[ItemNode alloc] init]; }; } +- (id)collectionNode:(ASCollectionNode *)collectionNode nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return _data[indexPath.item]; +} + - (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { if ([kind isEqualToString:UICollectionElementKindSectionHeader] && indexPath.section == 0) { diff --git a/examples/CustomCollectionView-Swift/Podfile b/examples/CustomCollectionView-Swift/Podfile index a0eec32ed..92a9acc9c 100644 --- a/examples/CustomCollectionView-Swift/Podfile +++ b/examples/CustomCollectionView-Swift/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' use_frameworks! diff --git a/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj b/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj index 1ecfa3fb9..e1912f909 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 = 9.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 = 9.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 1c12aaa4d..ec900dc24 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 000000000..7b5a2f305 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/CustomCollectionView/Podfile b/examples/CustomCollectionView/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/CustomCollectionView/Podfile +++ b/examples/CustomCollectionView/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj b/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj index 91818f8f8..edaa0d127 100644 --- a/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj @@ -228,13 +228,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 */ @@ -291,7 +294,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -327,7 +330,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -342,7 +345,6 @@ ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; DEVELOPMENT_TEAM = XSR3D45JSF; INFOPLIST_FILE = Sample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample.CustomCollectionView; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -357,7 +359,6 @@ ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; DEVELOPMENT_TEAM = XSR3D45JSF; INFOPLIST_FILE = Sample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample.CustomCollectionView; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/examples/CustomCollectionView/Sample/AppDelegate.m b/examples/CustomCollectionView/Sample/AppDelegate.m index c0769e5d5..dec3c29fe 100644 --- a/examples/CustomCollectionView/Sample/AppDelegate.m +++ b/examples/CustomCollectionView/Sample/AppDelegate.m @@ -4,15 +4,15 @@ // // 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) through the 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 "AppDelegate.h" @@ -23,7 +23,7 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = [[ViewController alloc] init]; diff --git a/examples/HorizontalWithinVerticalScrolling/Podfile b/examples/HorizontalWithinVerticalScrolling/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/HorizontalWithinVerticalScrolling/Podfile +++ b/examples/HorizontalWithinVerticalScrolling/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj index c99967990..6472106a7 100644 --- a/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj +++ b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj @@ -123,12 +123,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - A79A9172A45D7C9595AA01CC /* Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + A79A9172A45D7C9595AA01CC /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -185,14 +185,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - A79A9172A45D7C9595AA01CC /* Embed Pods Frameworks */ = { + A79A9172A45D7C9595AA01CC /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -200,29 +200,32 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Check Pods 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; }; - F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -282,7 +285,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -317,7 +320,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/Kittens/Podfile b/examples/Kittens/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/Kittens/Podfile +++ b/examples/Kittens/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/Kittens/Sample.xcodeproj/project.pbxproj b/examples/Kittens/Sample.xcodeproj/project.pbxproj index 7e5f1a9ef..3c2cb1a59 100644 --- a/examples/Kittens/Sample.xcodeproj/project.pbxproj +++ b/examples/Kittens/Sample.xcodeproj/project.pbxproj @@ -125,12 +125,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - 9C2078E0C7EEEFF207C7F6A9 /* Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 9C2078E0C7EEEFF207C7F6A9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -187,14 +187,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 9C2078E0C7EEEFF207C7F6A9 /* Embed Pods Frameworks */ = { + 9C2078E0C7EEEFF207C7F6A9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -202,29 +202,32 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Check Pods 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; }; - F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -285,7 +288,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -320,7 +323,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/Kittens/Sample/AppDelegate.m b/examples/Kittens/Sample/AppDelegate.m index 230173d51..e683d16d4 100644 --- a/examples/Kittens/Sample/AppDelegate.m +++ b/examples/Kittens/Sample/AppDelegate.m @@ -1,18 +1,18 @@ // // AppDelegate.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. // -// 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) through the 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 "AppDelegate.h" @@ -24,7 +24,6 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [ASTextNode setExperimentOptions:ASTextNodeExperimentRandomInstances]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index 63a662327..38d01d6f5 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -1,18 +1,18 @@ // // ViewController.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. // -// 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) through the 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" @@ -35,12 +35,10 @@ @interface ViewController () // array of boxed CGSizes corresponding to placekitten.com kittens NSMutableArray *_kittenDataSource; - BOOL _dataSourceLocked; NSIndexPath *_blurbNodeIndexPath; } @property (nonatomic, strong) NSMutableArray *kittenDataSource; -@property (atomic, assign) BOOL dataSourceLocked; @end @@ -96,12 +94,6 @@ - (NSMutableArray *)createLitterWithSize:(NSInteger)litterSize return kittens; } -- (void)setKittenDataSource:(NSMutableArray *)kittenDataSource { - ASDisplayNodeAssert(!self.dataSourceLocked, @"Could not update data source when it is locked !"); - - _kittenDataSource = kittenDataSource; -} - - (void)toggleEditingMode { [_tableNode.view setEditing:!_tableNode.view.editing animated:YES]; diff --git a/examples/LayoutSpecExamples-Swift/Podfile b/examples/LayoutSpecExamples-Swift/Podfile index 8458e64c6..83d2cae8b 100644 --- a/examples/LayoutSpecExamples-Swift/Podfile +++ b/examples/LayoutSpecExamples-Swift/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' use_frameworks! diff --git a/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj b/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/project.pbxproj index 557269520..5ac0a8718 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; @@ -292,7 +310,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -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; @@ -331,7 +355,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/LayoutSpecExamples-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index 34c6101e3..05f94265c 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 000000000..7b5a2f305 --- /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 eeeba4f0a..6cce6558a 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 75531d902..8be335683 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 6e5240c7d..e4b6f5d5a 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 683130618..1f1bad89c 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/Podfile b/examples/LayoutSpecExamples/Podfile index 922ff50ec..08d1b7add 100644 --- a/examples/LayoutSpecExamples/Podfile +++ b/examples/LayoutSpecExamples/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj b/examples/LayoutSpecExamples/Sample.xcodeproj/project.pbxproj index 802713181..c065fcab3 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 */ @@ -296,7 +299,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -333,7 +336,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata b/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /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 7355285d3..65241146b 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 e7f9fbe6f..9b84cb16e 100644 --- a/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m +++ b/examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m @@ -260,6 +260,188 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize @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 e3f44dfb0..c3cd7f9b6 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 @@ - (instancetype)init _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 0fcb0ece9..b4bf2f824 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 74b4ae87a..92e5c4bda 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 @@ - (UIImage *)makeCircularImageWithSize:(CGSize)size withBorderWidth:(CGFloat)wid 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/Podfile b/examples/PagerNode/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/PagerNode/Podfile +++ b/examples/PagerNode/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/PagerNode/Sample.xcodeproj/project.pbxproj b/examples/PagerNode/Sample.xcodeproj/project.pbxproj index 16d7ec840..febfa871c 100644 --- a/examples/PagerNode/Sample.xcodeproj/project.pbxproj +++ b/examples/PagerNode/Sample.xcodeproj/project.pbxproj @@ -118,12 +118,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - 6E05308BEF86AD80AEB4EEE7 /* Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -180,14 +180,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 6E05308BEF86AD80AEB4EEE7 /* Embed Pods Frameworks */ = { + 6E05308BEF86AD80AEB4EEE7 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -195,29 +195,32 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Check Pods 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; }; - F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -277,7 +280,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -312,7 +315,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/README.md b/examples/README.md index e56667c0c..76c71a75d 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/SocialAppLayout-Inverted/Podfile b/examples/SocialAppLayout-Inverted/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/SocialAppLayout-Inverted/Podfile +++ b/examples/SocialAppLayout-Inverted/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.pbxproj b/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.pbxproj index b43fcaac6..3ad0bf877 100644 --- a/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.pbxproj +++ b/examples/SocialAppLayout-Inverted/Sample.xcodeproj/project.pbxproj @@ -319,13 +319,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 */ @@ -385,7 +388,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -422,7 +425,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/SocialAppLayout/Podfile b/examples/SocialAppLayout/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/SocialAppLayout/Podfile +++ b/examples/SocialAppLayout/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/SocialAppLayout/Sample.xcodeproj/project.pbxproj b/examples/SocialAppLayout/Sample.xcodeproj/project.pbxproj index abd0cb665..b187eec74 100644 --- a/examples/SocialAppLayout/Sample.xcodeproj/project.pbxproj +++ b/examples/SocialAppLayout/Sample.xcodeproj/project.pbxproj @@ -200,12 +200,12 @@ isa = PBXNativeTarget; buildConfigurationList = 3EEA4EFB1BECC4A1008A7F35 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - B5BD9E5609B2CB179EEE0CF4 /* Check Pods Manifest.lock */, + B5BD9E5609B2CB179EEE0CF4 /* [CP] Check Pods Manifest.lock */, 3EEA4EE01BECC4A1008A7F35 /* Sources */, 3EEA4EE11BECC4A1008A7F35 /* Frameworks */, 3EEA4EE21BECC4A1008A7F35 /* Resources */, - 21F2C1D9B53F9468EAF1653F /* Copy Pods Resources */, - 852437589F1D53B9483A75DF /* Embed Pods Frameworks */, + 21F2C1D9B53F9468EAF1653F /* [CP] Copy Pods Resources */, + 852437589F1D53B9483A75DF /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -281,14 +281,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 21F2C1D9B53F9468EAF1653F /* Copy Pods Resources */ = { + 21F2C1D9B53F9468EAF1653F /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -296,14 +296,14 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 852437589F1D53B9483A75DF /* Embed Pods Frameworks */ = { + 852437589F1D53B9483A75DF /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -311,19 +311,22 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - B5BD9E5609B2CB179EEE0CF4 /* Check Pods Manifest.lock */ = { + B5BD9E5609B2CB179EEE0CF4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Check Pods 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 */ @@ -383,7 +386,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -420,7 +423,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/Swift/Podfile b/examples/Swift/Podfile index 8458e64c6..83d2cae8b 100644 --- a/examples/Swift/Podfile +++ b/examples/Swift/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' use_frameworks! diff --git a/examples/Swift/Sample.xcodeproj/project.pbxproj b/examples/Swift/Sample.xcodeproj/project.pbxproj index 081a5a2d6..533a738ea 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", @@ -270,7 +291,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -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,15 +329,17 @@ 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; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.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 f7f575e82..05f94265c 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 000000000..7b5a2f305 --- /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 e56067296..c40f1be4e 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/VerticalWithinHorizontalScrolling/Podfile b/examples/VerticalWithinHorizontalScrolling/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/VerticalWithinHorizontalScrolling/Podfile +++ b/examples/VerticalWithinHorizontalScrolling/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj b/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj index 7f7950060..80493caef 100644 --- a/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj +++ b/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj @@ -123,12 +123,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - 6E94068CFAA7736333E7D960 /* Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 6E94068CFAA7736333E7D960 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -185,14 +185,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 6E94068CFAA7736333E7D960 /* Embed Pods Frameworks */ = { + 6E94068CFAA7736333E7D960 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -200,29 +200,32 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Check Pods 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; }; - F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -282,7 +285,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -317,7 +320,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/Videos/Podfile b/examples/Videos/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples/Videos/Podfile +++ b/examples/Videos/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples/Videos/Sample.xcodeproj/project.pbxproj b/examples/Videos/Sample.xcodeproj/project.pbxproj index b43c7e7fc..8a0460100 100644 --- a/examples/Videos/Sample.xcodeproj/project.pbxproj +++ b/examples/Videos/Sample.xcodeproj/project.pbxproj @@ -125,12 +125,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */, - 93B7780A33739EF25F20366B /* 📦 Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 93B7780A33739EF25F20366B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -191,14 +191,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 93B7780A33739EF25F20366B /* 📦 Embed Pods Frameworks */ = { + 93B7780A33739EF25F20366B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "📦 Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -206,29 +206,32 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* 📦 Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "📦 Check Pods 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; }; - F012A6F39E0149F18F564F50 /* 📦 Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "📦 Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -286,7 +289,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -321,7 +324,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj index 5927a1d96..23ef18b40 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 000000000..102e5c601 --- /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/PhotoFeedModel.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift index 9a7cd5202..f344df34a 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift @@ -67,7 +67,10 @@ final class PhotoFeedModel { private func fetchNextPageOfPopularPhotos(replaceData: Bool, numberOfAdditionsCompletion: @escaping (Int, NetworkingErrors?) -> ()) { if currentPage == totalPages, currentPage != 0 { - return numberOfAdditionsCompletion(0, .customError("No pages left to parse")) + DispatchQueue.main.async { + numberOfAdditionsCompletion(0, .customError("No pages left to parse")) + } + return } var newPhotos: [PhotoModel] = [] @@ -106,7 +109,9 @@ final class PhotoFeedModel { case .failure(let fail): print(fail) - numberOfAdditionsCompletion(0, fail) + DispatchQueue.main.async { + numberOfAdditionsCompletion(0, fail) + } } } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift index e8a0dce3c..08c1bc327 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 e2196208d..830d78eb0 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/ASLayoutSpecPlayground-Swift/Podfile b/examples_extra/ASLayoutSpecPlayground-Swift/Podfile index f092b7ba8..3b379097a 100644 --- a/examples_extra/ASLayoutSpecPlayground-Swift/Podfile +++ b/examples_extra/ASLayoutSpecPlayground-Swift/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' use_frameworks! target 'Sample' do pod 'Texture', :path => '../..' diff --git a/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.pbxproj b/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.pbxproj index ced4aae5c..b77a5e410 100644 --- a/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcodeproj/project.pbxproj @@ -188,13 +188,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 */ @@ -245,7 +248,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -280,7 +283,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; @@ -305,7 +308,6 @@ GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = Sample/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; @@ -339,7 +341,6 @@ GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = Sample/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample; diff --git a/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcworkspace/contents.xcworkspacedata b/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples_extra/ASLayoutSpecPlayground-Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples_extra/ASTableViewStressTest/Podfile b/examples_extra/ASTableViewStressTest/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/ASTableViewStressTest/Podfile +++ b/examples_extra/ASTableViewStressTest/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.pbxproj b/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.pbxproj index 3e68545a8..c0da5a201 100644 --- a/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/ASTableViewStressTest/Sample.xcodeproj/project.pbxproj @@ -113,12 +113,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - 75CADB9ECE58AB74892E1D67 /* Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 75CADB9ECE58AB74892E1D67 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -175,14 +175,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 75CADB9ECE58AB74892E1D67 /* Embed Pods Frameworks */ = { + 75CADB9ECE58AB74892E1D67 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -190,29 +190,32 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Check Pods 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; }; - F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -270,7 +273,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -305,7 +308,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; @@ -318,7 +321,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = Sample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -330,7 +332,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = Sample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; }; diff --git a/examples_extra/ASTableViewStressTest/Sample.xcworkspace/contents.xcworkspacedata b/examples_extra/ASTableViewStressTest/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples_extra/ASTableViewStressTest/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples_extra/ASTraitCollection/Podfile b/examples_extra/ASTraitCollection/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/ASTraitCollection/Podfile +++ b/examples_extra/ASTraitCollection/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/ASTraitCollection/Sample.xcodeproj/project.pbxproj b/examples_extra/ASTraitCollection/Sample.xcodeproj/project.pbxproj index 20186b37a..ca9ec160e 100644 --- a/examples_extra/ASTraitCollection/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/ASTraitCollection/Sample.xcodeproj/project.pbxproj @@ -292,7 +292,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -327,7 +327,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples_extra/BackgroundPropertySetting/Podfile b/examples_extra/BackgroundPropertySetting/Podfile index f092b7ba8..3b379097a 100644 --- a/examples_extra/BackgroundPropertySetting/Podfile +++ b/examples_extra/BackgroundPropertySetting/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' use_frameworks! target 'Sample' do pod 'Texture', :path => '../..' diff --git a/examples_extra/CollectionViewWithViewControllerCells/Podfile b/examples_extra/CollectionViewWithViewControllerCells/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/CollectionViewWithViewControllerCells/Podfile +++ b/examples_extra/CollectionViewWithViewControllerCells/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.pbxproj b/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.pbxproj index 0c20d4914..718ba91ba 100644 --- a/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/CollectionViewWithViewControllerCells/Sample.xcodeproj/project.pbxproj @@ -284,7 +284,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -320,7 +320,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/examples_extra/EditableText/Podfile b/examples_extra/EditableText/Podfile index 922ff50ec..08d1b7add 100644 --- a/examples_extra/EditableText/Podfile +++ b/examples_extra/EditableText/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/EditableText/Sample.xcodeproj/project.pbxproj b/examples_extra/EditableText/Sample.xcodeproj/project.pbxproj index 46aa0e942..2afe6c7b2 100644 --- a/examples_extra/EditableText/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/EditableText/Sample.xcodeproj/project.pbxproj @@ -273,7 +273,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -308,7 +308,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples_extra/InterfaceCoalescing/Default-568h@2x.png b/examples_extra/InterfaceCoalescing/Default-568h@2x.png new file mode 100644 index 000000000..6ee80b939 Binary files /dev/null and b/examples_extra/InterfaceCoalescing/Default-568h@2x.png differ diff --git a/examples_extra/InterfaceCoalescing/Default-667h@2x.png b/examples_extra/InterfaceCoalescing/Default-667h@2x.png new file mode 100644 index 000000000..e7b975e21 Binary files /dev/null and b/examples_extra/InterfaceCoalescing/Default-667h@2x.png differ diff --git a/examples_extra/InterfaceCoalescing/Default-736h@3x.png b/examples_extra/InterfaceCoalescing/Default-736h@3x.png new file mode 100644 index 000000000..c8949cae1 Binary files /dev/null and b/examples_extra/InterfaceCoalescing/Default-736h@3x.png differ diff --git a/examples_extra/InterfaceCoalescing/Podfile b/examples_extra/InterfaceCoalescing/Podfile new file mode 100644 index 000000000..71a7f2c4b --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Podfile @@ -0,0 +1,5 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' +target 'Sample' do + pod 'Texture', :path => '../..' +end diff --git a/examples_extra/InterfaceCoalescing/Sample.xcodeproj/project.pbxproj b/examples_extra/InterfaceCoalescing/Sample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..26859039b --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,440 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 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 */; }; + AD61969DBDF7FD632C7D07F8 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F8E019F6C75C267AC72A2F3 /* libPods-Sample.a */; }; + C088923B2076E4A600998174 /* ICCollectionNode.m in Sources */ = {isa = PBXBuildFile; fileRef = C088923A2076E4A600998174 /* ICCollectionNode.m */; }; + C088923E2076E61600998174 /* ICCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = C088923D2076E61600998174 /* ICCellNode.m */; }; + C08892412077EC0300998174 /* ICDisplayNode.m in Sources */ = {isa = PBXBuildFile; fileRef = C08892402077EC0300998174 /* ICDisplayNode.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 1F2942882A3B5220B7506FFC /* 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 = ""; }; + 4E60AE604B72B745B8D6B008 /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; + 4F8E019F6C75C267AC72A2F3 /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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; }; + C08892392076E4A600998174 /* ICCollectionNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ICCollectionNode.h; sourceTree = ""; }; + C088923A2076E4A600998174 /* ICCollectionNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ICCollectionNode.m; sourceTree = ""; }; + C088923C2076E61600998174 /* ICCellNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ICCellNode.h; sourceTree = ""; }; + C088923D2076E61600998174 /* ICCellNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ICCellNode.m; sourceTree = ""; }; + C088923F2077EC0300998174 /* ICDisplayNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ICDisplayNode.h; sourceTree = ""; }; + C08892402077EC0300998174 /* ICDisplayNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ICDisplayNode.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AD61969DBDF7FD632C7D07F8 /* libPods-Sample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + C088923F2077EC0300998174 /* ICDisplayNode.h */, + C08892402077EC0300998174 /* ICDisplayNode.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + C08892392076E4A600998174 /* ICCollectionNode.h */, + C088923A2076E4A600998174 /* ICCollectionNode.m */, + C088923C2076E61600998174 /* ICCellNode.h */, + C088923D2076E61600998174 /* ICCellNode.m */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4F8E019F6C75C267AC72A2F3 /* libPods-Sample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + 4E60AE604B72B745B8D6B008 /* Pods-Sample.debug.xcconfig */, + 1F2942882A3B5220B7506FFC /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + 0B36DF845776B41DB3DB1FC3 /* [CP] Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + 682D6483AA9410CE0AF21363 /* Embed Pods Frameworks */, + 9E34612BC7C51DAA56B6AFA3 /* [CP] Embed Pods Frameworks */, + A51F01E9B9FF9C3FEAFC3182 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + DevelopmentTeam = F4M3Z8FSX9; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0B36DF845776B41DB3DB1FC3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + 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_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; + }; + 682D6483AA9410CE0AF21363 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9E34612BC7C51DAA56B6AFA3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A51F01E9B9FF9C3FEAFC3182 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + 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"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C08892412077EC0300998174 /* ICDisplayNode.m in Sources */, + C088923B2076E4A600998174 /* ICCollectionNode.m in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + C088923E2076E61600998174 /* ICCellNode.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E60AE604B72B745B8D6B008 /* Pods-Sample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = F4M3Z8FSX9; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "AS_FIXED_CONFIG_JSON=\"{ \\\"version\\\" : 1, \\\"experimental_features\\\": [ \\\"exp_interface_state_coalesce\\\" ] }\"", + ); + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Samplex; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1F2942882A3B5220B7506FFC /* Pods-Sample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = F4M3Z8FSX9; + INFOPLIST_FILE = Sample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Samplex; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/examples_extra/InterfaceCoalescing/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples_extra/InterfaceCoalescing/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..a80c03824 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples_extra/InterfaceCoalescing/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples_extra/InterfaceCoalescing/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 000000000..4a653521b --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples_extra/InterfaceCoalescing/Sample.xcworkspace/contents.xcworkspacedata b/examples_extra/InterfaceCoalescing/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples_extra/InterfaceCoalescing/Sample/AppDelegate.h b/examples_extra/InterfaceCoalescing/Sample/AppDelegate.h new file mode 100644 index 000000000..27e560aaf --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/AppDelegate.h @@ -0,0 +1,24 @@ +// +// AppDelegate.h +// Sample +// +// 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. +// +// 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. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/AppDelegate.m b/examples_extra/InterfaceCoalescing/Sample/AppDelegate.m new file mode 100644 index 000000000..c844e2aa0 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/AppDelegate.m @@ -0,0 +1,50 @@ +// +// AppDelegate.m +// Sample +// +// 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. +// +// 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. +// + +#import "AppDelegate.h" + +#import "ViewController.h" +#import +#import + +@interface ASConfigurationManager (Testing) ++ (void)test_resetWithConfiguration:(ASConfiguration *)configuration; +@end + + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + [ASDisplayNode setShouldShowRangeDebugOverlay:YES]; + + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures |= ASExperimentalInterfaceStateCoalescing; + [ASConfigurationManager test_resetWithConfiguration:config]; + + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + ViewController *vc = [[ViewController alloc] init]; + UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:vc]; + + self.window.rootViewController = nvc; + [self.window makeKeyAndVisible]; + + return YES; +} + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/ICCellNode.h b/examples_extra/InterfaceCoalescing/Sample/ICCellNode.h new file mode 100644 index 000000000..502f11f56 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ICCellNode.h @@ -0,0 +1,27 @@ +// +// ICCellNode.h +// Sample +// +// Created by Max Wang on 4/5/18. +// Copyright © 2018 Facebook. All rights reserved. +// + +#import + +@class ICCellNode; + +@protocol ICCellNodeDelegate +- (void)cellDidEnterVisibleState:(ICCellNode *)cellNode; +@end + +@interface ICCellNode : ASCellNode + +- (instancetype)initWithColor:(UIColor *)color colorName:(NSString *)colorName; + +@property (nonatomic, copy) NSString *colorName; +@property (nonatomic, assign) NSUInteger didEnterVisibleCount; +@property (nonatomic, assign) NSUInteger didExitVisibleCount; + +@property (nonatomic, weak) id delegate; + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/ICCellNode.m b/examples_extra/InterfaceCoalescing/Sample/ICCellNode.m new file mode 100644 index 000000000..fe5512073 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ICCellNode.m @@ -0,0 +1,73 @@ +// +// ICCellNode.m +// Sample +// +// Created by Max Wang on 4/5/18. +// Copyright © 2018 Facebook. All rights reserved. +// + +#import "ICCellNode.h" + +@implementation ICCellNode { + UIColor *_color; + NSString *_colorName; +} + +- (instancetype)initWithColor:(UIColor *)color colorName:(NSString *)colorName { + if (self = [super init]) { +// self.automaticallyManagesSubnodes = NO; + _color = color; + _colorName = colorName; + + _didEnterVisibleCount = 0; + _didExitVisibleCount = 0; + + self.backgroundColor = _color; + } + return self; +} + +- (void)layout { + [super layout]; + NSLog(@"^^^^ Layout"); +} + +- (void)didLoad { + [super didLoad]; +} + +- (void)didEnterVisibleState { + [super didEnterVisibleState]; + self.didEnterVisibleCount += 1; + NSLog(@"^^^^ %@ didEnterVisibleState %@", _colorName, self); + + [self.delegate cellDidEnterVisibleState:self]; +} + +- (void)didExitVisibleState { + [super didExitVisibleState]; + self.didExitVisibleCount += 1; + NSLog(@"^^^^ %@ didExitVisibleState %@", _colorName, self); +} + +- (void)didEnterDisplayState { + [super didEnterDisplayState]; + NSLog(@"^^^^ %@ didEnterDisplayState %@", _colorName, self); +} + +- (void)didExitDisplayState { + [super didExitDisplayState]; + NSLog(@"^^^^ %@ didExitDisplayState %@", _colorName, self); +} + +- (void)didEnterPreloadState { + [super didEnterPreloadState]; + NSLog(@"^^^^ %@ didEnterPreloadState %@", _colorName, self); +} + +- (void)didExitPreloadState { + [super didExitPreloadState]; + NSLog(@"^^^^ %@ didExitPreloadState %@", _colorName, self); +} + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/ICCollectionNode.h b/examples_extra/InterfaceCoalescing/Sample/ICCollectionNode.h new file mode 100644 index 000000000..ee3290748 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ICCollectionNode.h @@ -0,0 +1,13 @@ +// +// ICCollectionNode.h +// Sample +// +// Created by Max Wang on 4/5/18. +// Copyright © 2018 Facebook. All rights reserved. +// + +#import + +@interface ICCollectionNode : ASCollectionNode + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/ICCollectionNode.m b/examples_extra/InterfaceCoalescing/Sample/ICCollectionNode.m new file mode 100644 index 000000000..fcdfff7dd --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ICCollectionNode.m @@ -0,0 +1,43 @@ +// +// ICCollectionNode.m +// Sample +// +// Created by Max Wang on 4/5/18. +// Copyright © 2018 Facebook. All rights reserved. +// + +#import "ICCollectionNode.h" + +@implementation ICCollectionNode + +- (void)didEnterVisibleState { + [super didEnterVisibleState]; + NSLog(@"^^^^ didEnterVisibleState %@", self); +} + +- (void)didExitVisibleState { + [super didExitVisibleState]; + NSLog(@"^^^^ didExitVisibleState %@", self); +} + +- (void)didEnterDisplayState { + [super didEnterDisplayState]; + NSLog(@"^^^^ didEnterDisplayState %@", self); +} + +- (void)didExitDisplayState { + [super didExitDisplayState]; + NSLog(@"^^^^ didExitDisplayState %@", self); +} + +- (void)didEnterPreloadState { + [super didEnterPreloadState]; + NSLog(@"^^^^ didEnterPreloadState %@", self); +} + +- (void)didExitPreloadState { + [super didExitPreloadState]; + NSLog(@"^^^^ didExitPreloadState %@", self); +} + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/ICDisplayNode.h b/examples_extra/InterfaceCoalescing/Sample/ICDisplayNode.h new file mode 100644 index 000000000..40248b6de --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ICDisplayNode.h @@ -0,0 +1,13 @@ +// +// ICDisplayNode.h +// Sample +// +// Created by Max Wang on 4/6/18. +// Copyright © 2018 Facebook. All rights reserved. +// + +#import + +@interface ICDisplayNode : ASDisplayNode + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/ICDisplayNode.m b/examples_extra/InterfaceCoalescing/Sample/ICDisplayNode.m new file mode 100644 index 000000000..bc1d9ac3a --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ICDisplayNode.m @@ -0,0 +1,51 @@ +// +// ICDisplayNode.m +// Sample +// +// Created by Max Wang on 4/6/18. +// Copyright © 2018 Facebook. All rights reserved. +// + +#import "ICDisplayNode.h" + +@implementation ICDisplayNode + +- (instancetype)init +{ + self = [super init]; + if (self) { + self.backgroundColor = [UIColor whiteColor]; + } + return self; +} +- (void)didEnterVisibleState { + [super didEnterVisibleState]; + NSLog(@"^^^^ didEnterVisibleState %@", self); +} + +- (void)didExitVisibleState { + [super didExitVisibleState]; + NSLog(@"^^^^ didExitVisibleState %@", self); +} + +- (void)didEnterDisplayState { + [super didEnterDisplayState]; + NSLog(@"^^^^ didEnterDisplayState %@", self); +} + +- (void)didExitDisplayState { + [super didExitDisplayState]; + NSLog(@"^^^^ didExitDisplayState %@", self); +} + +- (void)didEnterPreloadState { + [super didEnterPreloadState]; + NSLog(@"^^^^ didEnterPreloadState %@", self); +} + +- (void)didExitPreloadState { + [super didExitPreloadState]; + NSLog(@"^^^^ didExitPreloadState %@", self); +} + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/Info.plist b/examples_extra/InterfaceCoalescing/Sample/Info.plist new file mode 100644 index 000000000..fb4115c84 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples_extra/InterfaceCoalescing/Sample/ViewController.h b/examples_extra/InterfaceCoalescing/Sample/ViewController.h new file mode 100644 index 000000000..db689fe32 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ViewController.h @@ -0,0 +1,22 @@ +// +// ViewController.h +// Sample +// +// 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. +// +// 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. +// + +#import + +@interface ViewController : ASViewController + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/ViewController.m b/examples_extra/InterfaceCoalescing/Sample/ViewController.m new file mode 100644 index 000000000..2cf126297 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/ViewController.m @@ -0,0 +1,108 @@ +// +// ViewController.m +// Sample +// +// 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. +// +// 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. +// + +#import "ViewController.h" +#import "ICCellNode.h" +#import "ICCollectionNode.h" +#import "ICDisplayNode.h" + +@interface ViewController () +@end + +@implementation ViewController { + NSArray *_colors; + NSArray *_colorNames; + + ICCollectionNode *_collectionNode; + UICollectionViewFlowLayout *_flowLayout; +} + +- (instancetype)init +{ + _flowLayout = [[UICollectionViewFlowLayout alloc] init]; + _collectionNode = [[ICCollectionNode alloc] initWithCollectionViewLayout:_flowLayout]; + + if (!(self = [super initWithNode:_collectionNode])) + return nil; + _flowLayout.minimumLineSpacing = 0; + self.navigationController.navigationBarHidden = NO; + _colors = @[[UIColor redColor], [UIColor greenColor], [UIColor blueColor]]; + _colorNames = @[@"Red", @"Green", @"Blue"]; + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + _collectionNode.delegate = self; + _collectionNode.dataSource = self; +} + +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode { + return 1; +} + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section { + return _colors.count; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath { + return ASSizeRangeMake([UIScreen mainScreen].bounds.size); +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { + return ^ASCellNode *{ + ICCellNode *cellNode = [[ICCellNode alloc] initWithColor: _colors[indexPath.row] colorName: _colorNames[indexPath.row]]; + cellNode.delegate = self; + return cellNode; + }; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node { + NSLog(@"^^^^ willDisplayItemWithNode %@", node); +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + ICDisplayNode *node = [[ICDisplayNode alloc] init]; + ViewController *vc = [[ViewController alloc] initWithNode:node]; + [self.navigationController pushViewController:vc animated:YES]; +} + +- (void)cellDidEnterVisibleState:(ICCellNode *)cellNode { + //_collectionNode.contentOffset = CGPointMake(0, 100); +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + NSLog(@"^^^^ viewWillDisappear %@", self); +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + NSLog(@"^^^^ viewDidDisappear %@", self); +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + NSLog(@"^^^^ viewWillAppear %@", self); +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + NSLog(@"^^^^ viewDidAppear %@", self); +} + +@end diff --git a/examples_extra/InterfaceCoalescing/Sample/main.m b/examples_extra/InterfaceCoalescing/Sample/main.m new file mode 100644 index 000000000..756080fb2 --- /dev/null +++ b/examples_extra/InterfaceCoalescing/Sample/main.m @@ -0,0 +1,26 @@ +// +// main.m +// Sample +// +// 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. +// +// 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. +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/examples_extra/InterfaceCoalescing/best.png b/examples_extra/InterfaceCoalescing/best.png new file mode 100644 index 000000000..d50d8103a Binary files /dev/null and b/examples_extra/InterfaceCoalescing/best.png differ diff --git a/examples_extra/InterfaceCoalescing/medium.png b/examples_extra/InterfaceCoalescing/medium.png new file mode 100644 index 000000000..7c08e0adc Binary files /dev/null and b/examples_extra/InterfaceCoalescing/medium.png differ diff --git a/examples_extra/InterfaceCoalescing/worst.png b/examples_extra/InterfaceCoalescing/worst.png new file mode 100644 index 000000000..78727fa98 Binary files /dev/null and b/examples_extra/InterfaceCoalescing/worst.png differ diff --git a/examples_extra/Multiplex/Podfile b/examples_extra/Multiplex/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/Multiplex/Podfile +++ b/examples_extra/Multiplex/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/Placeholders/Podfile b/examples_extra/Placeholders/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/Placeholders/Podfile +++ b/examples_extra/Placeholders/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/Placeholders/Sample.xcodeproj/project.pbxproj b/examples_extra/Placeholders/Sample.xcodeproj/project.pbxproj index e360fd6cd..6054b17bd 100644 --- a/examples_extra/Placeholders/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/Placeholders/Sample.xcodeproj/project.pbxproj @@ -298,7 +298,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -333,7 +333,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples_extra/RepoSearcher/Podfile b/examples_extra/RepoSearcher/Podfile index ac26415f1..21dc49860 100644 --- a/examples_extra/RepoSearcher/Podfile +++ b/examples_extra/RepoSearcher/Podfile @@ -1,5 +1,4 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '9.0' target 'RepoSearcher' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks diff --git a/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj b/examples_extra/RepoSearcher/RepoSearcher.xcodeproj/project.pbxproj index 2dac7da9f..aa470f0eb 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 = 9.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 = 9.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 b1c623b65..df923205c 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 040634c93..fe65d2dfa 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 176b48430..f6b95a193 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 440cc3752..57bb1eebe 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 75738a0c9..1619b5598 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 35f1c3913..dd00d9aa7 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 cdbdc8169..5c9cc401b 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 = 9.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 = 9.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 97bcec3ae..0efaf45d3 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 855625172..83a37564f 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 45c36e656..7cb0a2b9f 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 af716bf62..ab33726d9 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 ad531709e..c32ce3f0a 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/SynchronousConcurrency/Podfile b/examples_extra/SynchronousConcurrency/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/SynchronousConcurrency/Podfile +++ b/examples_extra/SynchronousConcurrency/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.pbxproj b/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.pbxproj index 80d3e6e37..f3964a956 100644 --- a/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/SynchronousConcurrency/Sample.xcodeproj/project.pbxproj @@ -123,12 +123,12 @@ isa = PBXNativeTarget; buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */, 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* Copy Pods Resources */, - 0342F7A1563F38A62746D4B8 /* Embed Pods Frameworks */, + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, + 0342F7A1563F38A62746D4B8 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -185,14 +185,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0342F7A1563F38A62746D4B8 /* Embed Pods Frameworks */ = { + 0342F7A1563F38A62746D4B8 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Embed Pods Frameworks"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -200,29 +200,32 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Check Pods 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; }; - F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -282,7 +285,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -317,7 +320,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples_extra/SynchronousConcurrency/Sample.xcworkspace/contents.xcworkspacedata b/examples_extra/SynchronousConcurrency/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples_extra/SynchronousConcurrency/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.m b/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.m index 3ab470844..d87c22ea5 100644 --- a/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.m +++ b/examples_extra/SynchronousConcurrency/Sample/AsyncTableViewController.m @@ -64,7 +64,7 @@ - (void)viewDidLoad tuningParameters.leadingBufferScreenfuls = 0.5; tuningParameters.trailingBufferScreenfuls = 1.0; [_tableView setTuningParameters:tuningParameters forRangeType:ASLayoutRangeTypePreload]; - [_tableView setTuningParameters:tuningParameters forRangeType:ASLayoutRangeTypeRender]; + [_tableView setTuningParameters:tuningParameters forRangeType:ASLayoutRangeTypeDisplay]; [self.view addSubview:_tableView]; } @@ -75,7 +75,7 @@ - (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath { return ^{ RandomCoreGraphicsNode *elementNode = [[RandomCoreGraphicsNode alloc] init]; - elementNode.size = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(320, 100)); + elementNode.style.preferredSize = CGSizeMake(320, 100); return elementNode; }; } diff --git a/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.h b/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.h index fd357593c..f307e65d6 100644 --- a/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.h +++ b/examples_extra/SynchronousConcurrency/Sample/AsyncViewController.h @@ -17,7 +17,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -#import "ASViewController.h" +#import @interface AsyncViewController : ASViewController diff --git a/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.m b/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.m index d2afbd6e1..66b490df0 100644 --- a/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.m +++ b/examples_extra/SynchronousConcurrency/Sample/RandomCoreGraphicsNode.m @@ -86,7 +86,7 @@ - (instancetype)init - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { - [_textNode measure:constrainedSize]; + [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)]; return CGSizeMake(constrainedSize.width, 100); } diff --git a/examples_extra/SynchronousKittens/Podfile b/examples_extra/SynchronousKittens/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/SynchronousKittens/Podfile +++ b/examples_extra/SynchronousKittens/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/SynchronousKittens/Sample.xcodeproj/project.pbxproj b/examples_extra/SynchronousKittens/Sample.xcodeproj/project.pbxproj index e7ecd2fc2..05a646e3c 100644 --- a/examples_extra/SynchronousKittens/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/SynchronousKittens/Sample.xcodeproj/project.pbxproj @@ -282,7 +282,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -317,7 +317,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples_extra/TextStressTest/Podfile b/examples_extra/TextStressTest/Podfile index 922ff50ec..73e26195c 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' +platform :ios, '9.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 3453463d2..806d9a2e8 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; @@ -271,7 +292,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -306,7 +327,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples_extra/TextStressTest/Sample.xcworkspace/contents.xcworkspacedata b/examples_extra/TextStressTest/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples_extra/TextStressTest/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples_extra/TextStressTest/Sample/AppDelegate.m b/examples_extra/TextStressTest/Sample/AppDelegate.m index a8e559478..5be641c02 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 000000000..159b2fa1c --- /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 000000000..d93005236 --- /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 000000000..5a25bb04a --- /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 000000000..a7905cb23 --- /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 000000000..c4b35cfaf --- /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 000000000..13bb7694c --- /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/examples_extra/VideoTableView/Podfile b/examples_extra/VideoTableView/Podfile index b75e492fa..71a7f2c4b 100644 --- a/examples_extra/VideoTableView/Podfile +++ b/examples_extra/VideoTableView/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '8.0' +platform :ios, '9.0' target 'Sample' do pod 'Texture', :path => '../..' end diff --git a/examples_extra/VideoTableView/Sample.xcodeproj/project.pbxproj b/examples_extra/VideoTableView/Sample.xcodeproj/project.pbxproj index 8bf0789b1..c7a68ff0b 100644 --- a/examples_extra/VideoTableView/Sample.xcodeproj/project.pbxproj +++ b/examples_extra/VideoTableView/Sample.xcodeproj/project.pbxproj @@ -282,7 +282,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -317,7 +317,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj b/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj index b4eeb8cc2..62bb5e98b 100644 --- a/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj +++ b/smoke-tests/Framework/Sample.xcodeproj/project.pbxproj @@ -298,7 +298,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -334,7 +334,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.pbxproj b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.pbxproj index 75e17d52f..dd795b754 100644 --- a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.pbxproj +++ b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods.xcodeproj/project.pbxproj @@ -410,7 +410,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -447,7 +447,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES;