diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 58ac2eec4..edcbb825e 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -424,18 +424,23 @@ DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; - E516FC7F1E9FE24200714FF4 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E516FC7D1E9FE24200714FF4 /* ASHashing.h */; }; - E516FC801E9FE24200714FF4 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E516FC7E1E9FE24200714FF4 /* ASHashing.m */; }; E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASCollectionElement.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */; }; + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; }; + E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */; }; + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 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, ); }; }; 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, ); }; }; - E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */; }; + E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.m in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */; }; 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 */; }; @@ -443,11 +448,16 @@ 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 */; }; - E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; }; + E5B225281F1790D6001E1431 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B225271F1790B5001E1431 /* ASHashing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5B225291F1790EE001E1431 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B225261F1790B5001E1431 /* ASHashing.m */; }; + E5B2252E1F17E521001E1431 /* ASDispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B2252D1F17E521001E1431 /* ASDispatch.m */; }; + E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */; }; E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; }; E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E5E281761E71C845006B67C2 /* ASCollectionLayoutState.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */; }; + E5E281761E71C845006B67C2 /* ASCollectionLayoutState.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */; }; + E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */; }; F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */; }; /* End PBXBuildFile section */ @@ -899,8 +909,6 @@ DEC146B51C37A16A004A0EE7 /* ASCollectionInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASCollectionInternal.m; path = Details/ASCollectionInternal.m; sourceTree = ""; }; DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; - E516FC7D1E9FE24200714FF4 /* ASHashing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = ""; }; - E516FC7E1E9FE24200714FF4 /* ASHashing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; 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 = ""; }; @@ -909,10 +917,17 @@ E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = ""; }; E5711A2A1C840C81009619D4 /* ASCollectionElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionElement.h; sourceTree = ""; }; E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionElement.mm; sourceTree = ""; }; + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutItem.h; sourceTree = ""; }; + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionGalleryLayoutItem.mm; sourceTree = ""; }; + E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutState+Private.h"; sourceTree = ""; }; + E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutCache.h; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; - E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutContext.mm; sourceTree = ""; }; + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutContext.m; sourceTree = ""; }; 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 = ""; }; @@ -920,11 +935,16 @@ 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 = ""; }; + E5B225271F1790B5001E1431 /* ASHashing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = ""; }; + E5B2252D1F17E521001E1431 /* ASDispatch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASDispatch.m; sourceTree = ""; }; E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutContext+Private.h"; sourceTree = ""; }; E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetchingDelegate.h; sourceTree = ""; }; E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableNode+Beta.h"; sourceTree = ""; }; E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutState.h; sourceTree = ""; }; - E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutState.m; sourceTree = ""; }; + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutState.mm; sourceTree = ""; }; + E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionGalleryLayoutDelegate.h; sourceTree = ""; }; + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.m; 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 = ""; }; 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 = ""; }; @@ -1242,6 +1262,8 @@ 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.m */, 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */, 696F01EB1DD2AF450049FBD5 /* ASEventLog.mm */, + E5B225271F1790B5001E1431 /* ASHashing.h */, + E5B225261F1790B5001E1431 /* ASHashing.m */, 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */, 4640521C1A3F83C40061C0BA /* ASTableLayoutController.m */, 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */, @@ -1334,6 +1356,7 @@ CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, 6947B0BB1E36B4E30007C478 /* Layout */, CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */, + E52F8AEE1EAE659600B5A912 /* Collection Layout */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, @@ -1347,9 +1370,6 @@ 044285051BAA63FE00D16268 /* ASBatchFetching.h */, 044285061BAA63FE00D16268 /* ASBatchFetching.m */, CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */, - E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, - E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, - E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */, CC0F885A1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h */, CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.m */, @@ -1360,6 +1380,7 @@ AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */, AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */, CC54A81B1D70077A00296A24 /* ASDispatch.h */, + E5B2252D1F17E521001E1431 /* ASDispatch.m */, 058D0A08195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm */, 058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */, 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */, @@ -1369,8 +1390,6 @@ 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */, 6959433C1D70815300B0EE1F /* ASDisplayNodeLayout.mm */, - E516FC7D1E9FE24200714FF4 /* ASHashing.h */, - E516FC7E1E9FE24200714FF4 /* ASHashing.m */, 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */, 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, @@ -1637,16 +1656,35 @@ path = Debug; sourceTree = ""; }; + E52F8AEE1EAE659600B5A912 /* Collection Layout */ = { + isa = PBXGroup; + children = ( + E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, + E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, + E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */, + E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */, + E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, + E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, + E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, + E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */, + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, + ); + name = "Collection Layout"; + sourceTree = ""; + }; E5B077EB1E6843AF00C24B5B /* Collection Layout */ = { isa = PBXGroup; children = ( E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */, - E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */, + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */, E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */, - E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */, + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */, E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, + E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); @@ -1670,8 +1708,10 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 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 */, E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */, @@ -1778,11 +1818,14 @@ 044285081BAA63FE00D16268 /* ASBatchFetching.h in Headers */, AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */, + E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */, + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, + E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, + E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, 254C6B791BF94DF4003EC431 /* ASTextKitEntityAttribute.h in Headers */, CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */, DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */, - E516FC7F1E9FE24200714FF4 /* ASHashing.h in Headers */, 9C70F20F1CDBE9FF007D6C76 /* ASLayoutManager.h in Headers */, 6947B0C31E36B5040007C478 /* ASStackPositionedLayout.h in Headers */, DBABFAFC1C6A8D2F0039EA4A /* _ASTransitionContext.h in Headers */, @@ -1798,7 +1841,6 @@ 6977965F1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.h in Headers */, 692BE8D71E36B65B00C86D87 /* ASLayoutSpecPrivate.h in Headers */, 34EFC75D1B701BE900AD841F /* ASInternalHelpers.h in Headers */, - E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, DEC146B71C37A16A004A0EE7 /* ASCollectionInternal.h in Headers */, 68B8A4E21CBDB958007E4543 /* ASWeakProxy.h in Headers */, 9F98C0271DBE29FC00476D92 /* ASControlTargetAction.h in Headers */, @@ -1850,6 +1892,7 @@ 254C6B751BF94DF4003EC431 /* ASTextKitComponents.h in Headers */, B35062081B010EFD0018CF92 /* ASScrollNode.h in Headers */, CCA282CC1E9EB73E0037E8B7 /* ASTipNode.h in Headers */, + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, 25E327571C16819500A2170C /* ASPagerNode.h in Headers */, CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */, 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */, @@ -2140,6 +2183,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E5B225291F1790EE001E1431 /* ASHashing.m in Sources */, DEB8ED7C1DD003D300DBDE55 /* ASLayoutTransition.mm in Sources */, CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */, 9F98C0261DBE29E000476D92 /* ASControlTargetAction.m in Sources */, @@ -2154,7 +2198,6 @@ CCA282B91E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.m in Sources */, 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */, B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.mm in Sources */, - E516FC801E9FE24200714FF4 /* ASHashing.m in Sources */, 6947B0C51E36B5040007C478 /* ASStackPositionedLayout.mm in Sources */, B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.m in Sources */, AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */, @@ -2183,13 +2226,14 @@ B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */, AC47D9421B3B891B00AAEE9D /* ASCellNode.mm in Sources */, - E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.mm in Sources */, + E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.m in Sources */, 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */, 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */, E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */, 68FC85EC1CE29C7D00EDD713 /* ASVisibilityProtocols.m in Sources */, CC55A7121E52A0F200594372 /* ASResponderChainEnumerator.m in Sources */, 68B8A4E41CBDB958007E4543 /* ASWeakProxy.m in Sources */, + E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */, 9C70F20A1CDBE949007D6C76 /* ASTableNode.mm in Sources */, 69CB62AE1CB8165900024920 /* _ASDisplayViewAccessiblity.mm in Sources */, B35061F61B010EFD0018CF92 /* ASCollectionView.mm in Sources */, @@ -2211,7 +2255,7 @@ 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */, E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */, B35062511B010EFD0018CF92 /* ASDisplayNode+UIViewBridge.mm in Sources */, - E5E281761E71C845006B67C2 /* ASCollectionLayoutState.m in Sources */, + E5E281761E71C845006B67C2 /* ASCollectionLayoutState.mm in Sources */, B35061FC1B010EFD0018CF92 /* ASDisplayNode.mm in Sources */, B35061FF1B010EFD0018CF92 /* ASDisplayNodeExtras.mm in Sources */, B35062011B010EFD0018CF92 /* ASEditableTextNode.mm in Sources */, @@ -2226,16 +2270,19 @@ E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */, 6947B0C01E36B4E30007C478 /* ASStackUnpositionedLayout.mm in Sources */, 68355B401CB57A69001D4E68 /* ASImageContainerProtocolCategories.m in Sources */, + E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.m in Sources */, B35062031B010EFD0018CF92 /* ASImageNode.mm in Sources */, 254C6B821BF94F8A003EC431 /* ASTextKitComponents.mm in Sources */, 34EFC7601B701C8B00AD841F /* ASInsetLayoutSpec.mm in Sources */, AC6145441D8AFD4F003D62A2 /* ASSection.m in Sources */, + E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */, 34EFC75E1B701BF000AD841F /* ASInternalHelpers.m in Sources */, 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */, DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */, CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */, CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, @@ -2265,6 +2312,7 @@ E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */, 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */, + E5B2252E1F17E521001E1431 /* ASDispatch.m in Sources */, 696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */, 9C70F2051CDA4F06007D6C76 /* ASTraitCollection.m in Sources */, 83A7D95B1D44547700BF333E /* ASWeakMap.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f585d2bc..b39354c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Add your own contributions to the next release on the line below this with your name. - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) + - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index a558193da..67aea1fc8 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -216,6 +216,7 @@ - (void)didEnterPreloadState [super didEnterPreloadState]; // 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 [[self view] layoutIfNeeded]; } diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 2c319b0f2..ed61ea4c5 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -52,6 +52,7 @@ #import #import #import +#import #import #import @@ -98,17 +99,18 @@ #import #import #import +#import +#import #import #import #import #import -#import #import #import +#import #import #import #import -#import #import #import diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.h b/Source/Details/ASCollectionFlowLayoutDelegate.h index a143eaf80..f68cc74fb 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.h +++ b/Source/Details/ASCollectionFlowLayoutDelegate.h @@ -16,7 +16,6 @@ // #import -#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 009ba2123..855f3791c 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -17,10 +17,11 @@ #import -#import +#import #import #import #import +#import #import #import #import @@ -43,18 +44,9 @@ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirect return self; } -- (ASSizeRange)sizeRangeThatFits:(CGSize)viewportSize +- (ASScrollDirection)scrollableDirections { - ASSizeRange sizeRange = ASSizeRangeUnconstrained; - if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections) == NO) { - sizeRange.min.height = viewportSize.height; - sizeRange.max.height = viewportSize.height; - } - if (ASScrollDirectionContainsHorizontalDirection(_scrollableDirections) == NO) { - sizeRange.min.width = viewportSize.width; - sizeRange.max.width = viewportSize.width; - } - return sizeRange; + return _scrollableDirections; } - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements @@ -62,7 +54,7 @@ - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements return nil; } -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { ASElementMap *elements = context.elements; NSMutableArray *children = ASArrayByFlatMapping(elements.itemElements, ASCollectionElement *element, element.node); @@ -80,8 +72,13 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte alignContent:ASStackLayoutAlignContentStart children:children]; stackSpec.concurrent = YES; - ASLayout *layout = [stackSpec layoutThatFits:[self sizeRangeThatFits:context.viewportSize]]; - return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout]; + + ASSizeRange sizeRange = ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, context.scrollableDirections); + ASLayout *layout = [stackSpec layoutThatFits:sizeRange]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nonnull(ASLayout * _Nonnull sublayout) { + return ((ASCellNode *)sublayout.layoutElement).collectionElement; + }]; } @end diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.h b/Source/Details/ASCollectionGalleryLayoutDelegate.h new file mode 100644 index 000000000..2b9e64c7c --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -0,0 +1,32 @@ +// +// ASCollectionGalleryLayoutDelegate.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 thread-safe layout delegate that arranges items with the same size into a flow layout. + * + * @note Supplemenraty elements are not supported. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionGalleryLayoutDelegate : NSObject + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m new file mode 100644 index 000000000..0c98f0fbf --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -0,0 +1,86 @@ +// +// ASCollectionGalleryLayoutDelegate.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 +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - ASCollectionGalleryLayoutDelegate + +@implementation ASCollectionGalleryLayoutDelegate { + ASScrollDirection _scrollableDirections; + CGSize _itemSize; +} + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize +{ + self = [super init]; + if (self) { + ASDisplayNodeAssertFalse(CGSizeEqualToSize(CGSizeZero, itemSize)); + _scrollableDirections = scrollableDirections; + _itemSize = itemSize; + } + return self; +} + +- (ASScrollDirection)scrollableDirections +{ + return _scrollableDirections; +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + return [NSValue valueWithCGSize:_itemSize]; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + CGSize pageSize = context.viewportSize; + CGSize itemSize = ((NSValue *)context.additionalInfo).CGSizeValue; + ASScrollDirection scrollableDirections = context.scrollableDirections; + NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:CGSizeZero + elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; + } + + // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + flexWrap:ASStackLayoutFlexWrapWrap + alignContent:ASStackLayoutAlignContentStart + children:children]; + stackSpec.concurrent = YES; + ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { + return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; + }]; +} + +@end diff --git a/Source/Details/ASCollectionLayoutContext.h b/Source/Details/ASCollectionLayoutContext.h index 60c7c3929..a20873a81 100644 --- a/Source/Details/ASCollectionLayoutContext.h +++ b/Source/Details/ASCollectionLayoutContext.h @@ -15,20 +15,20 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import #import #import +#import @class ASElementMap; NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED - @interface ASCollectionLayoutContext : NSObject @property (nonatomic, assign, readonly) CGSize viewportSize; -@property (nonatomic, strong, readonly) ASElementMap *elements; +@property (nonatomic, assign, readonly) ASScrollDirection scrollableDirections; +@property (nonatomic, weak, readonly) ASElementMap *elements; @property (nonatomic, strong, readonly, nullable) id additionalInfo; - (instancetype)init __unavailable; diff --git a/Source/Details/ASCollectionLayoutContext.m b/Source/Details/ASCollectionLayoutContext.m new file mode 100644 index 000000000..6620e391e --- /dev/null +++ b/Source/Details/ASCollectionLayoutContext.m @@ -0,0 +1,107 @@ +// +// ASCollectionLayoutContext.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 +#import +#import +#import +#import + +@implementation ASCollectionLayoutContext { + Class _layoutDelegateClass; + + // This ivar doesn't directly involve in the layout calculation process, i.e contexts can be equal regardless of the layout caches. + // As a result, this ivar is ignored in -isEqualToContext: and -hash. + __weak ASCollectionLayoutCache *_layoutCache; +} + +- (instancetype)initWithViewportSize:(CGSize)viewportSize + scrollableDirections:(ASScrollDirection)scrollableDirections + elements:(ASElementMap *)elements + layoutDelegateClass:(Class)layoutDelegateClass + layoutCache:(ASCollectionLayoutCache *)layoutCache + additionalInfo:(id)additionalInfo +{ + self = [super init]; + if (self) { + ASDisplayNodeAssertTrue([layoutDelegateClass conformsToProtocol:@protocol(ASCollectionLayoutDelegate)]); + _viewportSize = viewportSize; + _scrollableDirections = scrollableDirections; + _elements = elements; + _layoutDelegateClass = layoutDelegateClass; + _layoutCache = layoutCache; + _additionalInfo = additionalInfo; + } + return self; +} + +- (Class)layoutDelegateClass +{ + return _layoutDelegateClass; +} + +- (ASCollectionLayoutCache *)layoutCache +{ + return _layoutCache; +} + +- (BOOL)isEqualToContext:(ASCollectionLayoutContext *)context +{ + if (context == nil) { + return NO; + } + + // NOTE: ASObjectIsEqual returns YES when both objects are nil. + // So don't use ASObjectIsEqual on _elements. + // It is a weak property and 2 layouts generated from different sets of elements + // should never be considered the same even if they are nil now. + return CGSizeEqualToSize(_viewportSize, context.viewportSize) + && _scrollableDirections == context.scrollableDirections + && [_elements isEqual:context.elements] + && _layoutDelegateClass == context.layoutDelegateClass + && ASObjectIsEqual(_additionalInfo, context.additionalInfo); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[ASCollectionLayoutContext class]]) { + return NO; + } + return [self isEqualToContext:other]; +} + +- (NSUInteger)hash +{ + struct { + CGSize viewportSize; + ASScrollDirection scrollableDirections; + NSUInteger elementsHash; + NSUInteger layoutDelegateClassHash; + NSUInteger additionalInfoHash; + } data = { + _viewportSize, + _scrollableDirections, + _elements.hash, + _layoutDelegateClass.hash, + [_additionalInfo hash] + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end diff --git a/Source/Details/ASCollectionLayoutContext.mm b/Source/Details/ASCollectionLayoutContext.mm deleted file mode 100644 index d84de9f67..000000000 --- a/Source/Details/ASCollectionLayoutContext.mm +++ /dev/null @@ -1,72 +0,0 @@ -// -// ASCollectionLayoutContext.mm -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#import -#import - -#import -#import -#import -#import - -@implementation ASCollectionLayoutContext - -- (instancetype)initWithViewportSize:(CGSize)viewportSize elements:(ASElementMap *)elements additionalInfo:(id)additionalInfo -{ - self = [super init]; - if (self) { - _viewportSize = viewportSize; - _elements = elements; - _additionalInfo = additionalInfo; - } - return self; -} - -- (BOOL)isEqualToContext:(ASCollectionLayoutContext *)context -{ - if (context == nil) { - return NO; - } - return CGSizeEqualToSize(_viewportSize, context.viewportSize) && ASObjectIsEqual(_elements, context.elements) && ASObjectIsEqual(_additionalInfo, context.additionalInfo); -} - -- (BOOL)isEqual:(id)other -{ - if (self == other) { - return YES; - } - if (! [other isKindOfClass:[ASCollectionLayoutContext class]]) { - return NO; - } - return [self isEqualToContext:other]; -} - -- (NSUInteger)hash -{ - struct { - CGSize viewportSize; - NSUInteger elementsHash; - NSUInteger addlInfoHash; - } data = { - _viewportSize, - _elements.hash, - [_additionalInfo hash] - }; - return ASHashBytes(&data, sizeof(data)); -} - -@end diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index 7fd17467a..9ef3da874 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -17,6 +17,7 @@ #import #import +#import @class ASElementMap, ASCollectionLayoutContext, ASCollectionLayoutState; @@ -25,13 +26,22 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASCollectionLayoutDelegate /** - * @abstract Returns any additional information needed for a coming layout pass with the given elements. + * @abstract Returns the scrollable directions of the coming layout (@see @c -calculateLayoutWithContext:). + * It will be available in the context parameter in +calculateLayoutWithContext: + * + * @return The scrollable directions. + */ +- (ASScrollDirection)scrollableDirections; + +/** + * @abstract Returns any additional information needed for a coming layout pass (@see @c -calculateLayoutWithContext:) with the given elements. * * @param elements The elements to be laid out later. * * @discussion The returned object must support equality and hashing (i.e `-isEqual:` and `-hash` must be properly implemented). + * It should contain all the information needed for the layout pass to perform. It will be available in the context parameter in +calculateLayoutWithContext: * - * @discussion This method will be called on main thread. + * This method will be called on main thread. */ - (nullable id)additionalInfoForLayoutWithElements:(ASElementMap *)elements; @@ -43,13 +53,12 @@ NS_ASSUME_NONNULL_BEGIN * @return The new layout calculated for the given context. * * @discussion This method is called ahead of time, i.e before the underlying collection/table view is aware of the provided elements. - * As a result, this method should rely solely on the given context and should not reach out to other objects for information not available in the context. - * - * @discussion This method will be called on background theads. It must be thread-safe and should not change any internal state of this object. + * As a result, clients must solely rely on the given context and should not reach out to other objects for information not available in the context. * - * @discussion This method must block its calling thread. It can dispatch to other theads to reduce blocking time. + * This method can be called on background theads. It must be thread-safe and should not change any internal state of this delegate. + * It must block the calling thread but can dispatch to other theads to reduce total blocking time. */ -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; @end diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index e70b951aa..1804c58a3 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -30,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN @end AS_SUBCLASSING_RESTRICTED + +/// An immutable state of the collection layout @interface ASCollectionLayoutState : NSObject /// The context used to calculate this object @@ -47,20 +49,25 @@ AS_SUBCLASSING_RESTRICTED * * @param contentSize The content size of the collection's layout * - * @param table A map between elements to their layout attributes. It may contain all elements, or a subset of them that will be updated later. - * It should be initialized using +[NSMapTable elementToLayoutAttributesTable] convenience initializer. + * @param table A map between elements to their layout attributes. It must contain all elements. + * It should have NSMapTableObjectPointerPersonality and NSMapTableWeakMemory as key options. */ -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; /** * Convenience initializer. * * @param context The context used to calculate this object * - * @param layout The layout describes size and position of all elements, or a subset of them and will be updated over time. + * @param layout The layout describes size and position of all elements. * + * @param getElementBlock A block that can retrieve the collection element from a direct sublayout of the root layout. */ -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + layout:(ASLayout *)layout + getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; /** * Returns all layout attributes present in this object. @@ -88,7 +95,8 @@ AS_SUBCLASSING_RESTRICTED * * @param indexPath The index path of the element. */ -- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind + atIndexPath:(NSIndexPath *)indexPath; /** * Returns layout attributes of the specified element. diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m deleted file mode 100644 index ecb7e1503..000000000 --- a/Source/Details/ASCollectionLayoutState.m +++ /dev/null @@ -1,135 +0,0 @@ -// -// ASCollectionLayoutState.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 - -#import -#import -#import -#import -#import -#import -#import - -@implementation NSMapTable (ASCollectionLayoutConvenience) - -+ (NSMapTable *)elementToLayoutAttributesTable -{ - return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; -} - -@end - -@implementation ASCollectionLayoutState { - NSMapTable *_elementToLayoutAttributesTable; - ASPageTable *> *_pageToLayoutAttributesTable; -} - -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout -{ - ASElementMap *elements = context.elements; - NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; - - for (ASLayout *sublayout in layout.sublayouts) { - ASCollectionElement *element = ((ASCellNode *)sublayout.layoutElement).collectionElement; - if (element == nil) { - ASDisplayNodeFailAssert(@"Element not found!"); - continue; - } - - NSIndexPath *indexPath = [elements indexPathForElement:element]; - NSString *supplementaryElementKind = element.supplementaryElementKind; - - UICollectionViewLayoutAttributes *attrs; - if (supplementaryElementKind == nil) { - attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; - } else { - attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:supplementaryElementKind withIndexPath:indexPath]; - } - - attrs.frame = sublayout.frame; - [table setObject:attrs forKey:element]; - } - - return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; -} - -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table -{ - self = [super init]; - if (self) { - _context = context; - _contentSize = contentSize; - _elementToLayoutAttributesTable = table; - _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; - } - return self; -} - -- (NSArray *)allLayoutAttributes -{ - return [_elementToLayoutAttributesTable.objectEnumerator allObjects]; -} - -- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect -{ - CGSize pageSize = _context.viewportSize; - NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize); - if (pages.count == 0) { - return @[]; - } - - // Use a mutable set here because some items may span multiple pages - NSMutableSet *result = [NSMutableSet set]; - for (id pagePtr in pages) { - ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; - if (allAttrs.count > 0) { - CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); - - if (CGRectContainsRect(rect, pageRect)) { - [result addObjectsFromArray:allAttrs]; - } else { - for (UICollectionViewLayoutAttributes *attrs in allAttrs) { - if (CGRectIntersectsRect(rect, attrs.frame)) { - [result addObject:attrs]; - } - } - } - } - } - return [result allObjects]; -} - -- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath -{ - ASCollectionElement *element = [_context.elements elementForItemAtIndexPath:indexPath]; - return [_elementToLayoutAttributesTable objectForKey:element]; -} - -- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath -{ - ASCollectionElement *element = [_context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; - return [_elementToLayoutAttributesTable objectForKey:element]; -} - -- (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element -{ - return [_elementToLayoutAttributesTable objectForKey:element]; -} - -@end diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm new file mode 100644 index 000000000..0b2616a22 --- /dev/null +++ b/Source/Details/ASCollectionLayoutState.mm @@ -0,0 +1,217 @@ +// +// ASCollectionLayoutState.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 +#import +#import +#import +#import +#import +#import +#import + +@implementation NSMapTable (ASCollectionLayoutConvenience) + ++ (NSMapTable *)elementToLayoutAttributesTable +{ + return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; +} + +@end + +@implementation ASCollectionLayoutState { + ASDN::Mutex __instanceLock__; + NSMapTable *_elementToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_pageToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_unmeasuredPageToLayoutAttributesTable; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + layout:(ASLayout *)layout + getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock +{ + ASElementMap *elements = context.elements; + NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; + + for (ASLayout *sublayout in layout.sublayouts) { + ASCollectionElement *element = getElementBlock(sublayout); + if (element == nil) { + ASDisplayNodeFailAssert(@"Element not found!"); + continue; + } + + NSIndexPath *indexPath = [elements indexPathForElement:element]; + NSString *supplementaryElementKind = element.supplementaryElementKind; + + UICollectionViewLayoutAttributes *attrs; + if (supplementaryElementKind == nil) { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; + } else { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:supplementaryElementKind withIndexPath:indexPath]; + } + + attrs.frame = sublayout.frame; + [table setObject:attrs forKey:element]; + } + + return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + elementToLayoutAttributesTable:(NSMapTable *)table +{ + self = [super init]; + if (self) { + _context = context; + _contentSize = contentSize; + _elementToLayoutAttributesTable = [table copy]; // Copy the given table to make sure clients can't mutate it after this point. + CGSize pageSize = context.viewportSize; + _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:table.objectEnumerator contentSize:contentSize pageSize:pageSize]; + _unmeasuredPageToLayoutAttributesTable = [ASCollectionLayoutState _unmeasuredLayoutAttributesTableFromTable:table contentSize:contentSize pageSize:pageSize]; + } + return self; +} + +- (NSArray *)allLayoutAttributes +{ + return [_elementToLayoutAttributesTable.objectEnumerator allObjects]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = [_context.elements elementForItemAtIndexPath:indexPath]; + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)elementKind + atIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = [_context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element +{ + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +{ + CGSize pageSize = _context.viewportSize; + NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize); + if (pages.count == 0) { + return @[]; + } + + // Use a set here because some items may span multiple pages + NSMutableSet *result = [NSMutableSet set]; + for (id pagePtr in pages) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; + if (allAttrs.count > 0) { + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + + if (CGRectContainsRect(rect, pageRect)) { + [result addObjectsFromArray:allAttrs]; + } else { + for (UICollectionViewLayoutAttributes *attrs in allAttrs) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + [result addObject:attrs]; + } + } + } + } + } + + return [result allObjects]; +} + +- (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize +{ + ASDN::MutexLocker l(__instanceLock__); + if (_unmeasuredPageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { + return nil; + } + + // Step 1: Determine all the pages that intersect the specified rect + NSPointerArray *pagesInRect = ASPageCoordinatesForPagesThatIntersectRect(rect, contentSize, pageSize); + if (pagesInRect.count == 0) { + return nil; + } + + // Step 2: Filter out attributes in these pages that intersect the specified rect. + ASPageToLayoutAttributesTable *result = nil; + for (id pagePtr in pagesInRect) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSMutableArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page]; + if (attrsInPage.count == 0) { + continue; + } + + NSMutableArray *intersectingAttrsInPage = nil; + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + if (CGRectContainsRect(rect, pageRect)) { + // This page fits well within the specified rect. Simply return all of its attributes. + intersectingAttrsInPage = attrsInPage; + } else { + // The page intersects the specified rect. Some attributes in this page are returned, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + if (intersectingAttrsInPage == nil) { + intersectingAttrsInPage = [NSMutableArray array]; + } + [intersectingAttrsInPage addObject:attrs]; + } + } + } + + if (intersectingAttrsInPage.count > 0) { + if (attrsInPage.count == intersectingAttrsInPage.count) { + [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; + } else { + [attrsInPage removeObjectsInArray:intersectingAttrsInPage]; + } + if (result == nil) { + result = [ASPageTable pageTableForStrongObjectPointers]; + } + [result setObject:intersectingAttrsInPage forPage:page]; + } + } + + return result; +} + +#pragma mark - Private methods + ++ (ASPageToLayoutAttributesTable *)_unmeasuredLayoutAttributesTableFromTable:(NSMapTable *)table + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize +{ + NSMutableArray *unmeasuredAttrs = [NSMutableArray array]; + for (ASCollectionElement *element in table) { + UICollectionViewLayoutAttributes *attrs = [table objectForKey:element]; + if (element.nodeIfAllocated == nil || CGSizeEqualToSize(element.nodeIfAllocated.calculatedSize, attrs.frame.size) == NO) { + [unmeasuredAttrs addObject:attrs]; + } + } + + return [ASPageTable pageTableWithLayoutAttributes:unmeasuredAttrs contentSize:contentSize pageSize:pageSize]; +} + +@end diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 2bd56fe5e..e0e7142e3 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -35,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN @class ASCellNode; @class ASCollectionElement; +@class ASCollectionLayoutContext; +@class ASCollectionLayoutState; @class ASDataController; @class ASElementMap; @class ASLayout; @@ -136,22 +138,22 @@ extern NSString * const ASCollectionInvalidUpdateException; * * @discussion This method will be called on main thread. */ -- (id)layoutContextWithElements:(ASElementMap *)elements; +- (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements; /** - * @abstract Prepares in advance a new layout with the given context. + * @abstract Prepares and returns a new layout for given context. * * @param context A context that was previously returned by `-layoutContextWithElements:`. * - * @discussion This method is called ahead of time, i.e before the underlying collection/table view is aware of the provided elements. - * As a result, this method should rely solely on the given context and should not reach out to its collection/table view for information regarding items. + * @return The new layout calculated for the given context. * - * @discussion This method will be called on background theads. It must be thread-safe and should not change any internal state of the conforming object. - * It's recommended to put the resulting layouts of this method into a thread-safe cache that can be looked up later on. + * @discussion This method is called ahead of time, i.e before the underlying collection/table view is aware of the provided elements. + * As a result, clients must solely rely on the given context and should not reach out to other objects for information not available in the context. * - * @discussion This method must block its calling thread. It can dispatch to other theads to reduce blocking time. + * This method will be called on background theads. It must be thread-safe and should not change any internal state of the conforming object. + * It must block the calling thread but can dispatch to other theads to reduce total blocking time. */ -- (void)prepareLayoutWithContext:(id)context; ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; @end diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 5516dbb7d..a83a94da2 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -54,7 +54,7 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException"; -typedef void (^ASDataControllerCompletionBlock)(NSArray *elements, NSArray *nodes); +typedef dispatch_block_t ASDataControllerCompletionBlock; @interface ASDataController () { id _layoutDelegate; @@ -151,12 +151,12 @@ - (void)setLayoutDelegate:(id)layoutDelegate #pragma mark - Cell Layout -- (void)batchAllocateNodesFromElements:(NSArray *)elements andLayout:(BOOL)shouldLayout batchSize:(NSInteger)batchSize batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler +- (void)batchAllocateNodesFromElements:(NSArray *)elements batchSize:(NSInteger)batchSize batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler { ASSERT_ON_EDITING_QUEUE; if (elements.count == 0 || _dataSource == nil) { - batchCompletionHandler(@[], @[]); + batchCompletionHandler(); return; } @@ -171,12 +171,11 @@ - (void)batchAllocateNodesFromElements:(NSArray *)element for (NSUInteger i = 0; i < count; i += batchSize) { NSRange batchedRange = NSMakeRange(i, MIN(count - i, batchSize)); NSArray *batchedElements = [elements subarrayWithRange:batchedRange]; - NSArray *nodes; { as_activity_create_for_scope("Data controller batch"); - nodes = [self _allocateNodesFromElements:batchedElements andLayout:shouldLayout]; + [self _allocateNodesFromElements:batchedElements]; } - batchCompletionHandler(batchedElements, nodes); + batchCompletionHandler(); } ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (_dataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed)); @@ -195,17 +194,15 @@ - (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrai } // TODO Is returned array still needed? Can it be removed? -- (NSArray *)_allocateNodesFromElements:(NSArray *)elements andLayout:(BOOL)shouldLayout +- (void)_allocateNodesFromElements:(NSArray *)elements { ASSERT_ON_EDITING_QUEUE; NSUInteger nodeCount = elements.count; if (!nodeCount || _dataSource == nil) { - return @[]; + return; } - __strong ASCellNode **allocatedNodeBuffer = (__strong ASCellNode **)calloc(nodeCount, sizeof(ASCellNode *)); - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); ASDispatchApply(nodeCount, queue, 0, ^(size_t i) { RETURN_IF_NO_DATASOURCE(); @@ -218,29 +215,12 @@ - (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrai node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps. } - if (shouldLayout) { - // Layout the node if the size range is valid. - ASSizeRange sizeRange = context.constrainedSize; - if (ASSizeRangeHasSignificantArea(sizeRange)) { - [self _layoutNode:node withConstrainedSize:sizeRange]; - } + // Layout the node if the size range is valid. + ASSizeRange sizeRange = context.constrainedSize; + if (ASSizeRangeHasSignificantArea(sizeRange)) { + [self _layoutNode:node withConstrainedSize:sizeRange]; } - - allocatedNodeBuffer[i] = node; }); - - BOOL canceled = _dataSource == nil; - - // Create nodes array - NSArray *nodes = canceled ? nil : [NSArray arrayWithObjects:allocatedNodeBuffer count:nodeCount]; - - // Nil out buffer indexes to allow arc to free the stored cells. - for (int i = 0; i < nodeCount; i++) { - allocatedNodeBuffer[i] = nil; - } - free(allocatedNodeBuffer); - - return nodes; } #pragma mark - Data Source Access (Calling _dataSource) @@ -550,8 +530,8 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet @throw e; } } - - BOOL canDelegateLayout = (_layoutDelegate != nil); + + BOOL canDelegate = (self.layoutDelegate != nil); ASElementMap *newMap; id layoutContext; { @@ -569,7 +549,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet // Step 1.1: Update the mutable copies to match the data source's state [self _updateSectionContextsInMap:mutableMap changeSet:changeSet]; ASPrimitiveTraitCollection existingTraitCollection = [self.node primitiveTraitCollection]; - [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegateLayout) previousMap:previousMap]; + [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate) previousMap:previousMap]; // Step 1.2: Clone the new data newMap = [mutableMap copy]; @@ -577,37 +557,19 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet self.pendingMap = newMap; // Step 2: Ask layout delegate for contexts - if (canDelegateLayout) { - layoutContext = [_layoutDelegate layoutContextWithElements:newMap]; + if (canDelegate) { + layoutContext = [self.layoutDelegate layoutContextWithElements:newMap]; } } as_log_debug(ASCollectionLog(), "New content: %@", newMap.smallDescription); - + + Class layoutDelegateClass = [self.layoutDelegate class]; 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); - // Step 3: Allocate and layout elements if can't delegate - NSArray *elementsToProcess; - if (canDelegateLayout) { - // Allocate all nodes before handling them to the layout delegate. - // In the future, we may want to let the delegate drive allocation as well. - elementsToProcess = ASArrayByFlatMapping(newMap, - ASCollectionElement *element, - (element.nodeIfAllocated == nil ? element : nil)); - } else { - elementsToProcess = ASArrayByFlatMapping(newMap, - ASCollectionElement *element, - (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); - } - - [self batchAllocateNodesFromElements:elementsToProcess andLayout:(! canDelegateLayout) batchSize:elementsToProcess.count batchCompletion:^(NSArray *elements, NSArray *nodes) { - ASSERT_ON_EDITING_QUEUE; - - if (canDelegateLayout) { - [_layoutDelegate prepareLayoutWithContext:layoutContext]; - } + dispatch_block_t completion = ^() { [_mainSerialQueue performBlockOnMainThread:^{ as_activity_scope_leave(&preparationScope); // TODO Merge the two delegate methods below @@ -625,7 +587,18 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet self.visibleMap = newMap; }]; }]; - }]; + }; + + // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements + if (canDelegate) { + [layoutDelegateClass calculateLayoutWithContext:layoutContext]; + completion(); + } else { + NSArray *elementsToProcess = ASArrayByFlatMapping(newMap, + ASCollectionElement *element, + (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); + [self batchAllocateNodesFromElements:elementsToProcess batchSize:elementsToProcess.count batchCompletion:completion]; + } }); if (_usesSynchronousDataLoading) { @@ -837,7 +810,6 @@ - (void)environmentDidChange // Can't update the trait collection right away because _visibleMap may not be up-to-date, // i.e there might be some elements that were allocated using the old trait collection but haven't been added to _visibleMap - [self _scheduleBlockOnMainSerialQueue:^{ ASPrimitiveTraitCollection newTraitCollection = [self.node primitiveTraitCollection]; for (ASCollectionElement *element in _visibleMap) { diff --git a/Source/Private/ASHashing.h b/Source/Details/ASHashing.h similarity index 100% rename from Source/Private/ASHashing.h rename to Source/Details/ASHashing.h diff --git a/Source/Private/ASHashing.m b/Source/Details/ASHashing.m similarity index 100% rename from Source/Private/ASHashing.m rename to Source/Details/ASHashing.m diff --git a/Source/Details/ASPageTable.h b/Source/Details/ASPageTable.h index d7136f8c7..e295e0256 100644 --- a/Source/Details/ASPageTable.h +++ b/Source/Details/ASPageTable.h @@ -69,6 +69,11 @@ ASDISPLAYNODE_EXTERN_C_END */ typedef NSMapTable ASPageTable; +/** + * A page to array of layout attributes table. + */ +typedef ASPageTable *> ASPageToLayoutAttributesTable; + /** * A category for creating & using map tables meant for storing objects using ASPage as keys. */ @@ -93,7 +98,7 @@ typedef NSMapTable ASPageTable; * * @param pageSize The size of each page. */ -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; /** * 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 626ca975f..5cbf758dd 100644 --- a/Source/Details/ASPageTable.m +++ b/Source/Details/ASPageTable.m @@ -110,9 +110,9 @@ + (ASPageTable *)pageTableForWeakObjectPointers return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; } -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize { - ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; + ASPageToLayoutAttributesTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { // This attrs may span multiple pages. Make sure it's registered to all of them NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(attrs.frame, contentSize, pageSize); diff --git a/Source/Private/ASCollectionLayout.h b/Source/Private/ASCollectionLayout.h index f5bc713c5..45f12d707 100644 --- a/Source/Private/ASCollectionLayout.h +++ b/Source/Private/ASCollectionLayout.h @@ -2,13 +2,8 @@ // ASCollectionLayout.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"); +// 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 // @@ -25,7 +20,6 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED - @interface ASCollectionLayout : UICollectionViewLayout /** diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 2ca86f478..935a3d680 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -18,27 +18,34 @@ #import #import +#import #import #import +#import #import #import -#import +#import #import +#import #import #import #import -#import +#import + +static const ASRangeTuningParameters kASDefaultMeasureRangeTuningParameters = { + .leadingBufferScreenfuls = 2.0, + .trailingBufferScreenfuls = 2.0 +}; + +static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); @interface ASCollectionLayout () { - ASDN::Mutex __instanceLock__; // Non-recursive mutex, ftw! - - // Main thread only. - ASCollectionLayoutState *_layout; - - // The pending state calculated ahead of time, if any. - ASCollectionLayoutState *_pendingLayout; - - BOOL _layoutDelegateImplementsAdditionalInfoForLayoutWithElements; + ASCollectionLayoutCache *_layoutCache; + ASCollectionLayoutState *_layout; // Main thread only. + + struct { + unsigned int implementsAdditionalInfoForLayoutWithElements:1; + } _layoutDelegateFlags; } @end @@ -51,30 +58,53 @@ - (instancetype)initWithLayoutDelegate:(id)layoutDel if (self) { ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil"); _layoutDelegate = layoutDelegate; - _layoutDelegateImplementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutCache = [[ASCollectionLayoutCache alloc] init]; } return self; } #pragma mark - ASDataControllerLayoutDelegate -- (id)layoutContextWithElements:(ASElementMap *)elements +- (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); - CGSize viewportSize = [self viewportSize]; + CGSize viewportSize = [self _viewportSize]; id additionalInfo = nil; - if (_layoutDelegateImplementsAdditionalInfoForLayoutWithElements) { + if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } - return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize elements:elements additionalInfo:additionalInfo]; + return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize + scrollableDirections:[_layoutDelegate scrollableDirections] + elements:elements + layoutDelegateClass:[_layoutDelegate class] + layoutCache:_layoutCache + additionalInfo:additionalInfo]; } -- (void)prepareLayoutWithContext:(id)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { - ASCollectionLayoutState *layout = [_layoutDelegate calculateLayoutWithContext:context]; - - ASDN::MutexLocker l(__instanceLock__); - _pendingLayout = layout; + if (context.elements == nil) { + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:CGSizeZero + elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; + } + + ASDisplayNodeAssertTrue([context.layoutDelegateClass conformsToProtocol:@protocol(ASCollectionLayoutDelegate)]); + ASCollectionLayoutState *layout = [context.layoutDelegateClass calculateLayoutWithContext:context]; + [context.layoutCache setLayout:layout forContext:context]; + + // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly + CGSize viewportSize = context.viewportSize; + // TODO Consider content offset of the collection node + CGRect initialRect = CGRectMake(0, 0, viewportSize.width, viewportSize.height); + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, + kASDefaultMeasureRangeTuningParameters, + context.scrollableDirections, + kASStaticScrollDirection); + [self _measureElementsInRect:measureRect blockingRect:initialRect layout:layout]; + + return layout; } #pragma mark - UICollectionViewLayout overrides @@ -83,30 +113,29 @@ - (void)prepareLayout { ASDisplayNodeAssertMainThread(); [super prepareLayout]; + ASCollectionLayoutContext *context = [self layoutContextWithElements:_collectionNode.visibleElements]; - - ASCollectionLayoutState *layout = nil; - { - ASDN::MutexLocker l(__instanceLock__); - if (_pendingLayout != nil && ASObjectIsEqual(_pendingLayout.context, context)) { - // Looks like we can use the pending layout. Great! - layout = _pendingLayout; - _pendingLayout = nil; - } + if (_layout != nil && ASObjectIsEqual(_layout.context, context)) { + // The existing layout is still valid. No-op + return; } - - if (layout == nil) { - layout = [_layoutDelegate calculateLayoutWithContext:context]; + + if (ASCollectionLayoutState *cachedLayout = [_layoutCache layoutForContext:context]) { + _layout = cachedLayout; + } else { + // A new layout is needed now. Calculate and apply it immediately + _layout = [ASCollectionLayout calculateLayoutWithContext:context]; } - - _layout = layout; } - (void)invalidateLayout { ASDisplayNodeAssertMainThread(); [super invalidateLayout]; - _layout = nil; + if (_layout != nil) { + [_layoutCache removeLayoutForContext:_layout.context]; + _layout = nil; + } } - (CGSize)collectionViewContentSize @@ -116,25 +145,45 @@ - (CGSize)collectionViewContentSize return _layout.contentSize; } -- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)blockingRect { ASDisplayNodeAssertMainThread(); - NSArray *result = [_layout layoutAttributesForElementsInRect:rect]; + if (CGRectIsEmpty(blockingRect)) { + return nil; + } + + // Measure elements in the measure range, block on the requested rect + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect, + kASDefaultMeasureRangeTuningParameters, + _layout.context.scrollableDirections, + kASStaticScrollDirection); + [ASCollectionLayout _measureElementsInRect:measureRect blockingRect:blockingRect layout:_layout]; + NSArray *result = [_layout layoutAttributesForElementsInRect:blockingRect]; + ASElementMap *elements = _layout.context.elements; for (UICollectionViewLayoutAttributes *attrs in result) { ASCollectionElement *element = [elements elementForLayoutAttributes:attrs]; - [ASCollectionLayout setSize:attrs.frame.size toElement:element]; + ASCollectionLayoutSetSizeToElement(attrs.frame.size, element); } - + return result; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { + ASDisplayNodeAssertMainThread(); + ASCollectionElement *element = [_layout.context.elements elementForItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; - [ASCollectionLayout setSize:attrs.frame.size toElement:element]; + + ASCellNode *node = element.node; + CGSize elementSize = attrs.frame.size; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + } + + ASCollectionLayoutSetSizeToElement(attrs.frame.size, element); return attrs; } @@ -142,28 +191,25 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind { ASCollectionElement *element = [_layout.context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; - [ASCollectionLayout setSize:attrs.frame.size toElement:element]; + + ASCellNode *node = element.node; + CGSize elementSize = attrs.frame.size; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + } + + ASCollectionLayoutSetSizeToElement(attrs.frame.size, element); return attrs; } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { - return (! CGSizeEqualToSize([self viewportSize], newBounds.size)); + return (! CGSizeEqualToSize([self _viewportSize], newBounds.size)); } #pragma mark - Private methods -+ (void)setSize:(CGSize)size toElement:(ASCollectionElement *)element -{ - ASCellNode *node = element.node; - if (! CGSizeEqualToSize(size, node.frame.size)) { - CGRect nodeFrame = CGRectZero; - nodeFrame.size = size; - node.frame = nodeFrame; - } -} - -- (CGSize)viewportSize +- (CGSize)_viewportSize { ASCollectionNode *collectionNode = _collectionNode; if (collectionNode != nil && !collectionNode.isNodeLoaded) { @@ -175,4 +221,120 @@ - (CGSize)viewportSize } } +/** + * Measures all elements in the specified rect and blocks the calling thread while measuring those in the blocking rect. + */ ++ (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect layout:(ASCollectionLayoutState *)layout +{ + if (CGRectIsEmpty(rect) || layout.context.elements == nil) { + return; + } + BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect); + if (hasBlockingRect && CGRectContainsRect(rect, blockingRect) == NO) { + ASDisplayNodeCAssert(NO, @"Blocking rect, if specified, must be within the other (outer) rect"); + return; + } + + // Step 1: Clamp the specified rects between the bounds of content rect + CGSize contentSize = layout.contentSize; + CGRect contentRect = CGRectMake(0, 0, contentSize.width, contentSize.height); + rect = CGRectIntersection(contentRect, rect); + if (CGRectIsNull(rect)) { + return; + } + if (hasBlockingRect) { + blockingRect = CGRectIntersection(contentRect, blockingRect); + hasBlockingRect = !CGRectIsNull(blockingRect); + } + + // Step 2: Get layout attributes of all elements within the specified outer rect + ASCollectionLayoutContext *context = layout.context; + CGSize pageSize = context.viewportSize; + ASPageToLayoutAttributesTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect + contentSize:contentSize + pageSize:pageSize]; + if (attrsTable.count == 0) { + // No elements in this rect! Bail early + return; + } + + // Step 3: Split all those attributes into blocking and non-blocking buckets + // Use ordered sets here because some items may span multiple pages, and the sets will be accessed by indexes later on. + NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; + NSMutableOrderedSet *nonBlockingAttrs = [NSMutableOrderedSet orderedSet]; + for (id pagePtr in attrsTable) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSArray *attrsInPage = [attrsTable objectForPage:page]; + // Calculate the page's rect but only if it's going to be used. + CGRect pageRect = hasBlockingRect ? ASPageCoordinateGetPageRect(page, pageSize) : CGRectZero; + + if (hasBlockingRect && CGRectContainsRect(blockingRect, pageRect)) { + // The page fits well within the blocking rect. All attributes in this page are blocking. + [blockingAttrs addObjectsFromArray:attrsInPage]; + } else if (hasBlockingRect && CGRectIntersectsRect(blockingRect, pageRect)) { + // The page intersects the blocking rect. Some elements in this page are blocking, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(blockingRect, attrs.frame)) { + [blockingAttrs addObject:attrs]; + } else { + [nonBlockingAttrs addObject:attrs]; + } + } + } else { + // The page doesn't intersect the blocking rect. All elements in this page are non-blocking. + [nonBlockingAttrs addObjectsFromArray:attrsInPage]; + } + } + + // Step 4: Allocate and measure blocking elements' node + ASElementMap *elements = context.elements; + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + if (NSUInteger count = blockingAttrs.count) { + ASDispatchApply(count, queue, 0, ^(size_t i) { + UICollectionViewLayoutAttributes *attrs = blockingAttrs[i]; + ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node; + CGSize expectedSize = attrs.frame.size; + if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)]; + } + }); + } + + // Step 5: Allocate and measure non-blocking ones + if (NSUInteger count = nonBlockingAttrs.count) { + __weak ASElementMap *weakElements = elements; + ASDispatchAsync(count, queue, 0, ^(size_t i) { + __strong ASElementMap *strongElements = weakElements; + if (strongElements) { + UICollectionViewLayoutAttributes *attrs = nonBlockingAttrs[i]; + ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node; + CGSize expectedSize = attrs.frame.size; + if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)]; + } + } + }); + } +} + +# pragma mark - Convenient inline functions + +ASDISPLAYNODE_INLINE ASSizeRange ASCollectionLayoutElementSizeRangeFromSize(CGSize size) +{ + // The layout delegate consulted us that this element must fit within this size, + // and the only way to achieve that without asking it again is to use an exact size range here. + return ASSizeRangeMake(size); +} + +ASDISPLAYNODE_INLINE void ASCollectionLayoutSetSizeToElement(CGSize size, ASCollectionElement *element) +{ + if (ASCellNode *node = element.node) { + if (! CGSizeEqualToSize(size, node.frame.size)) { + CGRect frame = CGRectZero; + frame.size = size; + node.frame = frame; + } + } +} + @end diff --git a/Source/Private/ASCollectionLayoutCache.h b/Source/Private/ASCollectionLayoutCache.h new file mode 100644 index 000000000..1bf336b61 --- /dev/null +++ b/Source/Private/ASCollectionLayoutCache.h @@ -0,0 +1,34 @@ +// +// ASCollectionLayoutCache.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 + +@class ASCollectionLayoutContext, ASCollectionLayoutState; + +/// A thread-safe cache for ASCollectionLayoutContext-ASCollectionLayoutState pairs +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionLayoutCache : NSObject + +- (nullable ASCollectionLayoutState *)layoutForContext:(ASCollectionLayoutContext *)context; + +- (void)setLayout:(ASCollectionLayoutState *)layout forContext:(ASCollectionLayoutContext *)context; + +- (void)removeLayoutForContext:(ASCollectionLayoutContext *)context; + +- (void)removeAllLayouts; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASCollectionLayoutCache.mm b/Source/Private/ASCollectionLayoutCache.mm new file mode 100644 index 000000000..94b2bc18b --- /dev/null +++ b/Source/Private/ASCollectionLayoutCache.mm @@ -0,0 +1,90 @@ +// +// ASCollectionLayoutCache.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 +#import + +@implementation ASCollectionLayoutCache { + ASDN::Mutex __instanceLock__; + + /** + * The underlying data structure of this cache. + * + * The outer map table is a weak to strong table. That is because ASCollectionLayoutContext doesn't (and shouldn't) + * hold a strong reference on its element map. As a result, this cache should handle the case in which + * an element map no longer exists and all contexts and layouts associated with it should be cleared. + * + * The inner map table is a standard strong to strong map. + * Since different ASCollectionLayoutContext objects with the same content are considered equal, + * "object pointer personality" can't be used as a key option. + */ + NSMapTable *> *_map; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _map = [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; + } + return self; +} + +- (ASCollectionLayoutState *)layoutForContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (elements == nil) { + return nil; + } + + ASDN::MutexLocker l(__instanceLock__); + return [[_map objectForKey:elements] objectForKey:context]; +} + +- (void)setLayout:(ASCollectionLayoutState *)layout forContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (layout == nil || elements == nil) { + return; + } + + ASDN::MutexLocker l(__instanceLock__); + auto innerMap = [_map objectForKey:elements]; + if (innerMap == nil) { + innerMap = [NSMapTable strongToStrongObjectsMapTable]; + [_map setObject:innerMap forKey:elements]; + } + [innerMap setObject:layout forKey:context]; +} + +- (void)removeLayoutForContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (elements == nil) { + return; + } + + ASDN::MutexLocker l(__instanceLock__); + [[_map objectForKey:elements] removeObjectForKey:context]; +} + +- (void)removeAllLayouts +{ + ASDN::MutexLocker l(__instanceLock__); + [_map removeAllObjects]; +} + +@end diff --git a/Source/Private/ASCollectionLayoutContext+Private.h b/Source/Private/ASCollectionLayoutContext+Private.h index f6827be8d..3c615aef2 100644 --- a/Source/Private/ASCollectionLayoutContext+Private.h +++ b/Source/Private/ASCollectionLayoutContext+Private.h @@ -17,11 +17,22 @@ #import +@class ASCollectionLayoutCache; +@protocol ASCollectionLayoutDelegate; + NS_ASSUME_NONNULL_BEGIN @interface ASCollectionLayoutContext (Private) -- (instancetype)initWithViewportSize:(CGSize)viewportSize elements:(ASElementMap *)elements additionalInfo:(nullable id)additionalInfo; +@property (nonatomic, strong, readonly) Class layoutDelegateClass; +@property (nonatomic, weak, readonly) ASCollectionLayoutCache *layoutCache; + +- (instancetype)initWithViewportSize:(CGSize)viewportSize + scrollableDirections:(ASScrollDirection)scrollableDirections + elements:(ASElementMap *)elements + layoutDelegateClass:(Class)layoutDelegateClass + layoutCache:(ASCollectionLayoutCache *)layoutCache + additionalInfo:(nullable id)additionalInfo; @end diff --git a/Source/Private/ASCollectionLayoutDefines.h b/Source/Private/ASCollectionLayoutDefines.h new file mode 100644 index 000000000..07be880a0 --- /dev/null +++ b/Source/Private/ASCollectionLayoutDefines.h @@ -0,0 +1,27 @@ +// +// ASCollectionLayoutDefines.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 +#import + +NS_ASSUME_NONNULL_BEGIN + +ASDISPLAYNODE_EXTERN_C_BEGIN + +FOUNDATION_EXPORT ASSizeRange ASSizeRangeForCollectionLayoutThatFitsViewportSize(CGSize viewportSize, ASScrollDirection scrollableDirections) AS_WARN_UNUSED_RESULT; + +ASDISPLAYNODE_EXTERN_C_END + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASCollectionLayoutDefines.m b/Source/Private/ASCollectionLayoutDefines.m new file mode 100644 index 000000000..b8c9c21cc --- /dev/null +++ b/Source/Private/ASCollectionLayoutDefines.m @@ -0,0 +1,27 @@ +// +// ASCollectionLayoutDefines.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 + +extern ASSizeRange ASSizeRangeForCollectionLayoutThatFitsViewportSize(CGSize viewportSize, ASScrollDirection scrollableDirections) +{ + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections) == NO) { + sizeRange.min.height = viewportSize.height; + sizeRange.max.height = viewportSize.height; + } + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections) == NO) { + sizeRange.min.width = viewportSize.width; + sizeRange.max.width = viewportSize.width; + } + return sizeRange; +} diff --git a/Source/Private/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h new file mode 100644 index 000000000..170e57acc --- /dev/null +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -0,0 +1,31 @@ +// +// ASCollectionLayoutState+Private.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 + +@interface ASCollectionLayoutState (Private) + +/** + * Remove and returns layout attributes for unmeasured elements that intersect the specified rect + * + * @discussion This method is atomic and thread-safe + */ +- (nullable ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASDispatch.h b/Source/Private/ASDispatch.h index c0a14c77d..4f2a1cda9 100644 --- a/Source/Private/ASDispatch.h +++ b/Source/Private/ASDispatch.h @@ -16,7 +16,9 @@ // #import -#import +#import + +ASDISPLAYNODE_EXTERN_C_BEGIN /** * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. @@ -24,24 +26,14 @@ * Note: The actual number of threads may be lower than threadCount, if libdispatch * decides the system can't handle it. In reality this rarely happens. */ -static void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { - if (threadCount == 0) { - threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; - } - dispatch_group_t group = dispatch_group_create(); - // HACK: This is a workaround for mm files that include this in Clang4.0 - // Omitting ATOMIC_VAR_INIT is okay in this case because the current - // expansion of that macro no-ops. - // TODO: Move this implementation into a m file so it's not compiled in C++ - // See: https://github.com/TextureGroup/Texture/pull/426 - __block atomic_size_t counter = 0; - for (NSUInteger t = 0; t < threadCount; t++) { - dispatch_group_async(group, queue, ^{ - size_t i; - while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { - work(i); - } - }); - } - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); -}; +void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/Source/Private/ASDispatch.m b/Source/Private/ASDispatch.m new file mode 100644 index 000000000..14c60eb6d --- /dev/null +++ b/Source/Private/ASDispatch.m @@ -0,0 +1,59 @@ +// +// ASDispatch.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 + +/** + * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + dispatch_group_t group = dispatch_group_create(); + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_group_async(group, queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); +}; + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_async(queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } +}; + diff --git a/Source/Private/_ASCollectionGalleryLayoutItem.h b/Source/Private/_ASCollectionGalleryLayoutItem.h new file mode 100644 index 000000000..07ba41d68 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutItem.h @@ -0,0 +1,38 @@ +// +// _ASCollectionGalleryLayoutItem.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 + +@class ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A dummy item that represents a collection element to participate in the collection layout calculation process + * without triggering measurement on the actual node of the collection element. + * + * This item always has a fixed size that is the item size passed to it. + */ +AS_SUBCLASSING_RESTRICTED +@interface _ASGalleryLayoutItem : NSObject + +@property (nonatomic, assign, readonly) CGSize itemSize; +@property (nonatomic, weak, readonly) ASCollectionElement *collectionElement; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement; +- (instancetype)init __unavailable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/_ASCollectionGalleryLayoutItem.mm b/Source/Private/_ASCollectionGalleryLayoutItem.mm new file mode 100644 index 000000000..688ef44d4 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutItem.mm @@ -0,0 +1,86 @@ +// +// _ASCollectionGalleryLayoutItem.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 +#import +#import + +@implementation _ASGalleryLayoutItem { + std::atomic _primitiveTraitCollection; +} + +@synthesize style; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement +{ + self = [super init]; + if (self) { + ASDisplayNodeAssert(! CGSizeEqualToSize(CGSizeZero, itemSize), @"Item size should not be zero"); + ASDisplayNodeAssertNotNil(collectionElement, @"Collection element should not be nil"); + _itemSize = itemSize; + _collectionElement = collectionElement; + } + return self; +} + +ASLayoutElementStyleExtensibilityForwarding +ASPrimitiveTraitCollectionDefaults +ASPrimitiveTraitCollectionDeprecatedImplementation + +- (ASTraitCollection *)asyncTraitCollection +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeLayoutSpec; +} + +- (NSArray> *)sublayoutElements +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (BOOL)implementsLayoutMethod +{ + return YES; +} + +ASLayoutElementLayoutCalculationDefaults + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + ASDisplayNodeAssert(CGSizeEqualToSize(_itemSize, ASSizeRangeClamp(constrainedSize, _itemSize)), + @"Item size %@ can't fit within the bounds of constrained size %@", NSStringFromCGSize(_itemSize), NSStringFromASSizeRange(constrainedSize)); + return [ASLayout layoutWithLayoutElement:self size:_itemSize]; +} + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + return [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; +} + +@end diff --git a/Tests/ASDispatchTests.m b/Tests/ASDispatchTests.m index ba834b672..f159f5e9d 100644 --- a/Tests/ASDispatchTests.m +++ b/Tests/ASDispatchTests.m @@ -2,8 +2,17 @@ // ASDispatchTests.m // Texture // -// Created by Adlai Holler on 8/25/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import @@ -35,4 +44,29 @@ - (void)testDispatchApply XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]); } +- (void)testDispatchAsync +{ + dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSInteger expectedThreadCount = [NSProcessInfo processInfo].activeProcessorCount * 2; + NSLock *lock = [NSLock new]; + NSMutableSet *threads = [NSMutableSet set]; + NSMutableIndexSet *indices = [NSMutableIndexSet indexSet]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Executed all blocks"]; + + size_t const iterations = 1E5; + ASDispatchAsync(iterations, q, 0, ^(size_t i) { + [lock lock]; + [threads addObject:[NSThread currentThread]]; + XCTAssertFalse([indices containsIndex:i]); + [indices addIndex:i]; + if (indices.count == iterations) { + [expectation fulfill]; + } + [lock unlock]; + }); + [self waitForExpectationsWithTimeout:10 handler:nil]; + XCTAssertLessThanOrEqual(threads.count, expectedThreadCount); + XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]); +} + @end diff --git a/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index d00ea60df..13bfe64c7 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/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) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import "ViewController.h" @@ -21,6 +21,8 @@ #import "SupplementaryNode.h" #import "ItemNode.h" +#define ASYNC_COLLECTION_LAYOUT 0 + @interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @@ -43,8 +45,18 @@ - (void)dealloc - (void)viewDidLoad { [super viewDidLoad]; + +#if ASYNC_COLLECTION_LAYOUT + id layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections + itemSize:CGSizeMake(180, 90)]; + self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; +#else + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.headerReferenceSize = CGSizeMake(50.0, 50.0); + layout.footerReferenceSize = CGSizeMake(50.0, 50.0); + self.collectionNode = [[ASCollectionNode alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; +#endif - self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:[[ASCollectionFlowLayoutDelegate alloc] init] layoutFacilitator:nil]; self.collectionNode.dataSource = self; self.collectionNode.delegate = self; diff --git a/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj b/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj index c1014c643..91818f8f8 100644 --- a/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; + E5B2252C1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -35,6 +36,8 @@ AC3C4A691A11F47200143C57 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; E2F287D91FFDEA2A747630CE /* 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 = ""; }; + E5B2252A1F1791DE001E1431 /* MosaicCollectionLayoutInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MosaicCollectionLayoutInfo.h; sourceTree = ""; }; + E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MosaicCollectionLayoutInfo.m; sourceTree = ""; }; E5D73A3A1EA6766B006418A8 /* MosaicCollectionLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MosaicCollectionLayoutDelegate.h; sourceTree = ""; }; F36BCD8EBAF79797AB5C6708 /* 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 = ""; }; /* End PBXFileReference section */ @@ -83,6 +86,8 @@ children = ( E5D73A3A1EA6766B006418A8 /* MosaicCollectionLayoutDelegate.h */, 25A1FA841C02F7AC00193875 /* MosaicCollectionLayoutDelegate.m */, + E5B2252A1F1791DE001E1431 /* MosaicCollectionLayoutInfo.h */, + E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */, AC3C4A651A11F47200143C57 /* AppDelegate.h */, AC3C4A661A11F47200143C57 /* AppDelegate.m */, AC3C4A681A11F47200143C57 /* ViewController.h */, @@ -152,6 +157,7 @@ TargetAttributes = { AC3C4A5D1A11F47200143C57 = { CreatedOnToolsVersion = 6.1; + DevelopmentTeam = XSR3D45JSF; }; }; }; @@ -244,6 +250,7 @@ AC3C4A641A11F47200143C57 /* main.m in Sources */, 80364CCA1E3D95A90094400C /* ImageCollectionViewCell.m in Sources */, 25A1FA881C02FCB000193875 /* ImageCellNode.m in Sources */, + E5B2252C1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -333,9 +340,11 @@ baseConfigurationReference = F36BCD8EBAF79797AB5C6708 /* Pods-Sample.debug.xcconfig */; buildSettings = { 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)"; TARGETED_DEVICE_FAMILY = 1; }; @@ -346,9 +355,11 @@ baseConfigurationReference = E2F287D91FFDEA2A747630CE /* Pods-Sample.release.xcconfig */; buildSettings = { 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)"; TARGETED_DEVICE_FAMILY = 1; }; diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m index 4bbd53e6a..a7383f294 100644 --- a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -11,63 +11,65 @@ // #import "MosaicCollectionLayoutDelegate.h" +#import "MosaicCollectionLayoutInfo.h" #import "ImageCellNode.h" #import @implementation MosaicCollectionLayoutDelegate { // Read-only properties - NSInteger _numberOfColumns; - CGFloat _headerHeight; - CGFloat _columnSpacing; - UIEdgeInsets _sectionInset; - UIEdgeInsets _interItemSpacing; + MosaicCollectionLayoutInfo *_info; } - (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns headerHeight:(CGFloat)headerHeight { self = [super init]; if (self != nil) { - _numberOfColumns = numberOfColumns; - _headerHeight = headerHeight; - _columnSpacing = 10.0; - _sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0); - _interItemSpacing = UIEdgeInsetsMake(10.0, 0, 10.0, 0); + _info = [[MosaicCollectionLayoutInfo alloc] initWithNumberOfColumns:numberOfColumns + headerHeight:headerHeight + columnSpacing:10.0 + sectionInsets:UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0) + interItemSpacing:UIEdgeInsetsMake(10.0, 0, 10.0, 0)]; } return self; } +- (ASScrollDirection)scrollableDirections +{ + return ASScrollDirectionVerticalDirections; +} + - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { - return nil; + return _info; } -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { CGFloat layoutWidth = context.viewportSize.width; ASElementMap *elements = context.elements; CGFloat top = 0; + MosaicCollectionLayoutInfo *info = (MosaicCollectionLayoutInfo *)context.additionalInfo; - // TODO use +[NSMapTable elementToLayoutAttributesTable] - NSMapTable *attrsMap = [NSMapTable mapTableWithKeyOptions:(NSMapTableObjectPointerPersonality | NSMapTableWeakMemory) valueOptions:NSMapTableStrongMemory]; + NSMapTable *attrsMap = [NSMapTable elementToLayoutAttributesTable]; NSMutableArray *columnHeights = [NSMutableArray array]; NSInteger numberOfSections = [elements numberOfSections]; for (NSUInteger section = 0; section < numberOfSections; section++) { NSInteger numberOfItems = [elements numberOfItemsInSection:section]; - top += _sectionInset.top; + top += info.sectionInsets.top; - if (_headerHeight > 0) { + if (info.headerHeight > 0) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; ASCollectionElement *element = [elements supplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader - withIndexPath:indexPath]; + withIndexPath:indexPath]; - ASSizeRange sizeRange = [self sizeRangeForHeaderOfSection:section withLayoutWidth:layoutWidth]; + ASSizeRange sizeRange = [self _sizeRangeForHeaderOfSection:section withLayoutWidth:layoutWidth info:info]; CGSize size = [element.node layoutThatFits:sizeRange].size; - CGRect frame = CGRectMake(_sectionInset.left, top, size.width, size.height); + CGRect frame = CGRectMake(info.sectionInsets.left, top, size.width, size.height); attrs.frame = frame; [attrsMap setObject:attrs forKey:element]; @@ -75,31 +77,31 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte } [columnHeights addObject:[NSMutableArray array]]; - for (NSUInteger idx = 0; idx < _numberOfColumns; idx++) { + for (NSUInteger idx = 0; idx < info.numberOfColumns; idx++) { [columnHeights[section] addObject:@(top)]; } - CGFloat columnWidth = [self _columnWidthForSection:section withLayoutWidth:layoutWidth]; + CGFloat columnWidth = [self _columnWidthForSection:section withLayoutWidth:layoutWidth info:info]; for (NSUInteger idx = 0; idx < numberOfItems; idx++) { NSUInteger columnIndex = [self _shortestColumnIndexInSection:section withColumnHeights:columnHeights]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section]; ASCollectionElement *element = [elements elementForItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; - ASSizeRange sizeRange = [self sizeRangeForItem:element.node atIndexPath:indexPath withLayoutWidth:layoutWidth]; + ASSizeRange sizeRange = [self _sizeRangeForItem:element.node atIndexPath:indexPath withLayoutWidth:layoutWidth info:info]; CGSize size = [element.node layoutThatFits:sizeRange].size; - CGPoint position = CGPointMake(_sectionInset.left + (columnWidth + _columnSpacing) * columnIndex, - [columnHeights[section][columnIndex] floatValue]); + CGPoint position = CGPointMake(info.sectionInsets.left + (columnWidth + info.columnSpacing) * columnIndex, + [columnHeights[section][columnIndex] floatValue]); CGRect frame = CGRectMake(position.x, position.y, size.width, size.height); attrs.frame = frame; [attrsMap setObject:attrs forKey:element]; // TODO Profile and avoid boxing if there are significant retain/release overheads - columnHeights[section][columnIndex] = @(CGRectGetMaxY(frame) + _interItemSpacing.bottom); + columnHeights[section][columnIndex] = @(CGRectGetMaxY(frame) + info.interItemSpacing.bottom); } NSUInteger columnIndex = [self _tallestColumnIndexInSection:section withColumnHeights:columnHeights]; - top = [columnHeights[section][columnIndex] floatValue] - _interItemSpacing.bottom + _sectionInset.bottom; + top = [columnHeights[section][columnIndex] floatValue] - info.interItemSpacing.bottom + info.sectionInsets.bottom; for (NSUInteger idx = 0; idx < [columnHeights[section] count]; idx++) { columnHeights[section][idx] = @(top); @@ -108,22 +110,24 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte CGFloat contentHeight = [[[columnHeights lastObject] firstObject] floatValue]; CGSize contentSize = CGSizeMake(layoutWidth, contentHeight); - return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:contentSize elementToLayoutAttributesTable:attrsMap]; + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:contentSize + elementToLayoutAttributesTable:attrsMap]; } -- (CGFloat)_widthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth ++ (CGFloat)_columnWidthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - return layoutWidth - _sectionInset.left - _sectionInset.right; + return ([self _widthForSection:section withLayoutWidth:layoutWidth info:info] - ((info.numberOfColumns - 1) * info.columnSpacing)) / info.numberOfColumns; } -- (CGFloat)_columnWidthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth ++ (CGFloat)_widthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - return ([self _widthForSection:section withLayoutWidth:layoutWidth] - ((_numberOfColumns - 1) * _columnSpacing)) / _numberOfColumns; + return layoutWidth - info.sectionInsets.left - info.sectionInsets.right; } -- (ASSizeRange)sizeRangeForItem:(ASCellNode *)item atIndexPath:(NSIndexPath *)indexPath withLayoutWidth:(CGFloat)layoutWidth; ++ (ASSizeRange)_sizeRangeForItem:(ASCellNode *)item atIndexPath:(NSIndexPath *)indexPath withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - CGFloat itemWidth = [self _columnWidthForSection:indexPath.section withLayoutWidth:layoutWidth]; + CGFloat itemWidth = [self _columnWidthForSection:indexPath.section withLayoutWidth:layoutWidth info:info]; if ([item isKindOfClass:[ImageCellNode class]]) { return ASSizeRangeMake(CGSizeMake(itemWidth, 0), CGSizeMake(itemWidth, CGFLOAT_MAX)); } else { @@ -131,12 +135,12 @@ - (ASSizeRange)sizeRangeForItem:(ASCellNode *)item atIndexPath:(NSIndexPath *)in } } -- (ASSizeRange)sizeRangeForHeaderOfSection:(NSInteger)section withLayoutWidth:(CGFloat)layoutWidth ++ (ASSizeRange)_sizeRangeForHeaderOfSection:(NSInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - return ASSizeRangeMake(CGSizeMake(0, _headerHeight), CGSizeMake([self _widthForSection:section withLayoutWidth:layoutWidth], _headerHeight)); + return ASSizeRangeMake(CGSizeMake(0, info.headerHeight), CGSizeMake([self _widthForSection:section withLayoutWidth:layoutWidth info:info], info.headerHeight)); } -- (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights ++ (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights { __block NSUInteger index = 0; __block CGFloat tallestHeight = 0; @@ -149,7 +153,7 @@ - (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section withColumnHeights return index; } -- (NSUInteger)_shortestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights ++ (NSUInteger)_shortestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights { __block NSUInteger index = 0; __block CGFloat shortestHeight = CGFLOAT_MAX; diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h new file mode 100644 index 000000000..1e7db5204 --- /dev/null +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h @@ -0,0 +1,32 @@ +// +// MosaicCollectionLayoutInfo.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 MosaicCollectionLayoutInfo : NSObject + +// Read-only properties +@property (nonatomic, assign, readonly) NSInteger numberOfColumns; +@property (nonatomic, assign, readonly) CGFloat headerHeight; +@property (nonatomic, assign, readonly) CGFloat columnSpacing; +@property (nonatomic, assign, readonly) UIEdgeInsets sectionInsets; +@property (nonatomic, assign, readonly) UIEdgeInsets interItemSpacing; + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns + headerHeight:(CGFloat)headerHeight + columnSpacing:(CGFloat)columnSpacing + sectionInsets:(UIEdgeInsets)sectionInsets + interItemSpacing:(UIEdgeInsets)interItemSpacing NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m new file mode 100644 index 000000000..f7a4224ab --- /dev/null +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m @@ -0,0 +1,78 @@ +// +// MosaicCollectionLayoutInfo.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 "MosaicCollectionLayoutInfo.h" + +#import + +@implementation MosaicCollectionLayoutInfo + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns + headerHeight:(CGFloat)headerHeight + columnSpacing:(CGFloat)columnSpacing + sectionInsets:(UIEdgeInsets)sectionInsets + interItemSpacing:(UIEdgeInsets)interItemSpacing +{ + self = [super init]; + if (self) { + _numberOfColumns = numberOfColumns; + _headerHeight = headerHeight; + _columnSpacing = columnSpacing; + _sectionInsets = sectionInsets; + _interItemSpacing = interItemSpacing; + } + return self; +} + +- (BOOL)isEqualToInfo:(MosaicCollectionLayoutInfo *)info +{ + if (info == nil) { + return NO; + } + + return _numberOfColumns == info.numberOfColumns + && _headerHeight == info.headerHeight + && _columnSpacing == info.columnSpacing + && UIEdgeInsetsEqualToEdgeInsets(_sectionInsets, info.sectionInsets) + && UIEdgeInsetsEqualToEdgeInsets(_interItemSpacing, info.interItemSpacing); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[MosaicCollectionLayoutInfo class]]) { + return NO; + } + return [self isEqualToInfo:other]; +} + +- (NSUInteger)hash +{ + struct { + NSInteger numberOfColumns; + CGFloat headerHeight; + CGFloat columnSpacing; + UIEdgeInsets sectionInsets; + UIEdgeInsets interItemSpacing; + } data = { + _numberOfColumns, + _headerHeight, + _columnSpacing, + _sectionInsets, + _interItemSpacing, + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end