diff --git a/.gitignore b/.gitignore index aee909766f..84de6767f4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ docs/.sass-cache *.lock *.gcov +*.gcno +*.gcda diff --git a/AsyncDisplayKit-Prefix.gcda b/AsyncDisplayKit-Prefix.gcda new file mode 100644 index 0000000000..450c9162de Binary files /dev/null and b/AsyncDisplayKit-Prefix.gcda differ diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 7d14f646a4..7da5b80099 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |spec| spec.name = 'AsyncDisplayKit' - spec.version = '1.2.2' + spec.version = '1.9' spec.license = { :type => 'BSD' } spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com', 'Ryan Nystrom' => 'rnystrom@fb.com' } spec.summary = 'Smooth asynchronous user interfaces for iOS apps.' - spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.2.2' } + spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.9' } spec.documentation_url = 'http://asyncdisplaykit.org/appledoc/' diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 1a58fabfe9..12e50d0543 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -140,6 +140,12 @@ 205F0E211B376416007741D0 /* CGRect+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; 205F0E221B376416007741D0 /* CGRect+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */; }; 242995D31B29743C00090100 /* ASBasicImageDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */; }; + 251B8EF71BBB3D690087C538 /* ASCollectionDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */; settings = {ASSET_TAGS = (); }; }; + 251B8EF81BBB3D690087C538 /* ASCollectionDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */; settings = {ASSET_TAGS = (); }; }; + 251B8EF91BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */; settings = {ASSET_TAGS = (); }; }; + 251B8EFA1BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */; settings = {ASSET_TAGS = (); }; }; + 251B8EFB1BBB3D690087C538 /* ASDataController+Subclasses.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */; settings = {ASSET_TAGS = (); }; }; + 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */; settings = {ASSET_TAGS = (); }; }; 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2767E9421BB19BD600EA9B77 /* ASViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */; settings = {ASSET_TAGS = (); }; }; 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2911485B1A77147A005D0878 /* ASControlNodeTests.m */; }; @@ -206,8 +212,14 @@ 509E68651B3AEDC5009B9150 /* CGRect+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; 509E68661B3AEDD7009B9150 /* CGRect+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */; }; 6BDC61F61979037800E50D21 /* AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9B92C8851BC2EB6E00EE46B2 /* ASCollectionDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */; }; + 9B92C8861BC2EB7600EE46B2 /* ASCollectionViewFlowLayoutInspector.m in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */; }; 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C49C3701B853961000B0DD5 /* ASStackLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C5586691BD549CB00B50E3A /* ASAsciiArtBoxCreator.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C55866A1BD549CB00B50E3A /* ASAsciiArtBoxCreator.m in Sources */ = {isa = PBXBuildFile; fileRef = 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.m */; settings = {ASSET_TAGS = (); }; }; + 9C55866B1BD54A1900B50E3A /* ASAsciiArtBoxCreator.m in Sources */ = {isa = PBXBuildFile; fileRef = 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.m */; settings = {ASSET_TAGS = (); }; }; + 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C5FA3511B8F6ADF00A62714 /* ASLayoutOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C5FA3521B8F6ADF00A62714 /* ASLayoutOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C5FA3531B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5FA3501B8F6ADF00A62714 /* ASLayoutOptions.mm */; }; @@ -225,6 +237,15 @@ 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9CDC18CD1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */; }; + AC026B691BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */; }; + AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */; }; + AC026B6F1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */; }; + AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */; }; + AC026B711BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */; }; + AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */; }; + AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */; }; AC21EC101B3D0BF600C8B19A /* ASStackLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3C4A511A1139C100143C57 /* ASCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3C4A521A1139C100143C57 /* ASCollectionView.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A501A1139C100143C57 /* ASCollectionView.mm */; }; @@ -233,6 +254,8 @@ AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */ = {isa = PBXBuildFile; fileRef = AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC47D9461B3BB41900AAEE9D /* ASRelativeSize.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */; }; AC6456091B0A335000CF11B8 /* ASCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = AC6456071B0A335000CF11B8 /* ASCellNode.m */; }; + AC7A2C171BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */; }; + AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */; }; ACC945A91BA9E7A0005E1FB8 /* ASViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; ACC945AB1BA9E7C1005E1FB8 /* ASViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */; }; ACF6ED1A1B17843500DA7C62 /* ASBackgroundLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -544,6 +567,12 @@ 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CGRect+ASConvenience.h"; sourceTree = ""; }; 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CGRect+ASConvenience.m"; sourceTree = ""; }; 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderTests.m; sourceTree = ""; }; + 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionDataController.h; sourceTree = ""; }; + 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionDataController.mm; sourceTree = ""; }; + 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewFlowLayoutInspector.h; sourceTree = ""; }; + 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspector.m; sourceTree = ""; }; + 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDataController+Subclasses.h"; sourceTree = ""; }; + 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspectorTests.m; sourceTree = ""; }; 2911485B1A77147A005D0878 /* ASControlNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASControlNodeTests.m; sourceTree = ""; }; 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutRangeType.h; sourceTree = ""; }; 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandlerPreload.h; sourceTree = ""; }; @@ -567,6 +596,8 @@ 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = ""; }; 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutable.h; path = AsyncDisplayKit/Layout/ASStackLayoutable.h; sourceTree = ""; }; + 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASAsciiArtBoxCreator.h; path = AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.h; sourceTree = ""; }; + 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASAsciiArtBoxCreator.m; path = AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m; sourceTree = ""; }; 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutOptions.h; path = AsyncDisplayKit/Layout/ASLayoutOptions.h; sourceTree = ""; }; 9C5FA3501B8F6ADF00A62714 /* ASLayoutOptions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASLayoutOptions.mm; path = AsyncDisplayKit/Layout/ASLayoutOptions.mm; sourceTree = ""; }; 9C5FA35C1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASLayoutOptionsPrivate.mm; path = AsyncDisplayKit/Layout/ASLayoutOptionsPrivate.mm; sourceTree = ""; }; @@ -576,6 +607,11 @@ 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackBaselinePositionedLayout.mm; sourceTree = ""; }; 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutablePrivate.h; path = AsyncDisplayKit/Layout/ASLayoutablePrivate.h; sourceTree = ""; }; 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewTests.m; sourceTree = ""; }; + AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASChangeSetDataController.h; sourceTree = ""; }; + AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASChangeSetDataController.m; sourceTree = ""; }; + AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASHierarchyChangeSet.h; sourceTree = ""; }; + AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _ASHierarchyChangeSet.m; sourceTree = ""; }; + AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASStaticLayoutSpecSnapshotTests.m; sourceTree = ""; }; AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutDefines.h; path = AsyncDisplayKit/Layout/ASStackLayoutDefines.h; sourceTree = ""; }; AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionView.h; sourceTree = ""; }; AC3C4A501A1139C100143C57 /* ASCollectionView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionView.mm; sourceTree = ""; }; @@ -583,6 +619,7 @@ AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASRelativeSize.h; path = AsyncDisplayKit/Layout/ASRelativeSize.h; sourceTree = ""; }; AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASRelativeSize.mm; path = AsyncDisplayKit/Layout/ASRelativeSize.mm; sourceTree = ""; }; AC6456071B0A335000CF11B8 /* ASCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCellNode.m; sourceTree = ""; }; + AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableViewInternal.h; sourceTree = ""; }; ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASViewController.h; sourceTree = ""; }; ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASViewController.m; sourceTree = ""; }; ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASBackgroundLayoutSpec.h; path = AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.h; sourceTree = ""; }; @@ -769,6 +806,7 @@ D785F6611A74327E00291744 /* ASScrollNode.m */, 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */, 055F1A3319ABD3E3004DAFF1 /* ASTableView.mm */, + AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */, 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */, 058D09DF195D050800B7D73C /* ASTextNode.h */, 058D09E0195D050800B7D73C /* ASTextNode.mm */, @@ -805,6 +843,7 @@ ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */, ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */, ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */, + AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */, ACF6ED571B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.h */, ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m */, 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */, @@ -829,6 +868,7 @@ 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */, 058D09C6195D04C000B7D73C /* Supporting Files */, 052EE06A1A15A0D8002C6279 /* TestResources */, + 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */, ); path = AsyncDisplayKitTests; sourceTree = ""; @@ -847,6 +887,11 @@ children = ( CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */, CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */, + 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */, + 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */, + 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */, + 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */, + 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -861,6 +906,8 @@ 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.mm */, 464052191A3F83C40061C0BA /* ASDataController.h */, 4640521A1A3F83C40061C0BA /* ASDataController.mm */, + AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */, + AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */, 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */, 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */, 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */, @@ -925,6 +972,8 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( + AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, + AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */, 9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */, 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */, 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */, @@ -976,6 +1025,8 @@ AC6456051B0A333200CF11B8 /* Layout */ = { isa = PBXGroup; children = ( + 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */, + 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.m */, ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */, ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */, ACF6ED031B17843500DA7C62 /* ASCenterLayoutSpec.h */, @@ -1044,6 +1095,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + AC026B691BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */, 058D0A71195D05F800B7D73C /* _AS-objc-internal.h in Headers */, 058D0A68195D05EC00B7D73C /* _ASAsyncTransaction.h in Headers */, 058D0A6A195D05EC00B7D73C /* _ASAsyncTransactionContainer+Private.h in Headers */, @@ -1053,6 +1105,7 @@ 058D0A53195D05DC00B7D73C /* _ASDisplayLayer.h in Headers */, 058D0A55195D05DC00B7D73C /* _ASDisplayView.h in Headers */, 058D0A74195D05F800B7D73C /* _ASPendingState.h in Headers */, + 9C5586691BD549CB00B50E3A /* ASAsciiArtBoxCreator.h in Headers */, 058D0A76195D05F900B7D73C /* _ASScopeTimer.h in Headers */, 205F0E191B37339C007741D0 /* ASAbstractLayoutController.h in Headers */, 058D0A82195D060300B7D73C /* ASAssert.h in Headers */, @@ -1062,6 +1115,7 @@ 054963491A1EA066000F8E56 /* ASBasicImageDownloader.h in Headers */, 2967F9E21AB0A5190072E4AB /* ASBasicImageDownloaderInternal.h in Headers */, 299DA1A91A828D2900162D41 /* ASBatchContext.h in Headers */, + 251B8EF91BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h in Headers */, 044285071BAA63FE00D16268 /* ASBatchFetching.h in Headers */, 055F1A3C19ABD43F004DAFF1 /* ASCellNode.h in Headers */, ACF6ED1C1B17843500DA7C62 /* ASCenterLayoutSpec.h in Headers */, @@ -1078,6 +1132,7 @@ 058D0A4C195D05CB00B7D73C /* ASDisplayNode+Subclasses.h in Headers */, 058D0A4A195D05CB00B7D73C /* ASDisplayNode.h in Headers */, 058D0A84195D060300B7D73C /* ASDisplayNodeExtraIvars.h in Headers */, + AC7A2C171BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, 058D0A4D195D05CB00B7D73C /* ASDisplayNodeExtras.h in Headers */, 058D0A7B195D05F900B7D73C /* ASDisplayNodeInternal.h in Headers */, 0587F9BD1A7309ED00AFF0BA /* ASEditableTextNode.h in Headers */, @@ -1091,6 +1146,7 @@ ACF6ED221B17843500DA7C62 /* ASInsetLayoutSpec.h in Headers */, ACF6ED4B1B17847A00DA7C62 /* ASInternalHelpers.h in Headers */, ACF6ED241B17843500DA7C62 /* ASLayout.h in Headers */, + 251B8EFB1BBB3D690087C538 /* ASDataController+Subclasses.h in Headers */, ACF6ED2A1B17843500DA7C62 /* ASLayoutable.h in Headers */, 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */, 464052241A3F83C40061C0BA /* ASLayoutController.h in Headers */, @@ -1099,6 +1155,7 @@ 292C599F1A956527007E5DD6 /* ASLayoutRangeType.h in Headers */, ACF6ED261B17843500DA7C62 /* ASLayoutSpec.h in Headers */, ACF6ED4D1B17847A00DA7C62 /* ASLayoutSpecUtilities.h in Headers */, + AC026B6F1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, 0516FA3D1A15563400B4EBED /* ASLog.h in Headers */, 0442850D1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.h in Headers */, 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */, @@ -1125,6 +1182,7 @@ 9C6BB3B21B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */, ACF6ED311B17843500DA7C62 /* ASStaticLayoutSpec.h in Headers */, 055F1A3419ABD3E3004DAFF1 /* ASTableView.h in Headers */, + 251B8EF71BBB3D690087C538 /* ASCollectionDataController.h in Headers */, 0574D5E219C110940097DC25 /* ASTableViewProtocols.h in Headers */, 058D0A51195D05CB00B7D73C /* ASTextNode.h in Headers */, 058D0A5B195D05DC00B7D73C /* ASTextNodeCoreTextAdditions.h in Headers */, @@ -1147,6 +1205,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */, B35062481B010EFD0018CF92 /* _AS-objc-internal.h in Headers */, B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */, B350623E1B010EFD0018CF92 /* _ASAsyncTransactionContainer+Private.h in Headers */, @@ -1156,6 +1215,7 @@ B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */, B35062111B010EFD0018CF92 /* _ASDisplayView.h in Headers */, B350624B1B010EFD0018CF92 /* _ASPendingState.h in Headers */, + 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */, B350624D1B010EFD0018CF92 /* _ASScopeTimer.h in Headers */, 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */, B35062571B010F070018CF92 /* ASAssert.h in Headers */, @@ -1166,6 +1226,7 @@ B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */, B35062151B010EFD0018CF92 /* ASBatchContext.h in Headers */, 044285081BAA63FE00D16268 /* ASBatchFetching.h in Headers */, + AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */, 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, @@ -1187,6 +1248,7 @@ B350625B1B010F070018CF92 /* ASEqualityHelpers.h in Headers */, B350621B1B010EFD0018CF92 /* ASFlowLayoutController.h in Headers */, B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */, + AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */, B35062021B010EFD0018CF92 /* ASImageNode.h in Headers */, B350621F1B010EFD0018CF92 /* ASImageProtocols.h in Headers */, @@ -1442,8 +1504,10 @@ 058D0A23195D050800B7D73C /* _ASAsyncTransactionContainer.m in Sources */, 058D0A24195D050800B7D73C /* _ASAsyncTransactionGroup.m in Sources */, 058D0A26195D050800B7D73C /* _ASCoreAnimationExtras.mm in Sources */, + AC026B711BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */, 058D0A18195D050800B7D73C /* _ASDisplayLayer.mm in Sources */, 058D0A19195D050800B7D73C /* _ASDisplayView.mm in Sources */, + 9C55866A1BD549CB00B50E3A /* ASAsciiArtBoxCreator.m in Sources */, 058D0A27195D050800B7D73C /* _ASPendingState.m in Sources */, 205F0E1A1B37339C007741D0 /* ASAbstractLayoutController.mm in Sources */, ACF6ED1B1B17843500DA7C62 /* ASBackgroundLayoutSpec.mm in Sources */, @@ -1474,6 +1538,7 @@ ACF6ED251B17843500DA7C62 /* ASLayout.mm in Sources */, 9C5FA3531B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */, 9C5FA35F1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */, + 251B8EFA1BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m in Sources */, ACF6ED271B17843500DA7C62 /* ASLayoutSpec.mm in Sources */, 0516FA411A1563D200B4EBED /* ASMultiplexImageNode.mm in Sources */, 058D0A1B195D050800B7D73C /* ASMutableAttributedStringBuilder.m in Sources */, @@ -1490,10 +1555,12 @@ D785F6631A74327E00291744 /* ASScrollNode.m in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, 9C8221971BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */, + 251B8EF81BBB3D690087C538 /* ASCollectionDataController.mm in Sources */, ACF6ED301B17843500DA7C62 /* ASStackLayoutSpec.mm in Sources */, ACF6ED501B17847A00DA7C62 /* ASStackPositionedLayout.mm in Sources */, ACF6ED521B17847A00DA7C62 /* ASStackUnpositionedLayout.mm in Sources */, ACF6ED321B17843500DA7C62 /* ASStaticLayoutSpec.mm in Sources */, + AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, 058D0A17195D050800B7D73C /* ASTextNode.mm in Sources */, 058D0A1C195D050800B7D73C /* ASTextNodeCoreTextAdditions.m in Sources */, @@ -1505,7 +1572,6 @@ 205F0E221B376416007741D0 /* CGRect+ASConvenience.m in Sources */, 058D0A21195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.m in Sources */, 205F0E101B371875007741D0 /* UICollectionViewLayout+ASConvenience.m in Sources */, - 058D0A25195D050800B7D73C /* UIView+ASConvenience.m in Sources */, CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1522,10 +1588,12 @@ 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, 058D0A38195D057000B7D73C /* ASDisplayLayerTests.m in Sources */, + 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */, 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */, 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */, + AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */, ACF6ED5E1B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm in Sources */, ACF6ED601B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m in Sources */, CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */, @@ -1548,12 +1616,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9B92C8861BC2EB7600EE46B2 /* ASCollectionViewFlowLayoutInspector.m in Sources */, + 9B92C8851BC2EB6E00EE46B2 /* ASCollectionDataController.mm in Sources */, B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.m in Sources */, B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.m in Sources */, + AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */, B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.m in Sources */, B350624A1B010EFD0018CF92 /* _ASCoreAnimationExtras.mm in Sources */, 2767E9421BB19BD600EA9B77 /* ASViewController.m in Sources */, B35062101B010EFD0018CF92 /* _ASDisplayLayer.mm in Sources */, + 9C55866B1BD54A1900B50E3A /* ASAsciiArtBoxCreator.m in Sources */, B35062121B010EFD0018CF92 /* _ASDisplayView.mm in Sources */, B350624C1B010EFD0018CF92 /* _ASPendingState.m in Sources */, 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */, @@ -1604,6 +1676,7 @@ 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, 34EFC7761B701D2A00AD841F /* ASStackPositionedLayout.mm in Sources */, 34EFC7781B701D3100AD841F /* ASStackUnpositionedLayout.mm in Sources */, + AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */, 34EFC7741B701D0A00AD841F /* ASStaticLayoutSpec.mm in Sources */, B350620B1B010EFD0018CF92 /* ASTableView.mm in Sources */, B350620E1B010EFD0018CF92 /* ASTextNode.mm in Sources */, @@ -1616,7 +1689,6 @@ 34566CB31BC1213700715E6B /* ASPhotosFrameworkImageRequest.m in Sources */, B350623B1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.m in Sources */, 044284FD1BAA365100D16268 /* UICollectionViewLayout+ASConvenience.m in Sources */, - B35062441B010EFD0018CF92 /* UIView+ASConvenience.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AsyncDisplayKit/ASCellNode.h b/AsyncDisplayKit/ASCellNode.h index 176d519674..3acef17ce5 100644 --- a/AsyncDisplayKit/ASCellNode.h +++ b/AsyncDisplayKit/ASCellNode.h @@ -44,6 +44,16 @@ NS_ASSUME_NONNULL_BEGIN //@property (atomic, retain) UIColor *backgroundColor; @property (nonatomic) UITableViewCellSelectionStyle selectionStyle; +/* + * A Boolean value that indicates whether the node is selected. + */ +@property (nonatomic, assign) BOOL selected; + +/* + * A Boolean value that indicates whether the node is highlighted. + */ +@property (nonatomic, assign) BOOL highlighted; + /* * ASCellNode must forward touch events in order for UITableView and UICollectionView tap handling to work. Overriding * these methods (e.g. for highlighting) requires the super method be called. diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 80bc34598c..ae6b3f6761 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -123,7 +123,6 @@ static const CGFloat kFontSize = 18.0f; _textNode.attributedString = [[NSAttributedString alloc] initWithString:_text attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kFontSize]}]; - [self invalidateCalculatedLayout]; } @end diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index e546b44838..749b987a13 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -17,6 +17,7 @@ @class ASCellNode; @protocol ASCollectionViewDataSource; @protocol ASCollectionViewDelegate; +@protocol ASCollectionViewLayoutInspecting; NS_ASSUME_NONNULL_BEGIN @@ -64,7 +65,7 @@ NS_ASSUME_NONNULL_BEGIN * * @param asyncDataFetchingEnabled Enable the data fetching in async mode. * - * @discussion If asyncDataFetching is enabled, the `AScollectionView` will fetch data through `collectionView:numberOfRowsInSection:` and + * @discussion If asyncDataFetching is enabled, the `ASCollectionView` will fetch data through `collectionView:numberOfRowsInSection:` and * `collectionView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically * from calling thread. * Enabling asyncDataFetching could avoid blocking main thread for `ASCellNode` allocation, which is frequently reported issue for @@ -81,6 +82,18 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGFloat leadingScreensForBatching; +/** + * Optional introspection object for the collection view's layout. + * + * @discussion Since supplementary and decoration views are controlled by the collection view's layout, this object + * is used as a bridge to provide information to the internal data controller about the existence of these views and + * their associated index paths. For collection views using `UICollectionViewFlowLayout`, a default inspector + * implementation `ASCollectionViewFlowLayoutInspector` is created and set on this property by default. Custom + * collection view layout subclasses will need to provide their own implementation of an inspector object for their + * supplementary views to be compatible with `ASCollectionView`'s supplementary node support. + */ +@property (nonatomic, weak) id layoutInspector; + /** * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. * The asyncDataSource must be updated to reflect the changes before the update block completes. @@ -120,6 +133,26 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData; +/** + * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. + * + * @warning This method is substantially more expensive than UICollectionView's version and will block the main thread + * while all the cells load. + */ +- (void)reloadDataImmediately; + +/** + * Registers the given kind of supplementary node for use in creating node-backed supplementary views. + * + * @param kind The kind of supplementary node that will be requested through the data source. + * + * @discussion Use this method to register support for the use of supplementary nodes in place of the default + * `registerClass:forSupplementaryViewOfKind:withReuseIdentifier:` and `registerNib:forSupplementaryViewOfKind:withReuseIdentifier:` + * methods. This method will register an internal backing view that will host the contents of the supplementary nodes + * returned from the data source. + */ +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind; + /** * Inserts one or more sections. * @@ -192,6 +225,16 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths; +/** + * Relayouts the specified item. + * + * @param indexPath The index path identifying the item to relayout. + * + * @discussion This method must be called from the main thread. The relayout is excuted on main thread. + * The node of the specified item must be updated to cause layout changes before this method is called. + */ +- (void)relayoutItemAtIndexPath:(NSIndexPath *)indexPath; + /** * Moves the item at a specified location to a destination location. * @@ -273,6 +316,15 @@ NS_ASSUME_NONNULL_BEGIN @optional +/** + * Asks the collection view to provide a supplementary node to display in the collection view. + * + * @param collectionView An object representing the collection view requesting this information. + * @param kind The kind of supplementary node to provide. + * @param indexPath The index path that specifies the location of the new supplementary node. + */ +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + /** * Provides the constrained size range for measuring the node at the index path. * @@ -343,18 +395,37 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)shouldBatchFetchForCollectionView:(ASCollectionView *)collectionView; +@end + +/** + * Defines methods that let you coordinate with a `UICollectionViewFlowLayout` in combination with an `ASCollectionView`. + */ +@protocol ASCollectionViewDelegateFlowLayout + +@optional + /** * Passthrough support to UICollectionViewDelegateFlowLayout sectionInset behavior. * * @param collectionView The sender. * @param collectionViewLayout The layout object requesting the information. - * #param section The index number of the section whose insets are needed. + * @param section The index number of the section whose insets are needed. * * @discussion The same rules apply as the UICollectionView implementation, but this can also be used without a UICollectionViewFlowLayout. * https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionViewDelegateFlowLayout_protocol/index.html#//apple_ref/occ/intfm/UICollectionViewDelegateFlowLayout/collectionView:layout:insetForSectionAtIndex: * */ -- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; +- (UIEdgeInsets)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; + +/** + * Asks the delegate for the size of the header in the specified section. + */ +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; + +/** + * Asks the delegate for the size of the footer in the specified section. + */ +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section; @end diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index b1b29335b3..c123914ab7 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -11,11 +11,12 @@ #import "ASAssert.h" #import "ASCollectionViewLayoutController.h" #import "ASRangeController.h" -#import "ASDataController.h" +#import "ASCollectionDataController.h" #import "ASDisplayNodeInternal.h" #import "ASBatchFetching.h" #import "UICollectionViewLayout+ASConvenience.h" #import "ASInternalHelpers.h" +#import "ASCollectionViewFlowLayoutInspector.h" // FIXME: Temporary nonsense import until method names are finalized and exposed #import "ASDisplayNode+Subclasses.h" @@ -37,9 +38,7 @@ static BOOL _isInterceptedSelector(SEL sel) // handled by ASCollectionView node<->cell machinery sel == @selector(collectionView:cellForItemAtIndexPath:) || sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || - - // TODO: Supplementary views are currently not supported. An assertion is triggered if the _asyncDataSource implements this method. - // sel == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || + sel == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || // handled by ASRangeController sel == @selector(numberOfSectionsInCollectionView:) || @@ -106,6 +105,28 @@ static BOOL _isInterceptedSelector(SEL sel) @end +#pragma mark - +#pragma mark ASCellNode<->UICollectionViewCell bridging. + +@class _ASCollectionViewCell; + +@interface _ASCollectionViewCell : UICollectionViewCell +@property (nonatomic, weak) ASCellNode *node; +@end + +@implementation _ASCollectionViewCell + +- (void)setSelected:(BOOL)selected +{ + _node.selected = selected; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + _node.highlighted = highlighted; +} + +@end #pragma mark - #pragma mark ASCollectionView. @@ -114,9 +135,10 @@ static BOOL _isInterceptedSelector(SEL sel) _ASCollectionViewProxy *_proxyDataSource; _ASCollectionViewProxy *_proxyDelegate; - ASDataController *_dataController; + ASCollectionDataController *_dataController; ASRangeController *_rangeController; ASCollectionViewLayoutController *_layoutController; + ASCollectionViewFlowLayoutInspector *_flowLayoutInspector; BOOL _performingBatchUpdates; NSMutableArray *_batchUpdateBlocks; @@ -131,6 +153,8 @@ static BOOL _isInterceptedSelector(SEL sel) CGSize _maxSizeForNodesConstrainedSize; BOOL _ignoreMaxSizeChange; + NSMutableSet *_registeredSupplementaryKinds; + /** * If YES, the `UICollectionView` will reload its data on next layout pass so we should not forward any updates to it. @@ -179,10 +203,10 @@ static BOOL _isInterceptedSelector(SEL sel) _rangeController.delegate = self; _rangeController.layoutController = _layoutController; - _dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; + _dataController = [[ASCollectionDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; _dataController.delegate = _rangeController; _dataController.dataSource = self; - + _batchContext = [[ASBatchContext alloc] init]; _leadingScreensForBatching = 1.0; @@ -202,9 +226,17 @@ static BOOL _isInterceptedSelector(SEL sel) // and should not trigger a relayout. _ignoreMaxSizeChange = CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, CGSizeZero); + // Register the default layout inspector delegate for flow layouts only, custom layouts + // will need to roll their own ASCollectionViewLayoutInspecting implementation and set a layout delegate + if ([layout asdk_isFlowLayout]) { + _layoutInspector = [self flowLayoutInspector]; + } + + _registeredSupplementaryKinds = [NSMutableSet set]; + self.backgroundColor = [UIColor whiteColor]; - [self registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"_ASCollectionViewCell"]; + [self registerClass:[_ASCollectionViewCell class] forCellWithReuseIdentifier:@"_ASCollectionViewCell"]; return self; } @@ -217,6 +249,20 @@ static BOOL _isInterceptedSelector(SEL sel) super.dataSource = nil; } +/** + * A layout inspector implementation specific for the sizing behavior of UICollectionViewFlowLayouts + */ +- (ASCollectionViewFlowLayoutInspector *)flowLayoutInspector +{ + if (_flowLayoutInspector == nil) { + UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout; + ASDisplayNodeAssertNotNil(layout, @"Collection view layout must be a flow layout to use the built-in inspector"); + _flowLayoutInspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:self + flowLayout:layout]; + } + return _flowLayoutInspector; +} + #pragma mark - #pragma mark Overrides. @@ -235,6 +281,13 @@ static BOOL _isInterceptedSelector(SEL sel) [self reloadDataWithCompletion:nil]; } +- (void)reloadDataImmediately +{ + ASDisplayNodeAssertMainThread(); + [_dataController reloadDataImmediatelyWithAnimationOptions:kASCollectionViewAnimationNone]; + [super reloadData]; +} + - (void)setDataSource:(id)dataSource { // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. @@ -261,10 +314,6 @@ static BOOL _isInterceptedSelector(SEL sel) _asyncDataSourceImplementsConstrainedSizeForNode = NO; } else { _asyncDataSource = asyncDataSource; - // TODO: Support supplementary views with ASCollectionView. - if ([_asyncDataSource respondsToSelector:@selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:)]) { - ASDisplayNodeAssert(NO, @"ASCollectionView is planned to support supplementary views by September 2015. You can work around this issue by using standard items."); - } _proxyDataSource = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; super.dataSource = (id)_proxyDataSource; _asyncDataSourceImplementsConstrainedSizeForNode = ([_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)] ? 1 : 0); @@ -291,6 +340,8 @@ static BOOL _isInterceptedSelector(SEL sel) super.delegate = (id)_proxyDelegate; _asyncDelegateImplementsInsetSection = ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)] ? 1 : 0); } + + [_layoutInspector didChangeCollectionViewDelegate:asyncDelegate]; } - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType @@ -357,6 +408,14 @@ static BOOL _isInterceptedSelector(SEL sel) [self performBatchAnimated:YES updates:updates completion:completion]; } +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind +{ + ASDisplayNodeAssert(elementKind != nil, @"A kind is needed for supplementary node registration"); + [_registeredSupplementaryKinds addObject:elementKind]; + [self registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:elementKind + withReuseIdentifier:[self __reuseIdentifierForKind:elementKind]]; +} + - (void)insertSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); @@ -399,12 +458,25 @@ static BOOL _isInterceptedSelector(SEL sel) [_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone]; } +- (void)relayoutItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; + [node setNeedsLayout]; + [super reloadItemsAtIndexPaths:@[indexPath]]; +} + - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone]; } +- (NSString *)__reuseIdentifierForKind:(NSString *)kind +{ + return [@"_ASCollectionSupplementaryView_" stringByAppendingString:kind]; +} + #pragma mark - #pragma mark Intercepted selectors. @@ -412,12 +484,14 @@ static BOOL _isInterceptedSelector(SEL sel) { static NSString *reuseIdentifier = @"_ASCollectionViewCell"; - UICollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; + _ASCollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; [_rangeController configureContentView:cell.contentView forCellNode:node]; + cell.node = node; + return cell; } @@ -426,6 +500,15 @@ static BOOL _isInterceptedSelector(SEL sel) return [[_dataController nodeAtIndexPath:indexPath] calculatedSize]; } +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + NSString *identifier = [self __reuseIdentifierForKind:kind]; + UICollectionReusableView *view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:identifier forIndexPath:indexPath]; + ASCellNode *node = [_dataController supplementaryNodeOfKind:kind atIndexPath:indexPath]; + [_rangeController configureContentView:view forCellNode:node]; + return view; +} + - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { _superIsPendingDataLoad = NO; @@ -525,7 +608,7 @@ static BOOL _isInterceptedSelector(SEL sel) _ignoreMaxSizeChange = NO; } else { [self performBatchAnimated:NO updates:^{ - [_dataController relayoutAllRows]; + [_dataController relayoutAllNodes]; } completion:nil]; } } @@ -605,7 +688,7 @@ static BOOL _isInterceptedSelector(SEL sel) } if (_asyncDelegateImplementsInsetSection) { - sectionInset = [_asyncDelegate collectionView:self layout:self.collectionViewLayout insetForSectionAtIndex:indexPath.section]; + sectionInset = [(id)_asyncDelegate collectionView:self layout:self.collectionViewLayout insetForSectionAtIndex:indexPath.section]; } if (ASScrollDirectionContainsHorizontalDirection([self scrollableDirections])) { @@ -630,7 +713,7 @@ static BOOL _isInterceptedSelector(SEL sel) return [_asyncDataSource collectionView:self numberOfItemsInSection:section]; } -- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController { +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { return [_asyncDataSource numberOfSectionsInCollectionView:self]; } else { @@ -658,8 +741,39 @@ static BOOL _isInterceptedSelector(SEL sel) } } -#pragma mark - -#pragma mark ASRangeControllerDelegate. +#pragma mark - ASCollectionViewDataControllerSource Supplementary view support + +- (ASCellNode *)dataController:(ASCollectionDataController *)dataController supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASCellNode *node = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + ASDisplayNodeAssert(node != nil, @"A node must be returned for a supplementary node"); + return node; +} + +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController +{ + return [_registeredSupplementaryKinds allObjects]; +} + +- (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssert(_layoutInspector != nil, @"To support supplementary nodes in ASCollectionView, it must have a layoutDelegate for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return [_layoutInspector collectionView:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; +} + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + ASDisplayNodeAssert(_layoutInspector != nil, @"To support supplementary nodes in ASCollectionView, it must have a layoutDelegate for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return [_layoutInspector collectionView:self supplementaryNodesOfKind:kind inSection:section]; +} + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind; +{ + ASDisplayNodeAssert(_layoutInspector != nil, @"To support supplementary nodes in ASCollectionView, it must have a layoutDelegate for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return [_layoutInspector collectionView:self numberOfSectionsForSupplementaryNodeOfKind:kind]; +} + +#pragma mark - ASRangeControllerDelegate. - (void)rangeControllerBeginUpdates:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 7412a4972e..12acd99652 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -12,7 +12,7 @@ #import #import #import - +#import #import NS_ASSUME_NONNULL_BEGIN @@ -110,6 +110,10 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); /** @name Properties */ +/** + * @abstract The name of this node, which will be displayed in `description`. The default value is nil. + */ +@property (atomic, copy) NSString *name; /** * @abstract Returns whether the node is synchronous. @@ -521,7 +525,7 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); * Convenience methods for debugging. */ -@interface ASDisplayNode (Debugging) +@interface ASDisplayNode (Debugging) /** * @abstract Return a description of the node hierarchy. @@ -541,7 +545,7 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); * Using them will not cause the actual view/layer to be created, and will be applied when it is created (when the view * or layer property is accessed). * - * After the view is created, the properties pass through to the view directly as if called on the main thread. + * - NOTE: After the view or layer is created, the properties pass through to the view or layer directly and must be called on the main thread. * * See UIView and CALayer for documentation on these common properties. */ @@ -558,9 +562,13 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); * If this node was measured, calling this method triggers an internal relayout: the calculated layout is invalidated, * and the supernode is notified or (if this node is the root one) a full measurement pass is executed using the old constrained size. * - * Note: If the relayout causes a change in size of the root node that is attached to a container view - * (table or collection view, for example), the container view must be notified to relayout. - * For ASTableView and ASCollectionView, an empty batch editing transaction must be triggered to animate to new row / item sizes. + * Note: If the relayout causes a change in size of the root node that is attached to a container view, + * the container view must be notified to relayout. + * For ASTableView and ASCollectionView, instead of calling this method directly, + * it is recommended to call -relayoutRowAtIndexPath:withRowAnimation and -relayoutItemAtIndexPath: respectively. + * + * @see [ASTableView relayoutRowAtIndexPath:withRowAnimation:] + * @see [ASCollectionView relayoutItemAtIndexPath:] */ - (void)setNeedsLayout; @@ -585,7 +593,6 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); @property (atomic, assign) CGFloat contentsScale; // default=1.0f. See @contentsScaleForDisplay for more info @property (atomic, assign) CATransform3D transform; // default=CATransform3DIdentity @property (atomic, assign) CATransform3D subnodeTransform; // default=CATransform3DIdentity -@property (atomic, copy, nullable) NSString *name; // default=nil. Use this to tag your layers in the server-recurse-description / pca or for your own purposes /** * @abstract The node view's background color. @@ -649,6 +656,7 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); * @param node The node to be added. */ - (void)addSubnode:(ASDisplayNode *)node; +- (NSString *)name; @end /** CALayer(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to a CALayer. */ @@ -659,6 +667,7 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); * @param node The node to be added. */ - (void)addSubnode:(ASDisplayNode *)node; +- (NSString *)name; @end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index c5beb9347b..c299cb0d9c 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -19,6 +19,7 @@ #import "_ASDisplayView.h" #import "_ASScopeTimer.h" #import "ASDisplayNodeExtras.h" +#import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" #import "ASLayout.h" @@ -47,6 +48,7 @@ // these dynamic properties all defined in ASLayoutOptionsPrivate.m @dynamic spacingAfter, spacingBefore, flexGrow, flexShrink, flexBasis, alignSelf, ascender, descender, sizeRange, layoutPosition, layoutOptions; +@synthesize name = _name; @synthesize preferredFrameSize = _preferredFrameSize; @synthesize isFinalLayoutable = _isFinalLayoutable; @@ -458,9 +460,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _layer = _view.layer; } _layer.asyncdisplaykit_node = self; -#if DEBUG - _layer.name = self.description; -#endif + self.asyncLayer.asyncDelegate = self; { @@ -526,6 +526,20 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return (_view != nil || (_flags.layerBacked && _layer != nil)); } +- (NSString *)name +{ + ASDN::MutexLocker l(_propertyLock); + return _name; +} + +- (void)setName:(NSString *)name +{ + ASDN::MutexLocker l(_propertyLock); + if (!ASObjectIsEqual(_name, name)) { + _name = [name copy]; + } +} + - (BOOL)isSynchronous { return _flags.synchronous; @@ -2137,6 +2151,18 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, return subtree; } +#pragma mark - ASLayoutableAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + return NSStringFromClass([self class]); +} + @end // We use associated objects as a last resort if our view is not a _ASDisplayView ie it doesn't have the _node ivar to write to @@ -2175,6 +2201,11 @@ static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; } } +- (NSString *)name +{ + return self.asyncdisplaykit_node.name; +} + @end @implementation CALayer (AsyncDisplayKit) @@ -2184,6 +2215,11 @@ static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; [self addSublayer:node.layer]; } +- (NSString *)name +{ + return self.asyncdisplaykit_node.name; +} + @end diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index 31e2276351..b6c6e97b6f 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -33,6 +33,20 @@ typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { * Indicates that the best image identifier changed before a download for a worse identifier began. */ ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged, + + /** + * Indicates that the Photos framework returned no image and no error. + * This may happen if the image is in iCloud and the user did not specify `allowsNetworkAccess` + * in their image request. + */ + ASMultiplexImageNodeErrorCodePhotosImageManagerFailedWithoutError, + + /** + * Indicates that the image node could not retrieve the PHAsset for a given asset identifier. + * This typically means that the user has not given Photos framework permissions yet or the asset + * has been removed from the device. + */ + ASMultiplexImageNodeErrorCodePHAssetIsUnavailable }; diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 648c9bb7a0..c5c3edd793 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -577,8 +577,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent } if (imageAsset == nil) { - // Error. - completionBlock(nil, nil); + NSError *error = [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodePHAssetIsUnavailable userInfo:nil]; + completionBlock(nil, error); return; } @@ -597,15 +597,25 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent PHImageManager *imageManager = strongSelf.imageManager ?: PHImageManager.defaultManager; [imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) { + NSError *error = info[PHImageErrorKey]; + + if (error == nil && image == nil) { + error = [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodePhotosImageManagerFailedWithoutError userInfo:nil]; + } + if (NSThread.isMainThread) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ - completionBlock(image, info[PHImageErrorKey]); + completionBlock(image, error); }); } else { - completionBlock(image, info[PHImageErrorKey]); + completionBlock(image, error); } }]; }]; + if (AS_AT_LEAST_IOS8) { + // If you don't set this, iOS will sometimes infer NSQualityOfServiceUserInteractive and promote the entire queue to that level, damaging system responsiveness + newImageRequestOp.qualityOfService = NSQualityOfServiceUserInitiated; + } _phImageRequestOperation = newImageRequestOp; [phImageRequestQueue addOperation:newImageRequestOp]; } @@ -673,7 +683,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent { // If we failed to load, we stop the loading process. // Note that if we bailed before we began downloading because the best identifier changed, we don't bail, but rather just begin loading the best image identifier. - if (error && error.code != ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged) + if (error && !([error.domain isEqual:ASMultiplexImageNodeErrorDomain] && error.code == ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged)) return; OSSpinLockLock(&_imageIdentifiersLock); diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index e67a49634e..aa28d40a59 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -94,6 +94,14 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadData; +/** + * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. + * + * @warning This method is substantially more expensive than UITableView's version and will block the main thread while + * all the cells load. + */ +- (void)reloadDataImmediately; + /** * begins a batch of insert, delete reload and move operations. This method must be called from the main thread. */ @@ -207,6 +215,18 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; +/** + * Relayouts the specified row using a given animation effect. + * + * @param indexPath The index path identifying the row to relayout. + * + * @param animation A constant that indicates how the relayout is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The relayout is excuted on main thread. + * The node of the specified row must be updated to cause layout changes before this method is called. + */ +- (void)relayoutRowAtIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)animation; + /** * Moves the row at a specified location to a destination location. * diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 7c36c775a6..6729242e9c 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -7,9 +7,10 @@ */ #import "ASTableView.h" +#import "ASTableViewInternal.h" #import "ASAssert.h" -#import "ASDataController.h" +#import "ASChangeSetDataController.h" #import "ASCollectionViewLayoutController.h" #import "ASLayoutController.h" #import "ASRangeController.h" @@ -135,6 +136,16 @@ static BOOL _isInterceptedSelector(SEL sel) [super didTransitionToState:state]; } +- (void)setSelected:(BOOL)selected +{ + _node.selected = selected; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + _node.highlighted = highlighted; +} + @end @@ -145,7 +156,6 @@ static BOOL _isInterceptedSelector(SEL sel) _ASTableViewProxy *_proxyDataSource; _ASTableViewProxy *_proxyDelegate; - ASDataController *_dataController; ASFlowLayoutController *_layoutController; ASRangeController *_rangeController; @@ -159,11 +169,12 @@ static BOOL _isInterceptedSelector(SEL sel) NSIndexPath *_contentOffsetAdjustmentTopVisibleRow; CGFloat _contentOffsetAdjustment; - CGFloat _maxWidthForNodesConstrainedSize; - BOOL _ignoreMaxWidthChange; + CGFloat _nodesConstrainedWidth; + BOOL _ignoreNodesConstrainedWidthChange; } @property (atomic, assign) BOOL asyncDataSourceLocked; +@property (nonatomic, retain, readwrite) ASDataController *dataController; @end @@ -189,24 +200,29 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { } } ++ (Class)dataControllerClass +{ + return [ASChangeSetDataController class]; +} + #pragma mark - #pragma mark Lifecycle -- (void)configureWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled +- (void)configureWithDataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetching { _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; _rangeController = [[ASRangeController alloc] init]; _rangeController.layoutController = _layoutController; _rangeController.delegate = self; - - _dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; + + _dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:asyncDataFetching]; _dataController.dataSource = self; _dataController.delegate = _rangeController; _layoutController.dataSource = _dataController; - _asyncDataFetchingEnabled = asyncDataFetchingEnabled; + _asyncDataFetchingEnabled = asyncDataFetching; _asyncDataSourceLocked = NO; _leadingScreensForBatching = 1.0; @@ -214,10 +230,10 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { _automaticallyAdjustsContentOffset = NO; - _maxWidthForNodesConstrainedSize = self.bounds.size.width; + _nodesConstrainedWidth = self.bounds.size.width; // If the initial size is 0, expect a size change very soon which is part of the initial configuration // and should not trigger a relayout. - _ignoreMaxWidthChange = (_maxWidthForNodesConstrainedSize == 0); + _ignoreNodesConstrainedWidthChange = (_nodesConstrainedWidth == 0); } - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style @@ -226,6 +242,11 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { } - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled +{ + return [self initWithFrame:frame style:style dataControllerClass:[self.class dataControllerClass] asyncDataFetching:asyncDataFetchingEnabled]; +} + +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetchingEnabled { if (!(self = [super initWithFrame:frame style:style])) return nil; @@ -234,8 +255,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { // https://github.com/facebook/AsyncDisplayKit/issues/385 asyncDataFetchingEnabled = NO; - [self configureWithAsyncDataFetching:asyncDataFetchingEnabled]; - + [self configureWithDataControllerClass:dataControllerClass asyncDataFetching:asyncDataFetchingEnabled]; + return self; } @@ -244,7 +265,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { if (!(self = [super initWithCoder:aDecoder])) return nil; - [self configureWithAsyncDataFetching:NO]; + [self configureWithDataControllerClass:[self.class dataControllerClass] asyncDataFetching:NO]; return self; } @@ -324,6 +345,13 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { [self reloadDataWithCompletion:nil]; } +- (void)reloadDataImmediately +{ + ASDisplayNodeAssertMainThread(); + [_dataController reloadDataImmediatelyWithAnimationOptions:UITableViewRowAnimationNone]; + [super reloadData]; +} + - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType { [_layoutController setTuningParameters:tuningParameters forRangeType:rangeType]; @@ -386,21 +414,21 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (void)layoutSubviews { - if (_maxWidthForNodesConstrainedSize != self.bounds.size.width) { - _maxWidthForNodesConstrainedSize = self.bounds.size.width; + if (_nodesConstrainedWidth != self.bounds.size.width) { + _nodesConstrainedWidth = self.bounds.size.width; // First width change occurs during initial configuration. An expensive relayout pass is unnecessary at that time // and should be avoided, assuming that the initial data loading automatically runs shortly afterward. - if (_ignoreMaxWidthChange) { - _ignoreMaxWidthChange = NO; + if (_ignoreNodesConstrainedWidthChange) { + _ignoreNodesConstrainedWidthChange = NO; } else { [self beginUpdates]; - [_dataController relayoutAllRows]; + [_dataController relayoutAllNodes]; [self endUpdates]; } } - // To ensure _maxWidthForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last + // To ensure _nodesConstrainedWidth is up-to-date for every usage, this call to super must be done last [super layoutSubviews]; } @@ -449,6 +477,14 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { [_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animation]; } +- (void)relayoutRowAtIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)animation +{ + ASDisplayNodeAssertMainThread(); + ASCellNode *node = [self nodeForRowAtIndexPath:indexPath]; + [node setNeedsLayout]; + [super reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:animation]; +} + - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); @@ -818,8 +854,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { - return ASSizeRangeMake(CGSizeMake(_maxWidthForNodesConstrainedSize, 0), - CGSizeMake(_maxWidthForNodesConstrainedSize, FLT_MAX)); + return ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, 0), + CGSizeMake(_nodesConstrainedWidth, FLT_MAX)); } - (void)dataControllerLockDataSource @@ -849,7 +885,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { return [_asyncDataSource tableView:self numberOfRowsInSection:section]; } -- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { return [_asyncDataSource numberOfSectionsInTableView:self]; diff --git a/AsyncDisplayKit/ASTableViewInternal.h b/AsyncDisplayKit/ASTableViewInternal.h new file mode 100644 index 0000000000..af940837e2 --- /dev/null +++ b/AsyncDisplayKit/ASTableViewInternal.h @@ -0,0 +1,31 @@ +// +// ASTableViewInternal.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 26/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "ASTableView.h" + +@class ASDataController; + +@interface ASTableView (Internal) + +@property (nonatomic, retain, readonly) ASDataController *dataController; + +/** + * Initializer. + * + * @param frame A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. + * The frame of the table view changes as table cells are added and deleted. + * + * @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants. + * + * @param dataControllerClass A controller class injected to and used to create a data controller for the table view. + * + * @param asyncDataFetchingEnabled This option is reserved for future use, and currently a no-op. + */ +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetchingEnabled; + +@end diff --git a/AsyncDisplayKit/ASViewController.m b/AsyncDisplayKit/ASViewController.m index ef89b905a6..15fe664323 100644 --- a/AsyncDisplayKit/ASViewController.m +++ b/AsyncDisplayKit/ASViewController.m @@ -20,7 +20,7 @@ - (instancetype)initWithNode:(ASDisplayNode *)node { - if (!(self = [super init])) { + if (!(self = [super initWithNibName:nil bundle:nil])) { return nil; } diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.h b/AsyncDisplayKit/Details/ASChangeSetDataController.h new file mode 100644 index 0000000000..cc385a148f --- /dev/null +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.h @@ -0,0 +1,23 @@ +// +// ASChangeSetDataController.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 19/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +/** + * @abstract Subclass of ASDataController that simulates ordering of operations in batch updates defined in UITableView and UICollectionView. + * + * @discussion The ordering is achieved by using _ASHierarchyChangeSet to enqueue and sort operations. + * More information about the ordering and the index paths used for operations can be found here: + * https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/ManageInsertDeleteRow/ManageInsertDeleteRow.html#//apple_ref/doc/uid/TP40007451-CH10-SW17 + * + * @see ASDataController + * @see _ASHierarchyChangeSet + */ +@interface ASChangeSetDataController : ASDataController + +@end diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m new file mode 100644 index 0000000000..3e2c2ce118 --- /dev/null +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -0,0 +1,179 @@ +// +// ASChangeSetDataController.m +// AsyncDisplayKit +// +// Created by Huy Nguyen on 19/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "ASChangeSetDataController.h" +#import "ASInternalHelpers.h" +#import "_ASHierarchyChangeSet.h" +#import "ASAssert.h" + +@interface ASChangeSetDataController () + +@property (nonatomic, assign) NSUInteger batchUpdateCounter; +@property (nonatomic, strong) _ASHierarchyChangeSet *changeSet; + +@end + +@implementation ASChangeSetDataController + +- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled +{ + if (!(self = [super initWithAsyncDataFetching:asyncDataFetchingEnabled])) { + return nil; + } + + _batchUpdateCounter = 0; + + return self; +} + +#pragma mark - Batching (External API) + +- (void)beginUpdates +{ + ASDisplayNodeAssertMainThread(); + if (_batchUpdateCounter == 0) { + _changeSet = [_ASHierarchyChangeSet new]; + } + _batchUpdateCounter++; +} + +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + _batchUpdateCounter--; + + if (_batchUpdateCounter == 0) { + [_changeSet markCompleted]; + + [super beginUpdates]; + + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { + [super reloadSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { + [super reloadRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { + [super deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { + [super deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { + [super insertSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { + [super insertRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + + [super endUpdatesAnimated:animated completion:completion]; + + _changeSet = nil; + } +} + +- (BOOL)batchUpdating +{ + BOOL batchUpdating = (_batchUpdateCounter != 0); + // _changeSet must be available during batch update + ASDisplayNodeAssertTrue(batchUpdating == (_changeSet != nil)); + return batchUpdating; +} + +#pragma mark - Section Editing (External API) + +- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet insertSections:sections animationOptions:animationOptions]; + } else { + [super insertSections:sections withAnimationOptions:animationOptions]; + } +} + +- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteSections:sections animationOptions:animationOptions]; + } else { + [super deleteSections:sections withAnimationOptions:animationOptions]; + } +} + +- (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet reloadSections:sections animationOptions:animationOptions]; + } else { + [super reloadSections:sections withAnimationOptions:animationOptions]; + } +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; + [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; + } else { + [super moveSection:section toSection:newSection withAnimationOptions:animationOptions]; + } +} + +#pragma mark - Row Editing (External API) + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet insertItems:indexPaths animationOptions:animationOptions]; + } else { + [super insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + } +} + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteItems:indexPaths animationOptions:animationOptions]; + } else { + [super deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + } +} + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet reloadItems:indexPaths animationOptions:animationOptions]; + } else { + [super reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + } +} + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; + [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; + } else { + [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:animationOptions]; + } +} + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.h b/AsyncDisplayKit/Details/ASCollectionDataController.h new file mode 100644 index 0000000000..7c66ff41fb --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionDataController.h @@ -0,0 +1,39 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +@class ASDisplayNode; +@class ASCollectionDataController; +@protocol ASDataControllerSource; + +@protocol ASCollectionDataControllerSource + +- (ASCellNode *)dataController:(ASCollectionDataController *)dataController supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + The constrained size range for layout. + */ +- (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController; + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind; + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; + +@end + +@interface ASCollectionDataController : ASChangeSetDataController + +- (ASCellNode *)supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +@end \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm new file mode 100644 index 0000000000..1e3f7029d4 --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -0,0 +1,217 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASCollectionDataController.h" + +#import "ASAssert.h" +#import "ASMultidimensionalArrayUtils.h" +#import "ASDisplayNode.h" +#import "ASDisplayNodeInternal.h" +#import "ASDataController+Subclasses.h" + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +@interface ASCollectionDataController () + +- (id)collectionDataSource; + +@end + +@implementation ASCollectionDataController { + NSMutableDictionary *_pendingNodes; + NSMutableDictionary *_pendingIndexPaths; +} + +- (void)prepareForReloadData +{ + _pendingNodes = [NSMutableDictionary dictionary]; + _pendingIndexPaths = [NSMutableDictionary dictionary]; + + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + LOG(@"Populating elements of kind: %@", kind); + NSMutableArray *indexPaths = [NSMutableArray array]; + NSMutableArray *nodes = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withMutableNodes:nodes mutableIndexPaths:indexPaths]; + _pendingNodes[kind] = nodes; + _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + }]; +} + +- (void)willReloadData +{ + [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + // Remove everything that existed before the reload, now that we're ready to insert replacements + NSArray *indexPaths = [self indexPathsForEditingNodesOfKind:kind]; + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + + NSArray *editingNodes = [self editingNodesOfKind:kind]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; + [self deleteSectionsOfKind:kind atIndexSet:indexSet completion:nil]; + + // Insert each section + NSUInteger sectionCount = [self.collectionDataSource dataController:self numberOfSectionsForSupplementaryNodeOfKind:kind]; + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; + for (int i = 0; i < sectionCount; i++) { + [sections addObject:[NSMutableArray array]]; + } + [self insertSections:sections ofKind:kind atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] completion:nil]; + + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; + [_pendingNodes removeObjectForKey:kind]; + [_pendingIndexPaths removeObjectForKey:kind]; + }]; +} + +- (void)prepareForInsertSections:(NSIndexSet *)sections +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + LOG(@"Populating elements of kind: %@, for sections: %@", kind, sections); + NSMutableArray *nodes = [NSMutableArray array]; + NSMutableArray *indexPaths = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; + _pendingNodes[kind] = nodes; + _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + }]; +} + +- (void)willInsertSections:(NSIndexSet *)sections +{ + [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { + [sectionArray addObject:[NSMutableArray array]]; + } + + [self insertSections:sectionArray ofKind:kind atIndexSet:sections completion:nil]; + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:nil]; + _pendingNodes[kind] = nil; + _pendingIndexPaths[kind] = nil; + }]; +} + +- (void)willDeleteSections:(NSIndexSet *)sections +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); + + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + [self deleteSectionsOfKind:kind atIndexSet:sections completion:nil]; + }]; +} + +- (void)prepareForReloadSections:(NSIndexSet *)sections +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + NSMutableArray *nodes = [NSMutableArray array]; + NSMutableArray *indexPaths = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; + _pendingNodes[kind] = nodes; + _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + }]; +} + +- (void)willReloadSections:(NSIndexSet *)sections +{ + [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + // reinsert the elements + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:nil]; + _pendingNodes[kind] = nil; + _pendingIndexPaths[kind] = nil; + }]; +} + +- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths([self editingNodesOfKind:kind], indexPaths); + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + + // update the section of indexpaths + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; + NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; + }]; + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; +} + +- (void)_populateSupplementaryNodesOfKind:(NSString *)kind withMutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths +{ + NSUInteger sectionCount = [self.collectionDataSource dataController:self numberOfSectionsForSupplementaryNodeOfKind:kind]; + for (NSUInteger i = 0; i < sectionCount; i++) { + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; + NSUInteger rowCount = [self.collectionDataSource dataController:self supplementaryNodesOfKind:kind inSection:i]; + for (NSUInteger j = 0; j < rowCount; j++) { + NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j]; + [indexPaths addObject:indexPath]; + [nodes addObject:[self.collectionDataSource dataController:self supplementaryNodeOfKind:kind atIndexPath:indexPath]]; + } + } +} + +- (void)_populateSupplementaryNodesOfKind:(NSString *)kind withSections:(NSIndexSet *)sections mutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths +{ + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSUInteger rowNum = [self.collectionDataSource dataController:self supplementaryNodesOfKind:kind inSection:idx]; + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = 0; i < rowNum; i++) { + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [indexPaths addObject:indexPath]; + [nodes addObject:[self.collectionDataSource dataController:self supplementaryNodeOfKind:kind atIndexPath:indexPath]]; + } + }]; +} + +#pragma mark - Sizing query + +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if ([kind isEqualToString:ASDataControllerRowNodeKind]) { + return [super constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + } else { + return [self.collectionDataSource dataController:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; + } +} + +#pragma mark - External supplementary store querying + +- (ASCellNode *)supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + return [self completedNodesOfKind:kind][indexPath.section][indexPath.item]; +} + +#pragma mark - Private Helpers + +- (NSArray *)supplementaryKinds +{ + return [self.collectionDataSource supplementaryNodeKindsInDataController:self]; +} + +- (id)collectionDataSource +{ + return (id)self.dataSource; +} + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.h b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.h new file mode 100644 index 0000000000..179b880827 --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.h @@ -0,0 +1,48 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@class ASCollectionView; +@protocol ASCollectionViewDelegate; + +@protocol ASCollectionViewLayoutInspecting + +/** + * Asks the inspector to provide a constrained size range for the given supplementary node. + */ +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Asks the inspector for the number of supplementary sections in the collection view for the given kind. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind; + +/** + * Asks the inspector for the number of supplementary views for the given kind in the specified section. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; + +/** + * Allow the inspector to respond to delegate changes. + * + * @discussion A great time to update perform selector caches! + */ +- (void)didChangeCollectionViewDelegate:(id)delegate; + +@end + +@interface ASCollectionViewFlowLayoutInspector : NSObject + +@property (nonatomic, weak) UICollectionViewFlowLayout *layout; + +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView flowLayout:(UICollectionViewFlowLayout *)flowLayout; + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m new file mode 100644 index 0000000000..5ce3b9bfb6 --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m @@ -0,0 +1,123 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ASCollectionViewFlowLayoutInspector.h" + +#import "ASCollectionView.h" +#import "ASAssert.h" +#import "ASEqualityHelpers.h" + +@implementation ASCollectionViewFlowLayoutInspector { + BOOL _delegateImplementsReferenceSizeForHeader; + BOOL _delegateImplementsReferenceSizeForFooter; +} + +#pragma mark - Accessors + +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView flowLayout:(UICollectionViewFlowLayout *)flowLayout; +{ + self = [super init]; + + if (flowLayout == nil) { + ASDisplayNodeAssert(NO, @"Should never create a layout inspector without a layout"); + } + + if (self != nil) { + [self didChangeCollectionViewDelegate:collectionView.asyncDelegate]; + _layout = flowLayout; + } + return self; +} + +- (void)didChangeCollectionViewDelegate:(id)delegate; +{ + if (delegate == nil) { + _delegateImplementsReferenceSizeForHeader = NO; + _delegateImplementsReferenceSizeForFooter = NO; + } else { + _delegateImplementsReferenceSizeForHeader = [delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)]; + _delegateImplementsReferenceSizeForFooter = [delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)]; + } +} + +#pragma mark - ASCollectionViewLayoutInspecting + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + CGSize constrainedSize; + CGSize supplementarySize = [self sizeForSupplementaryViewOfKind:kind inSection:indexPath.section collectionView:collectionView]; + if (_layout.scrollDirection == UICollectionViewScrollDirectionVertical) { + constrainedSize = CGSizeMake(collectionView.bounds.size.width, supplementarySize.height); + } else { + constrainedSize = CGSizeMake(supplementarySize.height, collectionView.bounds.size.height); + } + return ASSizeRangeMake(CGSizeZero, constrainedSize); +} + +- (NSUInteger)collectionView:(ASCollectionView *)collectionView numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind +{ + if ([collectionView.asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { + return [collectionView.asyncDataSource numberOfSectionsInCollectionView:collectionView]; + } else { + return 1; + } +} + +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + return [self layoutHasSupplementaryViewOfKind:kind inSection:section collectionView:collectionView] ? 1 : 0; +} + +#pragma mark - Private helpers + +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)kind inSection:(NSUInteger)section collectionView:(ASCollectionView *)collectionView +{ + if (ASObjectIsEqual(kind, UICollectionElementKindSectionHeader)) { + if (_delegateImplementsReferenceSizeForHeader) { + return [[self delegateForCollectionView:collectionView] collectionView:collectionView layout:_layout referenceSizeForHeaderInSection:section]; + } else { + return [self.layout headerReferenceSize]; + } + } else if (ASObjectIsEqual(kind, UICollectionElementKindSectionFooter)) { + if (_delegateImplementsReferenceSizeForFooter) { + return [[self delegateForCollectionView:collectionView] collectionView:collectionView layout:_layout referenceSizeForFooterInSection:section]; + } else { + return [self.layout footerReferenceSize]; + } + } else { + return CGSizeZero; + } +} + +- (BOOL)layoutHasSupplementaryViewOfKind:(NSString *)kind inSection:(NSUInteger)section collectionView:(ASCollectionView *)collectionView +{ + CGSize size = [self sizeForSupplementaryViewOfKind:kind inSection:section collectionView:collectionView]; + if ([self usedLayoutValueForSize:size] > 0) { + return YES; + } else { + return NO; + } +} + +- (CGFloat)usedLayoutValueForSize:(CGSize)size +{ + if (_layout.scrollDirection == UICollectionViewScrollDirectionVertical) { + return size.height; + } else { + return size.width; + } +} + +- (id)delegateForCollectionView:(ASCollectionView *)collectionView +{ + return (id)collectionView.asyncDelegate; +} + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm index cbed7f2bb7..9619cba111 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm @@ -137,7 +137,9 @@ typedef struct ASRangeGeometry ASRangeGeometry; NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; NSArray *layoutAttributes = [_collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; for (UICollectionViewLayoutAttributes *la in layoutAttributes) { - [indexPathSet addObject:la.indexPath]; + if (la.representedElementCategory == UICollectionElementCategoryCell) { + [indexPathSet addObject:la.indexPath]; + } } return indexPathSet; } diff --git a/AsyncDisplayKit/Details/ASDataController+Subclasses.h b/AsyncDisplayKit/Details/ASDataController+Subclasses.h new file mode 100644 index 0000000000..23c28e9aa4 --- /dev/null +++ b/AsyncDisplayKit/Details/ASDataController+Subclasses.h @@ -0,0 +1,159 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASDataController.h" + +@interface ASDataController (Subclasses) + +#pragma mark - Internal editing & completed store querying + +/** + * Provides a collection of index paths for nodes of the given kind that are currently in the editing store + */ +- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind; + +/** + * Read-only access to the underlying editing nodes of the given kind + */ +- (NSMutableArray *)editingNodesOfKind:(NSString *)kind; + +/** + * Read only access to the underlying completed nodes of the given kind + */ +- (NSMutableArray *)completedNodesOfKind:(NSString *)kind; + +#pragma mark - Node sizing + +/** + * Measure and layout the given nodes in optimized batches, constraining each to a given size in `constrainedSizeForNodeOfKind:atIndexPath:`. + */ +- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; + +/* + * Perform measurement and layout of loaded nodes on the main thread, skipping unloaded nodes. + * + * @discussion Once nodes have loaded their views, we can't layout in the background so this is a chance + * to do so immediately on the main thread. + */ +- (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths; + +/** + * Provides the size range for a specific node during the layout process. + */ +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +#pragma mark - Node & Section Insertion/Deletion API + +/** + * Inserts the given nodes of the specified kind into the backing store, calling completion on the main thread when the write finishes. + */ +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; + +/** + * Deletes the given nodes of the specified kind in the backing store, calling completion on the main thread when the deletion finishes. + */ +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; + +/** + * Inserts the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + */ +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock; + +/** + * Deletes the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + */ +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock; + +#pragma mark - Data Manipulation Hooks + +/** + * Notifies the subclass to perform any work needed before the data controller is reloaded entirely + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + */ + - (void)prepareForReloadData; + +/** + * Notifies the subclass that the data controller is about to reload its data entirely + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform new node creation like supplementary views + * or header/footer nodes. + */ +- (void)willReloadData; + +/** + * Notifies the subclass to perform setup before sections are inserted in the data controller + * + * @discussion This method will be performed before the data controller enters its editing queue. + * The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param sections Indices of sections to be inserted + */ +- (void)prepareForInsertSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will insert new sections at the given position + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be inserted + */ +- (void)willInsertSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will delete sections at the given positions + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be deleted + */ +- (void)willDeleteSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass to perform any work needed before the given sections will be reloaded. + * + * @discussion This method will be performed before the data controller enters its editing queue, usually on the main + * thread. The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param sections Indices of sections to be reloaded + */ +- (void)prepareForReloadSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will reload the sections in the given index set + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be reloaded + */ +- (void)willReloadSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will move a section to a new position + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param section Index of current section position + * @param newSection Index of new section position + */ +- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection; + +@end diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index aac7d94cf5..8b27d7e2d9 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -16,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN @class ASCellNode; @class ASDataController; +FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; + typedef NSUInteger ASDataControllerAnimationOptions; /** @@ -42,7 +44,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; /** Fetch the number of sections. */ -- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController; +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController; /** Lock the data source for data fetching. @@ -92,13 +94,12 @@ typedef NSUInteger ASDataControllerAnimationOptions; @end - /** * Controller to layout data in background, and managed data updating. * * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the data * will be updated asynchronously. The dataSource must be updated to reflect the changes before these methods has been called. - * For each data updatin, the corresponding methods in delegate will be called. + * For each data updating, the corresponding methods in delegate will be called. */ @protocol ASFlowLayoutControllerDataSource; @interface ASDataController : ASDealloc2MainObject @@ -158,15 +159,19 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** - * Re-measures all loaded nodes. Used to respond to a change in size of the containing view + * Re-measures all loaded nodes in the backing store. + * + * @discussion Used to respond to a change in size of the containing view * (e.g. ASTableView or ASCollectionView after an orientation change). */ -- (void)relayoutAllRows; +- (void)relayoutAllNodes; - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^ _Nullable)())completion; +- (void)reloadDataImmediatelyWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; + /** @name Data Querying */ - (NSUInteger)numberOfSections; @@ -179,7 +184,10 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths; -- (NSArray *> *)completedNodes; // This provides efficient access to the entire _completedNodes multidimensional array. +/** + * Direct access to the nodes that have completed calculation and layout + */ +- (NSArray *> *)completedNodes; @end diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 6a5b361ff0..7d7e81568c 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -15,18 +15,23 @@ #import "ASDisplayNode.h" #import "ASMultidimensionalArrayUtils.h" #import "ASDisplayNodeInternal.h" +#import "ASLayout.h" //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; +NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; + static void *kASSizingQueueContext = &kASSizingQueueContext; @interface ASDataController () { NSMutableArray *_externalCompletedNodes; // Main thread only. External data access can immediately query this if available. - NSMutableArray *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable. - NSMutableArray *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes. + NSMutableDictionary *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable. + NSMutableDictionary *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes. + + NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking. NSOperationQueue *_editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. @@ -52,9 +57,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return nil; } - _completedNodes = [NSMutableArray array]; - _editingNodes = [NSMutableArray array]; + _completedNodes = [NSMutableDictionary dictionary]; + _editingNodes = [NSMutableDictionary dictionary]; + _completedNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; + _editingNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; + _pendingEditCommandBlocks = [NSMutableArray array]; _editingTransactionQueue = [[NSOperationQueue alloc] init]; @@ -96,29 +104,54 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Cell Layout -/* - * FIXME: Shouldn't this method, as well as `_layoutNodes:atIndexPaths:withAnimationOptions:` use the word "measure" instead? - * - * Once nodes have loaded their views, we can't layout in the background so this is a chance - * to do so immediately on the main thread. - */ -- (void)_layoutNodesWithMainThreadAffinity:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths { +- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +{ + NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; + + // Processing in batches + for (NSUInteger i = 0; i < indexPaths.count; i += blockSize) { + NSRange batchedRange = NSMakeRange(i, MIN(indexPaths.count - i, blockSize)); + NSArray *batchedIndexPaths = [indexPaths subarrayWithRange:batchedRange]; + NSArray *batchedNodes = [nodes subarrayWithRange:batchedRange]; + + [self _layoutNodes:batchedNodes ofKind:kind atIndexPaths:batchedIndexPaths completion:completionBlock]; + } +} + +- (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { NSAssert(NSThread.isMainThread, @"Main thread layout must be on the main thread."); [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, __unused BOOL * stop) { ASCellNode *node = nodes[idx]; if (node.isNodeLoaded) { - ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + [self _layoutNode:node withConstrainedSize:constrainedSize]; } }]; } -- (void)_layoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +/** + * Measure and layout the given node with the constrained size range. + */ +- (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize +{ + [node measureWithSizeRange:constrainedSize]; + node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); +} + +/** + * Measures and defines the layout for each node in optimized batches on an editing queue, inserting the results into the backing store. + */ +- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self batchLayoutNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + // Insert finished nodes into data storage + [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +- (void)_layoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { - ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"Cell node layout must be initiated from edit transaction queue"); - if (!nodes.count) { return; } @@ -131,7 +164,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; for (NSUInteger k = j; k < j + batchCount; k++) { ASCellNode *node = nodes[k]; if (!node.isNodeLoaded) { - nodeBoundSizes[k] = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPaths[k]]; + nodeBoundSizes[k] = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPaths[k]]; } } @@ -139,11 +172,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; for (NSUInteger k = j; k < j + batchCount; k++) { ASCellNode *node = nodes[k]; // Only measure nodes whose views aren't loaded, since we're in the background. - // We should already have measured loaded nodes before we left the main thread, using _layoutNodesWithMainThreadAffinity: + // We should already have measured loaded nodes before we left the main thread, using layoutLoadedNodes:ofKind:atIndexPaths: if (!node.isNodeLoaded) { - ASSizeRange constrainedSize = nodeBoundSizes[k]; - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + [self _layoutNode:node withConstrainedSize:nodeBoundSizes[k]]; } } }); @@ -152,83 +183,150 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Block the _editingTransactionQueue from executing a new edit transaction until layout is done & _editingNodes array is updated. dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); free(nodeBoundSizes); - // Insert finished nodes into data storage - [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; -} -- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions -{ - NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; - - // Processing in batches - for (NSUInteger i = 0; i < indexPaths.count; i += blockSize) { - NSRange batchedRange = NSMakeRange(i, MIN(indexPaths.count - i, blockSize)); - NSArray *batchedIndexPaths = [indexPaths subarrayWithRange:batchedRange]; - NSArray *batchedNodes = [nodes subarrayWithRange:batchedRange]; - - [self _layoutNodes:batchedNodes atIndexPaths:batchedIndexPaths withAnimationOptions:animationOptions]; + if (completionBlock) { + completionBlock(nodes, indexPaths); } } -#pragma mark - Internal Data Querying + Editing +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + return [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; +} -- (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +#pragma mark - External Data Querying + Editing + +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (indexPaths.count == 0) return; - ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths, nodes); + + NSMutableArray *editingNodes = _editingNodes[kind]; + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes); + _editingNodes[kind] = editingNodes; // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. - NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes); + NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(editingNodes); ASDisplayNodePerformBlockOnMainThread(^{ - _completedNodes = completedNodes; - if (_delegateDidInsertNodes) - [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + _completedNodes[kind] = completedNodes; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } }); } -- (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { - if (indexPaths.count == 0) + if (indexPaths.count == 0) { return; - LOG(@"_deleteNodesAtIndexPaths:%@, full index paths in _editingNodes = %@", indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes)); - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); + } + + LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@, full index paths in _editingNodes = %@", indexPaths, kind, ASIndexPathsForMultidimensionalArray(_editingNodes[kind])); + NSMutableArray *editingNodes = _editingNodes[kind]; + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths); + _editingNodes[kind] = editingNodes; ASDisplayNodePerformBlockOnMainThread(^{ - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); - if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); + if (completionBlock) { + completionBlock(nodes, indexPaths); + } }); } -- (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock { if (indexSet.count == 0) return; - [_editingNodes insertObjects:sections atIndexes:indexSet]; + + if (_editingNodes[kind] == nil) { + _editingNodes[kind] = [NSMutableArray array]; + } + + [_editingNodes[kind] insertObjects:sections atIndexes:indexSet]; // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); ASDisplayNodePerformBlockOnMainThread(^{ - [_completedNodes insertObjects:sectionsForCompleted atIndexes:indexSet]; - if (_delegateDidInsertSections) - [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + [_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet]; + if (completionBlock) { + completionBlock(sections, indexSet); + } }); } -- (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock { if (indexSet.count == 0) return; - [_editingNodes removeObjectsAtIndexes:indexSet]; + [_editingNodes[kind] removeObjectsAtIndexes:indexSet]; ASDisplayNodePerformBlockOnMainThread(^{ - [_completedNodes removeObjectsAtIndexes:indexSet]; + [_completedNodes[kind] removeObjectsAtIndexes:indexSet]; + if (completionBlock) { + completionBlock(indexSet); + } + }); +} + +#pragma mark - Internal Data Querying + Editing + +/** + * Inserts the specified nodes into the given index paths and notifies the delegate of newly inserted nodes. + * + * @discussion Nodes are first inserted into the editing store, then the completed store is replaced by a deep copy + * of the editing nodes. The delegate is invoked on the main thread. + */ +- (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +/** + * Removes the specified nodes at the given index paths and notifies the delegate of the nodes removed. + * + * @discussion Nodes are first removed from the editing store then removed from the completed store on the main thread. + * Once the backing stores are consistent, the delegate is invoked on the main thread. + */ +- (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + if (_delegateDidDeleteNodes) + [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +/** + * Inserts sections, represented as arrays, into the backing store at the given indicies and notifies the delegate. + * + * @discussion The section arrays are inserted into the editing store, then a deep copy of the sections are inserted + * in the completed store on the main thread. The delegate is invoked on the main thread. + */ +- (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) { + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + }]; +} + +/** + * Removes sections at the given indicies from the backing store and notifies the delegate. + * + * @discussion Section array are first removed from the editing store, then the associated section in the completed + * store is removed on the main thread. The delegate is invoked on the main thread. + */ +- (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSIndexSet *indexSet) { if (_delegateDidDeleteSections) [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - }); + }]; } #pragma mark - Initial Load & Full Reload (External API) @@ -239,7 +337,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; ASDisplayNodeAssertMainThread(); [self accessDataSourceWithBlock:^{ NSMutableArray *indexPaths = [NSMutableArray array]; - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; + NSUInteger sectionNum = [_dataSource numberOfSectionsInDataController:self]; // insert sections [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; @@ -260,30 +358,46 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } - (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^)())completion +{ + [self _reloadDataWithAnimationOptions:animationOptions synchronously:NO completion:completion]; +} + +- (void)reloadDataImmediatelyWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self _reloadDataWithAnimationOptions:animationOptions synchronously:YES completion:nil]; +} + +- (void)_reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions synchronously:(BOOL)synchronously completion:(void (^)())completion { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - [self accessDataSourceWithBlock:^{ - NSUInteger sectionCount = [_dataSource dataControllerNumberOfSections:self]; + [self accessDataSourceSynchronously:synchronously withBlock:^{ + NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self]; NSMutableArray *updatedNodes = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths]; + + // Allow subclasses to perform setup before going into the edit transaction + [self prepareForReloadData]; - [_editingTransactionQueue addOperationWithBlock:^{ + void (^transactionBlock)() = ^{ LOG(@"Edit Transaction - reloadData"); // Remove everything that existed before the reload, now that we're ready to insert replacements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind]); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _editingNodes.count)]; + NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + [self willReloadData]; + // Insert each section NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { @@ -297,16 +411,32 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; if (completion) { dispatch_async(dispatch_get_main_queue(), completion); } - }]; + }; + + if (synchronously) { + transactionBlock(); + } else { + [_editingTransactionQueue addOperationWithBlock:transactionBlock]; + } }]; }]; } #pragma mark - Data Source Access (Calling _dataSource) +/** + * Safely locks access to the data source and executes the given block, unlocking once complete. + * + * @discussion When `asyncDataFetching` is enabled, the block is executed on a background thread. + */ - (void)accessDataSourceWithBlock:(dispatch_block_t)block { - if (_asyncDataFetchingEnabled) { + [self accessDataSourceSynchronously:NO withBlock:block]; +} + +- (void)accessDataSourceSynchronously:(BOOL)synchronously withBlock:(dispatch_block_t)block +{ + if (!synchronously && _asyncDataFetchingEnabled) { [_dataSource dataControllerLockDataSource]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ block(); @@ -319,6 +449,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } } +/** + * Fetches row nodes and their specified index paths for the provided sections from the data source. + * + * @discussion Results are stored in the passed mutable arrays. + */ - (void)_populateFromDataSourceWithSectionIndexSet:(NSIndexSet *)indexSet mutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths { [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { @@ -333,9 +468,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } +/** + * Fetches row nodes and their specified index paths for all sections from the data source. + * + * @discussion Results are stored in the passed mutable arrays. + */ - (void)_populateFromEntireDataSourceWithMutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths { - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; + NSUInteger sectionNum = [_dataSource numberOfSectionsInDataController:self]; for (NSUInteger i = 0; i < sectionNum; i++) { NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; @@ -375,7 +515,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; ASDisplayNodePerformBlockOnMainThread(^{ // Deep copy _completedNodes to _externalCompletedNodes. // Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate. - _externalCompletedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes); + _externalCompletedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); [_delegate dataControllerBeginUpdates:self]; @@ -403,6 +543,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } } +/** + * Queues the given operation until an `endUpdates` synchronize update is completed. + * + * If this method is called outside of a begin/endUpdates batch update, the block is + * executed immediately. + */ - (void)performEditCommandWithBlock:(void (^)(void))block { // This method needs to block the thread and synchronously perform the operation if we are not @@ -416,49 +562,55 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Section Editing (External API) -- (void)insertSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - insertSections: %@", indexSet); + LOG(@"Edit Command - insertSections: %@", sections); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ NSMutableArray *updatedNodes = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths]; + + [self prepareForInsertSections:sections]; [_editingTransactionQueue addOperationWithBlock:^{ - LOG(@"Edit Transaction - insertSections: %@", indexSet); - NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; - for (NSUInteger i = 0; i < indexSet.count; i++) { + [self willInsertSections:sections]; + + LOG(@"Edit Transaction - insertSections: %@", sections); + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } - [self _insertSections:sectionArray atIndexSet:indexSet withAnimationOptions:animationOptions]; + [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; }]; }]; } -- (void)deleteSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - deleteSections: %@", indexSet); + LOG(@"Edit Command - deleteSections: %@", sections); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue addOperationWithBlock:^{ + [self willDeleteSections:sections]; + // remove elements - LOG(@"Edit Transaction - deleteSections: %@", indexSet); - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet); + LOG(@"Edit Transaction - deleteSections: %@", sections); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; }]; }]; } @@ -481,12 +633,16 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // at this time. Thus _editingNodes could be empty and crash in ASIndexPathsForMultidimensional[...] // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths]; + + [self prepareForReloadSections:sections]; [_editingTransactionQueue addOperationWithBlock:^{ - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections); + [self willReloadSections:sections]; + + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes)); + LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -506,12 +662,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue addOperationWithBlock:^{ + [self willMoveSection:section toSection:newSection]; + // remove elements LOG(@"Edit Transaction - moveSection"); - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], indexPaths); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; // update the section of indexpaths @@ -527,6 +685,49 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } + +#pragma mark - Backing store manipulation optional hooks (Subclass API) + +- (void)prepareForReloadData +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willReloadData +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForInsertSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willInsertSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willDeleteSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForReloadSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willReloadSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + #pragma mark - Row Editing (External API) - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions @@ -546,7 +747,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:nodes atIndexPaths:indexPaths]; + [self layoutLoadedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths]; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - insertRows: %@", indexPaths); @@ -596,7 +797,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:nodes atIndexPaths:indexPaths]; + [self layoutLoadedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths]; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadRows: %@", indexPaths); @@ -607,41 +808,46 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } -- (void)relayoutAllRows +- (void)relayoutAllNodes { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); LOG(@"Edit Command - relayoutRows"); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - - void (^relayoutNodesBlock)(NSMutableArray *) = ^void(NSMutableArray *nodes) { - if (!nodes.count) { - return; - } - - [self accessDataSourceWithBlock:^{ - [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { - [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; - ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); - }]; - }]; - }]; - }; // Can't relayout right away because _completedNodes may not be up-to-date, // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes // (see _layoutNodes:atIndexPaths:withAnimationOptions:). [_editingTransactionQueue addOperationWithBlock:^{ ASDisplayNodePerformBlockOnMainThread(^{ - relayoutNodesBlock(_completedNodes); + for (NSString *kind in [_completedNodes keyEnumerator]) { + [self _relayoutNodesOfKind:kind]; + } }); }]; }]; } +- (void)_relayoutNodesOfKind:(NSString *)kind +{ + ASDisplayNodeAssertMainThread(); + NSArray *nodes = [self completedNodesOfKind:kind]; + if (!nodes.count) { + return; + } + + [self accessDataSourceWithBlock:^{ + [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { + [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + ASLayout *layout = [node measureWithSizeRange:constrainedSize]; + node.frame = CGRectMake(0.0f, 0.0f, layout.size.width, layout.size.height); + }]; + }]; + }]; +} + - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ @@ -651,7 +857,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], [NSArray arrayWithObject:indexPath]); NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -662,6 +868,23 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } +#pragma mark - Data Querying (Subclass API) + +- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind +{ + return _editingNodes[kind] != nil ? ASIndexPathsForMultidimensionalArray(_editingNodes[kind]) : nil; +} + +- (NSMutableArray *)editingNodesOfKind:(NSString *)kind +{ + return _editingNodes[kind] != nil ? _editingNodes[kind] : [NSMutableArray array]; +} + +- (NSMutableArray *)completedNodesOfKind:(NSString *)kind +{ + return _completedNodes[kind]; +} + #pragma mark - Data Querying (External API) - (NSUInteger)numberOfSections @@ -711,7 +934,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (NSArray *)completedNodes { ASDisplayNodeAssertMainThread(); - return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes; + return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes[ASDataControllerRowNodeKind]; } #pragma mark - Dealloc @@ -719,15 +942,17 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)dealloc { ASDisplayNodeAssertMainThread(); - [_completedNodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { - [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { - if (node.isNodeLoaded) { - if (node.layerBacked) { - [node.layer removeFromSuperlayer]; - } else { - [node.view removeFromSuperview]; + [_completedNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { + [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { + if (node.isNodeLoaded) { + if (node.layerBacked) { + [node.layer removeFromSuperlayer]; + } else { + [node.view removeFromSuperview]; + } } - } + }]; }]; }]; } diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index aaf67689d9..015aa0f4f6 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN * * @param contentView UIView to add a (sized) node's view to. * - * @param node The ASCellNode to be added. + * @param cellNode The cell node to be added. */ - (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node; diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index b22db853bf..146b85897f 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -48,7 +48,7 @@ #pragma mark - View manipulation -- (void)moveNode:(ASCellNode *)node toView:(UIView *)view +- (void)moveCellNode:(ASCellNode *)node toView:(UIView *)view { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(node, @"Cannot move a nil node to a view"); @@ -75,7 +75,7 @@ // coalesce these events -- handling them multiple times per runloop is noisy and expensive _queuedRangeUpdate = YES; - + [self performSelector:@selector(updateVisibleNodeIndexPaths) withObject:nil afterDelay:0 @@ -158,9 +158,9 @@ return rangeType == ASLayoutRangeTypeRender; } -- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)cellNode +- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node { - if (cellNode.view.superview == contentView) { + if (node.view.superview == contentView) { // this content view is already correctly configured return; } @@ -170,7 +170,7 @@ [view removeFromSuperview]; } - [self moveNode:cellNode toView:contentView]; + [self moveCellNode:node toView:contentView]; } diff --git a/AsyncDisplayKit/Details/UIView+ASConvenience.h b/AsyncDisplayKit/Details/UIView+ASConvenience.h index dfd59167b7..43b8a1a714 100644 --- a/AsyncDisplayKit/Details/UIView+ASConvenience.h +++ b/AsyncDisplayKit/Details/UIView+ASConvenience.h @@ -32,9 +32,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) CGFloat shadowRadius; @property (nonatomic, assign) CGFloat borderWidth; @property (nonatomic, assign, getter = isOpaque) BOOL opaque; -@property (nullable, nonatomic, retain) __attribute__((NSObject)) CGColorRef borderColor; -@property (nullable, nonatomic, copy) NSString *asyncdisplaykit_name; -@property (nullable, nonatomic, retain) __attribute__((NSObject)) CGColorRef backgroundColor; +@property (nonatomic, retain) __attribute__((NSObject)) CGColorRef borderColor; +@property (nonatomic, retain) __attribute__((NSObject)) CGColorRef backgroundColor; @property (nonatomic, assign) BOOL allowsEdgeAntialiasing; @property (nonatomic, assign) unsigned int edgeAntialiasingMask; @@ -81,9 +80,3 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, copy) NSString *accessibilityIdentifier; @end - -@interface CALayer (ASDisplayNodeLayer) -@property (nullable, atomic, copy) NSString *asyncdisplaykit_name; -@end - -NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Details/UIView+ASConvenience.m b/AsyncDisplayKit/Details/UIView+ASConvenience.m deleted file mode 100644 index 6772024821..0000000000 --- a/AsyncDisplayKit/Details/UIView+ASConvenience.m +++ /dev/null @@ -1,13 +0,0 @@ -/* 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. - */ - -#import "UIView+ASConvenience.h" - -@implementation CALayer (ASDisplayNodeLayer) -@dynamic asyncdisplaykit_name; -@end diff --git a/AsyncDisplayKit/Details/_ASDisplayLayer.mm b/AsyncDisplayKit/Details/_ASDisplayLayer.mm index 1d9b023983..99e4bf68e8 100644 --- a/AsyncDisplayKit/Details/_ASDisplayLayer.mm +++ b/AsyncDisplayKit/Details/_ASDisplayLayer.mm @@ -36,11 +36,6 @@ _displaySentinel = [[ASSentinel alloc] init]; self.opaque = YES; - -#if DEBUG - // This is too expensive to do in production on all layers. - self.name = [NSString stringWithFormat:@"%@ (%p)", NSStringFromClass([self class]), self]; -#endif } return self; } diff --git a/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.h b/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.h new file mode 100644 index 0000000000..198e792d1e --- /dev/null +++ b/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.h @@ -0,0 +1,59 @@ +/* + * 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. + * + */ + +#import + +@protocol ASLayoutableAsciiArtProtocol +/** + * Returns an ascii-art representation of this object and its children. + * For example, an ASInsetSpec may return something like this: + * + * --ASInsetLayoutSpec-- + * | ASTextNode | + * --------------------- + */ +- (NSString *)asciiArtString; + +/** + * returns the name of this object that will display in the ascii art. Usually this can + * simply be NSStringFromClass([self class]). + */ +- (NSString *)asciiArtName; + +@end + +/** + * A that takes a parent and its children and renders as ascii art box. + */ +@interface ASAsciiArtBoxCreator : NSObject + +/** + * Renders an ascii art box with the children aligned horizontally + * Example: + * ------------ASStackLayoutSpec----------- + * | ASTextNode ASTextNode ASTextNode | + * ---------------------------------------- + */ ++ (NSString *)horizontalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent; + +/** + * Renders an ascii art box with the children aligned vertically. + * Example: + * --ASStackLayoutSpec-- + * | ASTextNode | + * | ASTextNode | + * | ASTextNode | + * --------------------- + */ ++ (NSString *)verticalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent; + +@end + + diff --git a/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m b/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m new file mode 100644 index 0000000000..ea9ef9490c --- /dev/null +++ b/AsyncDisplayKit/Layout/ASAsciiArtBoxCreator.m @@ -0,0 +1,185 @@ +/* + * 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. + * + */ + +@import UIKit; +#import "ASAsciiArtBoxCreator.h" + +static const NSUInteger kDebugBoxPadding = 2; + +typedef NS_ENUM(NSUInteger, PIDebugBoxPaddingLocation) +{ + PIDebugBoxPaddingLocationFront, + PIDebugBoxPaddingLocationEnd, + PIDebugBoxPaddingLocationBoth +}; + +@interface NSString(PIDebugBox) + +@end + +@implementation NSString(PIDebugBox) + ++ (instancetype)debugbox_stringWithString:(NSString *)stringToRepeat repeatedCount:(NSUInteger)repeatCount +{ + NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[stringToRepeat length] * repeatCount]; + for (NSUInteger index = 0; index < repeatCount; index++) { + [string appendString:stringToRepeat]; + } + return [string copy]; +} + +- (NSString *)debugbox_stringByAddingPadding:(NSString *)padding count:(NSUInteger)count location:(PIDebugBoxPaddingLocation)location +{ + NSString *paddingString = [NSString debugbox_stringWithString:padding repeatedCount:count]; + switch (location) { + case PIDebugBoxPaddingLocationFront: + return [NSString stringWithFormat:@"%@%@", paddingString, self]; + case PIDebugBoxPaddingLocationEnd: + return [NSString stringWithFormat:@"%@%@", self, paddingString]; + case PIDebugBoxPaddingLocationBoth: + return [NSString stringWithFormat:@"%@%@%@", paddingString, self, paddingString]; + } + return [self copy]; +} + +@end + +@implementation ASAsciiArtBoxCreator + ++ (NSString *)horizontalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent +{ + if ([children count] == 0) { + return parent; + } + + NSMutableArray *childrenLines = [NSMutableArray array]; + + // split the children into lines + NSUInteger lineCountPerChild = 0; + for (NSString *child in children) { + NSArray *lines = [child componentsSeparatedByString:@"\n"]; + lineCountPerChild = MAX(lineCountPerChild, [lines count]); + } + + for (NSString *child in children) { + NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy]; + NSUInteger topPadding = ceilf((CGFloat)(lineCountPerChild - [lines count])/2.0); + NSUInteger bottomPadding = (lineCountPerChild - [lines count])/2.0; + NSUInteger lineLength = [lines[0] length]; + + for (NSUInteger index = 0; index < topPadding; index++) { + [lines insertObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength] atIndex:0]; + } + for (NSUInteger index = 0; index < bottomPadding; index++) { + [lines addObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength]]; + } + [childrenLines addObject:lines]; + } + + NSMutableArray *concatenatedLines = [NSMutableArray array]; + NSString *padding = [NSString debugbox_stringWithString:@" " repeatedCount:kDebugBoxPadding]; + for (NSUInteger index = 0; index < lineCountPerChild; index++) { + NSMutableString *line = [[NSMutableString alloc] init]; + [line appendFormat:@"|%@",padding]; + for (NSArray *childLines in childrenLines) { + [line appendFormat:@"%@%@", childLines[index], padding]; + } + [line appendString:@"|"]; + [concatenatedLines addObject:line]; + } + + // surround the lines in a box + NSUInteger totalLineLength = [concatenatedLines[0] length]; + if (totalLineLength < [parent length]) { + NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - totalLineLength; + NSUInteger leftPadding = ceilf((CGFloat)difference/2.0); + NSUInteger rightPadding = difference/2; + + NSString *leftString = [@"|" debugbox_stringByAddingPadding:@" " count:leftPadding location:PIDebugBoxPaddingLocationEnd]; + NSString *rightString = [@"|" debugbox_stringByAddingPadding:@" " count:rightPadding location:PIDebugBoxPaddingLocationFront]; + + NSMutableArray *paddedLines = [NSMutableArray array]; + for (NSString *line in concatenatedLines) { + NSString *paddedLine = [line stringByReplacingOccurrencesOfString:@"|" withString:leftString options:NSCaseInsensitiveSearch range:NSMakeRange(0, 1)]; + paddedLine = [paddedLine stringByReplacingOccurrencesOfString:@"|" withString:rightString options:NSCaseInsensitiveSearch range:NSMakeRange([paddedLine length] - 1, 1)]; + [paddedLines addObject:paddedLine]; + } + concatenatedLines = paddedLines; + totalLineLength += difference; + } + concatenatedLines = [self appendTopAndBottomToBoxString:concatenatedLines parent:parent]; + return [concatenatedLines componentsJoinedByString:@"\n"]; + +} + ++ (NSString *)verticalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent +{ + if ([children count] == 0) { + return parent; + } + + NSMutableArray *childrenLines = [NSMutableArray array]; + + NSUInteger maxChildLength = 0; + for (NSString *child in children) { + NSArray *lines = [child componentsSeparatedByString:@"\n"]; + maxChildLength = MAX(maxChildLength, [lines[0] length]); + } + + NSUInteger rightPadding = 0; + NSUInteger leftPadding = 0; + + if (maxChildLength < [parent length]) { + NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - maxChildLength; + leftPadding = ceilf((CGFloat)difference/2.0); + rightPadding = difference/2; + } + + NSString *rightPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightPadding + kDebugBoxPadding]; + NSString *leftPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftPadding + kDebugBoxPadding]; + + for (NSString *child in children) { + NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy]; + + NSUInteger leftLinePadding = ceilf((CGFloat)(maxChildLength - [lines[0] length])/2.0); + NSUInteger rightLinePadding = (maxChildLength - [lines[0] length])/2.0; + + for (NSString *line in lines) { + NSString *rightLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightLinePadding]; + rightLinePaddingString = [NSString stringWithFormat:@"%@%@|", rightLinePaddingString, rightPaddingString]; + + NSString *leftLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftLinePadding]; + leftLinePaddingString = [NSString stringWithFormat:@"|%@%@", leftLinePaddingString, leftPaddingString]; + + NSString *paddingLine = [NSString stringWithFormat:@"%@%@%@", leftLinePaddingString, line, rightLinePaddingString]; + [childrenLines addObject:paddingLine]; + } + } + + childrenLines = [self appendTopAndBottomToBoxString:childrenLines parent:parent]; + return [childrenLines componentsJoinedByString:@"\n"]; +} + ++ (NSMutableArray *)appendTopAndBottomToBoxString:(NSMutableArray *)boxStrings parent:(NSString *)parent +{ + NSUInteger totalLineLength = [boxStrings[0] length]; + [boxStrings addObject:[NSString debugbox_stringWithString:@"-" repeatedCount:totalLineLength]]; + + NSUInteger leftPadding = ceilf(((CGFloat)(totalLineLength - [parent length]))/2.0); + NSUInteger rightPadding = (totalLineLength - [parent length])/2; + + NSString *topLine = [parent debugbox_stringByAddingPadding:@"-" count:leftPadding location:PIDebugBoxPaddingLocationFront]; + topLine = [topLine debugbox_stringByAddingPadding:@"-" count:rightPadding location:PIDebugBoxPaddingLocationEnd]; + [boxStrings insertObject:topLine atIndex:0]; + + return boxStrings; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.h b/AsyncDisplayKit/Layout/ASLayoutSpec.h index 15dda10ce6..da6de78e0f 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.h @@ -9,6 +9,7 @@ */ #import +#import NS_ASSUME_NONNULL_BEGIN @@ -99,5 +100,14 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface ASLayoutSpec (Debugging) +/** + * Used by other layout specs to create ascii art debug strings + */ ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName direction:(ASStackLayoutDirection)direction; ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName; + +@end + NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.mm b/AsyncDisplayKit/Layout/ASLayoutSpec.mm index 3ba312709b..35beca3161 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.mm @@ -122,5 +122,40 @@ static NSString * const kDefaultChildrenKey = @"kDefaultChildrenKey"; return self.layoutChildren[kDefaultChildrenKey]; } - +@end + +@implementation ASLayoutSpec (Debugging) + +#pragma mark - ASLayoutableAsciiArtProtocol + ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName direction:(ASStackLayoutDirection)direction +{ + NSMutableArray *childStrings = [NSMutableArray array]; + for (id layoutChild in children) { + NSString *childString = [layoutChild asciiArtString]; + if (childString) { + [childStrings addObject:childString]; + } + } + if (direction == ASStackLayoutDirectionHorizontal) { + return [ASAsciiArtBoxCreator horizontalBoxStringForChildren:childStrings parent:parentName]; + } + return [ASAsciiArtBoxCreator verticalBoxStringForChildren:childStrings parent:parentName]; +} + ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName +{ + return [self asciiArtStringForChildren:children parentName:parentName direction:ASStackLayoutDirectionHorizontal]; +} + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[self.child] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + return NSStringFromClass([self class]); +} + @end diff --git a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm index 1fdfd5fc28..ba580d08b1 100644 --- a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm @@ -73,3 +73,14 @@ static NSString * const kOverlayChildKey = @"kOverlayChildKey"; } @end + +@implementation ASOverlayLayoutSpec (Debugging) + +#pragma mark - ASLayoutableAsciiArtProtocol + +- (NSString *)debugBoxString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[self.overlay, self.child] parentName:[self asciiArtName]]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm b/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm index eac7464aa3..4e04d4ca05 100644 --- a/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm @@ -87,3 +87,14 @@ } @end + +@implementation ASRatioLayoutSpec (Debugging) + +#pragma mark - ASLayoutableAsciiArtProtocol + +- (NSString *)asciiArtName +{ + return [NSString stringWithFormat:@"%@ (%.1f)", NSStringFromClass([self class]), self.ratio]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index a1d27b3c5e..2269ffffeb 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -136,3 +136,14 @@ } @end + +@implementation ASStackLayoutSpec (Debugging) + +#pragma mark - ASLayoutableAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:self.children parentName:[self asciiArtName] direction:self.direction]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm index 7652247f9b..f346017316 100644 --- a/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm @@ -85,3 +85,14 @@ } @end + +@implementation ASStaticLayoutSpec (Debugging) + +#pragma mark - ASLayoutableAsciiArtProtocol + +- (NSString *)debugBoxString +{ + return [ASLayoutSpec asciiArtStringForChildren:self.children parentName:[self asciiArtName]]; +} + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index 4909abdd46..63ef4ed8da 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -496,18 +496,6 @@ _setToLayer(edgeAntialiasingMask, edgeAntialiasingMask); } -- (NSString *)name -{ - _bridge_prologue; - return _getFromLayer(asyncdisplaykit_name); -} - -- (void)setName:(NSString *)name -{ - _bridge_prologue; - _setToLayer(asyncdisplaykit_name, name); -} - - (BOOL)isAccessibilityElement { _bridge_prologue; diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index 6a9d74e97e..00bed4b1d6 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -26,3 +26,7 @@ CGFloat ASCeilPixelValue(CGFloat f); CGFloat ASRoundPixelValue(CGFloat f); ASDISPLAYNODE_EXTERN_C_END + +@interface NSIndexPath (ASInverseComparison) +- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath; +@end diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.mm b/AsyncDisplayKit/Private/ASInternalHelpers.mm index f57450d39e..d2337a44b8 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.mm +++ b/AsyncDisplayKit/Private/ASInternalHelpers.mm @@ -70,3 +70,12 @@ CGFloat ASRoundPixelValue(CGFloat f) { return roundf(f * ASScreenScale()) / ASScreenScale(); } + +@implementation NSIndexPath (ASInverseComparison) + +- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath +{ + return [otherIndexPath compare:self]; +} + +@end diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h new file mode 100644 index 0000000000..fa67235957 --- /dev/null +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -0,0 +1,72 @@ +// +// _ASHierarchyChangeSet.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/29/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import +#import "ASDataController.h" + +typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { + _ASHierarchyChangeTypeReload, + _ASHierarchyChangeTypeDelete, + _ASHierarchyChangeTypeInsert +}; + +@interface _ASHierarchySectionChange : NSObject + +// FIXME: Generalize this to `changeMetadata` dict? +@property (nonatomic, readonly) ASDataControllerAnimationOptions animationOptions; + +@property (nonatomic, strong, readonly) NSIndexSet *indexSet; +@property (nonatomic, readonly) _ASHierarchyChangeType changeType; +@end + +@interface _ASHierarchyItemChange : NSObject +@property (nonatomic, readonly) ASDataControllerAnimationOptions animationOptions; + +/// Index paths are sorted descending for changeType .Delete, ascending otherwise +@property (nonatomic, strong, readonly) NSArray *indexPaths; +@property (nonatomic, readonly) _ASHierarchyChangeType changeType; +@end + +@interface _ASHierarchyChangeSet : NSObject + +@property (nonatomic, strong, readonly) NSIndexSet *deletedSections; +@property (nonatomic, strong, readonly) NSIndexSet *insertedSections; +@property (nonatomic, strong, readonly) NSIndexSet *reloadedSections; +@property (nonatomic, strong, readonly) NSArray *insertedItems; +@property (nonatomic, strong, readonly) NSArray *deletedItems; +@property (nonatomic, strong, readonly) NSArray *reloadedItems; + +@property (nonatomic, readonly) BOOL completed; + +/// Call this once the change set has been constructed to prevent future modifications to the changeset. Calling this more than once is a programmer error. +- (void)markCompleted; + +/** + @abstract Return sorted changes of the given type, grouped by animation options. + + Items deleted from deleted sections are not reported. + Items inserted into inserted sections are not reported. + Items reloaded in reloaded sections are not reported. + + The safe order for processing change groups is: + - Reloaded sections & reloaded items + - Deleted items, descending order + - Deleted sections, descending order + - Inserted sections, ascending order + - Inserted items, ascending order + */ +- (NSArray /*<_ASHierarchySectionChange *>*/ *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; +- (NSArray /*<_ASHierarchyItemChange *>*/ *)itemChangesOfType:(_ASHierarchyChangeType)changeType; + +- (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +@end diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m new file mode 100644 index 0000000000..d7c6ecab0b --- /dev/null +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -0,0 +1,337 @@ +// +// _ASHierarchyChangeSet.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/29/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import "_ASHierarchyChangeSet.h" +#import "ASInternalHelpers.h" + +@interface _ASHierarchySectionChange () +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + On return `changes` is sorted according to the change type with changes coalesced by animationOptions + Assumes: `changes` is [_ASHierarchySectionChange] all with the same changeType + */ ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes; +@end + +@interface _ASHierarchyItemChange () +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted; + +/** + On return `changes` is sorted according to the change type with changes coalesced by animationOptions + Assumes: `changes` is [_ASHierarchyItemChange] all with the same changeType + */ ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections; +@end + +@interface _ASHierarchyChangeSet () + +@property (nonatomic, strong, readonly) NSMutableArray *insertItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray *deleteItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray *reloadItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray *insertSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray *deleteSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray *reloadSectionChanges; + +@end + +@implementation _ASHierarchyChangeSet { + NSMutableIndexSet *_deletedSections; + NSMutableIndexSet *_insertedSections; + NSMutableIndexSet *_reloadedSections; + NSMutableArray *_insertedItems; + NSMutableArray *_deletedItems; + NSMutableArray *_reloadedItems; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _deletedSections = [NSMutableIndexSet new]; + _insertedSections = [NSMutableIndexSet new]; + _reloadedSections = [NSMutableIndexSet new]; + + _deletedItems = [NSMutableArray new]; + _insertedItems = [NSMutableArray new]; + _reloadedItems = [NSMutableArray new]; + + _insertItemChanges = [NSMutableArray new]; + _deleteItemChanges = [NSMutableArray new]; + _reloadItemChanges = [NSMutableArray new]; + _insertSectionChanges = [NSMutableArray new]; + _deleteSectionChanges = [NSMutableArray new]; + _reloadSectionChanges = [NSMutableArray new]; + } + return self; +} + +#pragma mark External API + +- (void)markCompleted +{ + NSAssert(!_completed, @"Attempt to mark already-completed changeset as completed."); + _completed = YES; + [self _sortAndCoalesceChangeArrays]; +} + +- (NSArray *)sectionChangesOfType:(_ASHierarchyChangeType)changeType +{ + [self _ensureCompleted]; + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return _insertSectionChanges; + case _ASHierarchyChangeTypeReload: + return _reloadSectionChanges; + case _ASHierarchyChangeTypeDelete: + return _deleteSectionChanges; + default: + NSAssert(NO, @"Request for section changes with invalid type: %lu", changeType); + } +} + +- (NSArray *)itemChangesOfType:(_ASHierarchyChangeType)changeType +{ + [self _ensureCompleted]; + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return _insertItemChanges; + case _ASHierarchyChangeTypeReload: + return _reloadItemChanges; + case _ASHierarchyChangeTypeDelete: + return _deleteItemChanges; + default: + NSAssert(NO, @"Request for item changes with invalid type: %lu", changeType); + } +} + +- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexPaths:indexPaths animationOptions:options presorted:NO]; + [_deleteItemChanges addObject:change]; +} + +- (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexSet:sections animationOptions:options]; + [_deleteSectionChanges addObject:change]; +} + +- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:indexPaths animationOptions:options presorted:NO]; + [_insertItemChanges addObject:change]; +} + +- (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:sections animationOptions:options]; + [_insertSectionChanges addObject:change]; +} + +- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexPaths:indexPaths animationOptions:options presorted:NO]; + [_reloadItemChanges addObject:change]; +} + +- (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexSet:sections animationOptions:options]; + [_reloadSectionChanges addObject:change]; +} + +#pragma mark Private + +- (BOOL)_ensureNotCompleted +{ + NSAssert(!_completed, @"Attempt to modify completed changeset %@", self); + return !_completed; +} + +- (BOOL)_ensureCompleted +{ + NSAssert(_completed, @"Attempt to process incomplete changeset %@", self); + return _completed; +} + +- (void)_sortAndCoalesceChangeArrays +{ + @autoreleasepool { + [_ASHierarchySectionChange sortAndCoalesceChanges:_deleteSectionChanges]; + [_ASHierarchySectionChange sortAndCoalesceChanges:_insertSectionChanges]; + [_ASHierarchySectionChange sortAndCoalesceChanges:_reloadSectionChanges]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_reloadItemChanges ignoringChangesInSections:_reloadedSections]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_insertItemChanges ignoringChangesInSections:_insertedSections]; + } +} + +@end + +@implementation _ASHierarchySectionChange + +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + self = [super init]; + if (self) { + _changeType = changeType; + _indexSet = indexSet; + _animationOptions = animationOptions; + } + return self; +} + ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes +{ + if (changes.count < 1) { + return; + } + + _ASHierarchyChangeType type = [changes.firstObject changeType]; + + // Lookup table [Int: AnimationOptions] + NSMutableDictionary *animationOptions = [NSMutableDictionary new]; + + // All changed indexes, sorted + NSMutableIndexSet *allIndexes = [NSMutableIndexSet new]; + + for (_ASHierarchySectionChange *change in changes) { + [change.indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, __unused BOOL *stop) { + animationOptions[@(idx)] = @(change.animationOptions); + }]; + [allIndexes addIndexes:change.indexSet]; + } + + // Create new changes by grouping sorted changes by animation option + NSMutableArray *result = [NSMutableArray new]; + + __block ASDataControllerAnimationOptions currentOptions = 0; + __block NSMutableIndexSet *currentIndexes = nil; + NSUInteger lastIndex = allIndexes.lastIndex; + + NSEnumerationOptions options = type == _ASHierarchyChangeTypeDelete ? NSEnumerationReverse : kNilOptions; + [allIndexes enumerateIndexesWithOptions:options usingBlock:^(NSUInteger idx, __unused BOOL * stop) { + ASDataControllerAnimationOptions options = [animationOptions[@(idx)] integerValue]; + BOOL endingCurrentGroup = NO; + + if (currentIndexes == nil) { + // Starting a new group + currentIndexes = [NSMutableIndexSet indexSetWithIndex:idx]; + currentOptions = options; + } else if (options == currentOptions) { + // Continuing the current group + [currentIndexes addIndex:idx]; + } else { + endingCurrentGroup = YES; + } + + BOOL endingLastGroup = (currentIndexes != nil && lastIndex == idx); + + if (endingCurrentGroup || endingLastGroup) { + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:type indexSet:currentIndexes animationOptions:currentOptions]; + [result addObject:change]; + currentOptions = 0; + currentIndexes = nil; + } + }]; + + [changes setArray:result]; +} + +@end + +@implementation _ASHierarchyItemChange + +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted +{ + self = [super init]; + if (self) { + _changeType = changeType; + if (presorted) { + _indexPaths = indexPaths; + } else { + SEL sorting = changeType == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:); + _indexPaths = [indexPaths sortedArrayUsingSelector:sorting]; + } + _animationOptions = animationOptions; + } + return self; +} + ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections +{ + if (changes.count < 1) { + return; + } + + _ASHierarchyChangeType type = [changes.firstObject changeType]; + + // Lookup table [NSIndexPath: AnimationOptions] + NSMutableDictionary *animationOptions = [NSMutableDictionary new]; + + // All changed index paths, sorted + NSMutableArray *allIndexPaths = [NSMutableArray new]; + + NSPredicate *indexPathInValidSection = [NSPredicate predicateWithBlock:^BOOL(NSIndexPath *indexPath, __unused NSDictionary *_) { + return ![sections containsIndex:indexPath.section]; + }]; + for (_ASHierarchyItemChange *change in changes) { + for (NSIndexPath *indexPath in change.indexPaths) { + if ([indexPathInValidSection evaluateWithObject:indexPath]) { + animationOptions[indexPath] = @(change.animationOptions); + [allIndexPaths addObject:indexPath]; + } + } + } + + SEL sorting = type == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:); + [allIndexPaths sortUsingSelector:sorting]; + + // Create new changes by grouping sorted changes by animation option + NSMutableArray *result = [NSMutableArray new]; + + ASDataControllerAnimationOptions currentOptions = 0; + NSMutableArray *currentIndexPaths = nil; + NSIndexPath *lastIndexPath = allIndexPaths.lastObject; + + for (NSIndexPath *indexPath in allIndexPaths) { + ASDataControllerAnimationOptions options = [animationOptions[indexPath] integerValue]; + BOOL endingCurrentGroup = NO; + + if (currentIndexPaths == nil) { + // Starting a new group + currentIndexPaths = [NSMutableArray arrayWithObject:indexPath]; + currentOptions = options; + } else if (options == currentOptions) { + // Continuing the current group + [currentIndexPaths addObject:indexPath]; + } else { + endingCurrentGroup = YES; + } + + BOOL endingLastGroup = (currentIndexPaths != nil && (NSOrderedSame == [lastIndexPath compare:indexPath])); + + if (endingCurrentGroup || endingLastGroup) { + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:currentIndexPaths animationOptions:currentOptions presorted:YES]; + [result addObject:change]; + currentOptions = 0; + currentIndexPaths = nil; + } + } + + [changes setArray:result]; +} + +@end diff --git a/AsyncDisplayKit/Private/_ASPendingState.m b/AsyncDisplayKit/Private/_ASPendingState.m index f8c2426971..4d05431408 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.m +++ b/AsyncDisplayKit/Private/_ASPendingState.m @@ -37,7 +37,6 @@ CGFloat borderWidth; CGColorRef borderColor; BOOL asyncTransactionContainer; - NSString *name; BOOL isAccessibilityElement; NSString *accessibilityLabel; NSString *accessibilityHint; @@ -85,7 +84,6 @@ int setBorderWidth:1; int setBorderColor:1; int setAsyncTransactionContainer:1; - int setName:1; int setAllowsEdgeAntialiasing:1; int setEdgeAntialiasingMask:1; int setIsAccessibilityElement:1; @@ -133,7 +131,6 @@ @synthesize borderWidth=borderWidth; @synthesize borderColor=borderColor; @synthesize asyncdisplaykit_asyncTransactionContainer=asyncTransactionContainer; -@synthesize asyncdisplaykit_name=name; - (id)init { @@ -419,20 +416,6 @@ _flags.setAsyncTransactionContainer = YES; } -// This is named this way, since I'm not sure we can change the setter for the CA version -- (void)setAsyncdisplaykit_name:(NSString *)newName -{ - _flags.setName = YES; - if (name != newName) { - name = [newName copy]; - } -} - -- (NSString *)asyncdisplaykit_name -{ - return name; -} - - (BOOL)isAccessibilityElement { return isAccessibilityElement; @@ -641,9 +624,6 @@ if (_flags.setAsyncTransactionContainer) layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; - if (_flags.setName) - layer.asyncdisplaykit_name = name; - if (_flags.setOpaque) ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); } @@ -756,9 +736,6 @@ if (_flags.setAsyncTransactionContainer) view.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; - if (_flags.setName) - layer.asyncdisplaykit_name = name; - if (_flags.setOpaque) ASDisplayNodeAssert(view.layer.opaque == opaque, @"Didn't set opaque as desired"); @@ -1004,4 +981,11 @@ return pendingState; } +- (void)dealloc +{ + CGColorRelease(backgroundColor); + CGColorRelease(shadowColor); + CGColorRelease(borderColor); +} + @end diff --git a/AsyncDisplayKitTests/ASCollectionViewFlowLayoutInspectorTests.m b/AsyncDisplayKitTests/ASCollectionViewFlowLayoutInspectorTests.m new file mode 100644 index 0000000000..1230807f44 --- /dev/null +++ b/AsyncDisplayKitTests/ASCollectionViewFlowLayoutInspectorTests.m @@ -0,0 +1,368 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import "ASCollectionView.h" +#import "ASCollectionViewFlowLayoutInspector.h" + +/** + * Test Data Source + */ +@interface InspectorTestDataSource : NSObject +@end + +@implementation InspectorTestDataSource + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return [[ASCellNode alloc] init]; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return 0; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 2; +} + +@end + +@interface ASCollectionViewFlowLayoutInspectorTests : XCTestCase + +@end + +/** + * Test Delegate for Header Reference Size Implementation + */ +@interface HeaderReferenceSizeTestDelegate : NSObject + +@end + +@implementation HeaderReferenceSizeTestDelegate + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section +{ + return CGSizeMake(125.0, 125.0); +} + +@end + +/** + * Test Delegate for Footer Reference Size Implementation + */ +@interface FooterReferenceSizeTestDelegate : NSObject + +@end + +@implementation FooterReferenceSizeTestDelegate + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section +{ + return CGSizeMake(125.0, 125.0); +} + +@end + +@implementation ASCollectionViewFlowLayoutInspectorTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +#pragma mark - #collectionView:constrainedSizeForSupplementaryNodeOfKind:atIndexPath: + +// Vertical + +// Delegate implementation + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheHeaderDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheFooterDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + FooterReferenceSizeTestDelegate *delegate = [[FooterReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Size implementation + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheHeaderProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + layout.headerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheFooterProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Horizontal + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheHeaderDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.height)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheFooterDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + FooterReferenceSizeTestDelegate *delegate = [[FooterReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.height)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Size implementation + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheHeaderProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.headerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.width)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheFooterProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.height)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsZeroSizeWhenNoReferenceSizeIsImplemented +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeZero); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a zero size"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +#pragma mark - #collectionView:numberOfSectionsForSupplementaryNodeOfKind: + +- (void)testThatItRespondsWithTheDefaultNumberOfSections +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger sections = [inspector collectionView:collectionView numberOfSectionsForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + XCTAssert(sections == 1, @"should return 1 by default"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItProvidesTheNumberOfSectionsInTheDataSource +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger sections = [inspector collectionView:collectionView numberOfSectionsForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + XCTAssert(sections == 2, @"should return 2"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +#pragma mark - #collectionView:supplementaryNodesOfKind:inSection: + +- (void)testThatItReturnsOneWhenAValidSizeIsImplementedOnTheDelegate +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionHeader inSection:0]; + XCTAssert(count == 1, @"should have a header supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsOneWhenAValidSizeIsImplementedOnTheLayout +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionFooter inSection:0]; + XCTAssert(count == 1, @"should have a footer supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsNoneWhenNoReferenceSizeIsImplemented +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionFooter inSection:0]; + XCTAssert(count == 0, @"should not have a footer supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +@end diff --git a/AsyncDisplayKitTests/ASCollectionViewTests.m b/AsyncDisplayKitTests/ASCollectionViewTests.m index 3b4d661514..59a081ec21 100644 --- a/AsyncDisplayKitTests/ASCollectionViewTests.m +++ b/AsyncDisplayKitTests/ASCollectionViewTests.m @@ -6,7 +6,9 @@ // #import -#import +#import "ASCollectionView.h" +#import "ASCollectionDataController.h" +#import "ASCollectionViewFlowLayoutInspector.h" @interface ASCollectionViewTestDelegate : NSObject @@ -73,13 +75,43 @@ @end +@interface ASCollectionView (InternalTesting) + +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController; + +@end + @interface ASCollectionViewTests : XCTestCase @end @implementation ASCollectionViewTests -- (void)DISABLED_testCollectionViewController { +- (void)testThatItSetsALayoutInspectorForFlowLayouts +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + XCTAssert(collectionView.layoutInspector != nil, @"should automatically set a layout delegate for flow layouts"); + XCTAssert([collectionView.layoutInspector isKindOfClass:[ASCollectionViewFlowLayoutInspector class]], @"should have a flow layout inspector by default"); +} + +- (void)testThatItDoesNotSetALayoutInspectorForCustomLayouts +{ + UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + XCTAssert(collectionView.layoutInspector == nil, @"should not set a layout delegate for custom layouts"); +} + +- (void)testThatRegisteringASupplementaryNodeStoresItForIntrospection +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + [collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + XCTAssertEqualObjects([collectionView supplementaryNodeKindsInDataController:nil], @[UICollectionElementKindSectionHeader]); +} + +- (void)DISABLED_testCollectionViewController +{ ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; diff --git a/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m b/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m index 02bb2c427e..e08f50d15f 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m @@ -56,9 +56,15 @@ static dispatch_block_t modifyMethodByAddingPrologueBlockAndReturnCleanupBlock(C @interface ASDisplayNodeAppearanceTests : XCTestCase @end +// Conveniences for making nodes named a certain way #define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n -#define DeclareViewNamed(v) UIView *v = [[UIView alloc] init]; v.layer.asyncdisplaykit_name = @#v -#define DeclareLayerNamed(l) CALayer *l = [[CALayer alloc] init]; l.asyncdisplaykit_name = @#l +#define DeclareViewNamed(v) UIView *v = viewWithName(@#v) + +static UIView *viewWithName(NSString *name) { + ASDisplayNode *n = [[ASDisplayNode alloc] init]; + n.name = name; + return n.view; +} @implementation ASDisplayNodeAppearanceTests { diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index 75adeee18b..d617164650 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -17,21 +17,33 @@ #import "UIView+ASConvenience.h" // Conveniences for making nodes named a certain way - #define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n -#define DeclareViewNamed(v) UIView *v = [[UIView alloc] init]; v.layer.asyncdisplaykit_name = @#v -#define DeclareLayerNamed(l) CALayer *l = [[CALayer alloc] init]; l.asyncdisplaykit_name = @#l +#define DeclareViewNamed(v) UIView *v = viewWithName(@#v) +#define DeclareLayerNamed(l) CALayer *l = layerWithName(@#l) + +static UIView *viewWithName(NSString *name) { + ASDisplayNode *n = [[ASDisplayNode alloc] init]; + n.name = name; + return n.view; +} + +static CALayer *layerWithName(NSString *name) { + ASDisplayNode *n = [[ASDisplayNode alloc] init]; + n.layerBacked = YES; + n.name = name; + return n.layer; +} static NSString *orderStringFromSublayers(CALayer *l) { - return [[[l.sublayers valueForKey:@"asyncdisplaykit_name"] allObjects] componentsJoinedByString:@","]; + return [[l.sublayers valueForKey:@"name"] componentsJoinedByString:@","]; } static NSString *orderStringFromSubviews(UIView *v) { - return [[[v.subviews valueForKeyPath:@"layer.asyncdisplaykit_name"] allObjects] componentsJoinedByString:@","]; + return [[v.subviews valueForKey:@"name"] componentsJoinedByString:@","]; } static NSString *orderStringFromSubnodes(ASDisplayNode *n) { - return [[[n.subnodes valueForKey:@"name"] allObjects] componentsJoinedByString:@","]; + return [[n.subnodes valueForKey:@"name"] componentsJoinedByString:@","]; } // Asserts subnode, subview, sublayer order match what you provide here @@ -1375,7 +1387,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point DeclareNodeNamed(b); DeclareNodeNamed(c); DeclareViewNamed(d); - DeclareLayerNamed(e); [parent layer]; diff --git a/AsyncDisplayKitTests/ASStaticLayoutSpecSnapshotTests.m b/AsyncDisplayKitTests/ASStaticLayoutSpecSnapshotTests.m new file mode 100644 index 0000000000..36efb324a3 --- /dev/null +++ b/AsyncDisplayKitTests/ASStaticLayoutSpecSnapshotTests.m @@ -0,0 +1,85 @@ +// +// ASStaticLayoutSpecSnapshotTests.m +// AsyncDisplayKit +// +// Created by Huy Nguyen on 18/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#import "ASStaticLayoutSpec.h" +#import "ASBackgroundLayoutSpec.h" + +@interface ASStaticLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase +@end + +@implementation ASStaticLayoutSpecSnapshotTests + +- (void)setUp +{ + [super setUp]; + self.recordMode = NO; +} + +- (void)testSizingBehaviour +{ + [self testWithSizeRange:ASSizeRangeMake(CGSizeMake(150, 200), CGSizeMake(FLT_MAX, FLT_MAX)) + identifier:@"underflowChildren"]; + [self testWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(50, 100)) + identifier:@"overflowChildren"]; + // Expect the spec to wrap its content because children sizes are between constrained size + [self testWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(FLT_MAX / 2, FLT_MAX / 2)) + identifier:@"wrappedChildren"]; +} + +- (void)testChildrenMeasuredWithAutoMaxSize +{ + ASStaticSizeDisplayNode *firstChild = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + firstChild.layoutPosition = CGPointMake(0, 0); + firstChild.staticSize = CGSizeMake(50, 50); + + ASStaticSizeDisplayNode *secondChild = ASDisplayNodeWithBackgroundColor([UIColor blueColor]); + secondChild.layoutPosition = CGPointMake(10, 60); + secondChild.staticSize = CGSizeMake(100, 100); + + ASSizeRange sizeRange = ASSizeRangeMake(CGSizeMake(10, 10), CGSizeMake(110, 160)); + [self testWithChildren:@[firstChild, secondChild] sizeRange:sizeRange identifier:nil]; + + XCTAssertTrue(ASSizeRangeEqualToSizeRange(firstChild.constrainedSizeForCalculatedLayout, + ASSizeRangeMake(CGSizeZero, sizeRange.max))); + CGSize secondChildMaxSize = CGSizeMake(sizeRange.max.width - secondChild.layoutPosition.x, + sizeRange.max.height - secondChild.layoutPosition.y); + XCTAssertTrue(ASSizeRangeEqualToSizeRange(secondChild.constrainedSizeForCalculatedLayout, + ASSizeRangeMake(CGSizeZero, secondChildMaxSize))); +} + +- (void)testWithSizeRange:(ASSizeRange)sizeRange identifier:(NSString *)identifier +{ + ASDisplayNode *firstChild = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + firstChild.layoutPosition = CGPointMake(0, 0); + firstChild.sizeRange = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(50, 50)); + + + ASDisplayNode *secondChild = ASDisplayNodeWithBackgroundColor([UIColor blueColor]); + secondChild.layoutPosition = CGPointMake(0, 50); + secondChild.sizeRange = ASRelativeSizeRangeMakeWithExactCGSize(CGSizeMake(100, 100)); + + [self testWithChildren:@[firstChild, secondChild] sizeRange:sizeRange identifier:identifier]; +} + +- (void)testWithChildren:(NSArray *)children sizeRange:(ASSizeRange)sizeRange identifier:(NSString *)identifier +{ + ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]); + + NSMutableArray *subnodes = [NSMutableArray arrayWithArray:children]; + [subnodes insertObject:backgroundNode atIndex:0]; + + ASStaticLayoutSpec *staticLayoutSpec = [ASStaticLayoutSpec staticLayoutSpecWithChildren:children]; + ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:staticLayoutSpec + background:backgroundNode]; + + [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +@end diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index a0d2a413b7..b4e137bcd7 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -9,18 +9,44 @@ #import #import "ASTableView.h" +#import "ASTableViewInternal.h" #import "ASDisplayNode+Subclasses.h" +#import "ASChangeSetDataController.h" #define NumberOfSections 10 #define NumberOfRowsPerSection 20 #define NumberOfReloadIterations 50 +@interface ASTestDataController : ASChangeSetDataController +@property (atomic) int numberOfAllNodesRelayouts; +@end + +@implementation ASTestDataController + +- (void)relayoutAllNodes +{ + _numberOfAllNodesRelayouts++; + [super relayoutAllNodes]; +} + +@end + @interface ASTestTableView : ASTableView @property (atomic, copy) void (^willDeallocBlock)(ASTableView *tableView); @end @implementation ASTestTableView +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled +{ + return [super initWithFrame:frame style:style dataControllerClass:[ASTestDataController class] asyncDataFetching:asyncDataFetchingEnabled]; +} + +- (ASTestDataController *)testDataController +{ + return (ASTestDataController *)self.dataController; +} + - (void)dealloc { if (_willDeallocBlock) { @@ -219,7 +245,7 @@ } } -- (void)testRelayoutAllRowsWithNonZeroSizeInitially +- (void)testRelayoutAllNodesWithNonZeroSizeInitially { // Initial width of the table view is non-zero and all nodes are measured with this size. // Any subsequence size change must trigger a relayout. @@ -233,14 +259,14 @@ tableView.asyncDelegate = dataSource; tableView.asyncDataSource = dataSource; + + [tableView layoutIfNeeded]; - // Trigger layout measurement on all nodes - [tableView reloadData]; - - [self triggerSizeChangeAndAssertRelayoutAllRowsForTableView:tableView newSize:tableViewFinalSize]; + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 0); + [self triggerSizeChangeAndAssertRelayoutAllNodesForTableView:tableView newSize:tableViewFinalSize]; } -- (void)testRelayoutAllRowsWithZeroSizeInitially +- (void)testRelayoutAllNodesWithZeroSizeInitially { // Initial width of the table view is 0. The first size change is part of the initial config. // Any subsequence size change after that must trigger a relayout. @@ -258,40 +284,10 @@ [superview addSubview:tableView]; // Width and height are swapped so that a later size change will simulate a rotation tableView.frame = CGRectMake(0, 0, tableViewFinalSize.height, tableViewFinalSize.width); - // Trigger layout measurement on all nodes [tableView layoutIfNeeded]; - [self triggerSizeChangeAndAssertRelayoutAllRowsForTableView:tableView newSize:tableViewFinalSize]; -} - -- (void)triggerSizeChangeAndAssertRelayoutAllRowsForTableView:(ASTableView *)tableView newSize:(CGSize)newSize -{ - XCTestExpectation *nodesMeasuredUsingNewConstrainedSizeExpectation = [self expectationWithDescription:@"nodesMeasuredUsingNewConstrainedSize"]; - - [tableView beginUpdates]; - - CGRect frame = tableView.frame; - frame.size = newSize; - tableView.frame = frame; - [tableView layoutIfNeeded]; - - [tableView endUpdatesAnimated:NO completion:^(BOOL completed) { - for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; - ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; - XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1); - XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, newSize.width); - } - } - [nodesMeasuredUsingNewConstrainedSizeExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { - if (error) { - XCTFail(@"Expectation failed: %@", error); - } - }]; + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 0); + [self triggerSizeChangeAndAssertRelayoutAllNodesForTableView:tableView newSize:tableViewFinalSize]; } - (void)testRelayoutVisibleRowsWhenEditingModeIsChanged @@ -304,24 +300,8 @@ tableView.asyncDelegate = dataSource; tableView.asyncDataSource = dataSource; - - XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; - [tableView reloadDataWithCompletion:^{ - for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; - ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; - XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); - XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); - } - } - [reloadDataExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { - if (error) { - XCTFail(@"Expectation failed: %@", error); - } - }]; + + [self triggerFirstLayoutMeasurementForTableView:tableView]; NSArray *visibleNodes = [tableView visibleNodes]; XCTAssertGreaterThan(visibleNodes.count, 0); @@ -390,23 +370,7 @@ tableView.asyncDelegate = dataSource; tableView.asyncDataSource = dataSource; - XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; - [tableView reloadDataWithCompletion:^{ - for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; - ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; - XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); - XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); - } - } - [reloadDataExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { - if (error) { - XCTFail(@"Expectation failed: %@", error); - } - }]; + [self triggerFirstLayoutMeasurementForTableView:tableView]; // Cause table view to enter editing mode and then scroll to the bottom. // The last node should be re-measured on main thread with the new (smaller) content view width. @@ -451,4 +415,55 @@ }]; } +- (void)triggerFirstLayoutMeasurementForTableView:(ASTableView *)tableView{ + XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; + [tableView reloadDataWithCompletion:^{ + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < NumberOfRowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableView.frame.size.width); + } + } + [reloadDataExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + +- (void)triggerSizeChangeAndAssertRelayoutAllNodesForTableView:(ASTestTableView *)tableView newSize:(CGSize)newSize +{ + XCTestExpectation *nodesMeasuredUsingNewConstrainedSizeExpectation = [self expectationWithDescription:@"nodesMeasuredUsingNewConstrainedSize"]; + + [tableView beginUpdates]; + + CGRect frame = tableView.frame; + frame.size = newSize; + tableView.frame = frame; + [tableView layoutIfNeeded]; + + [tableView endUpdatesAnimated:NO completion:^(BOOL completed) { + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 1); + + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < NumberOfRowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertLessThanOrEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, newSize.width); + } + } + [nodesMeasuredUsingNewConstrainedSizeExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + @end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testChildrenMeasuredWithAutoMaxSize@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testChildrenMeasuredWithAutoMaxSize@2x.png new file mode 100644 index 0000000000..2b60cf2bcf Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testChildrenMeasuredWithAutoMaxSize@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_overflowChildren@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_overflowChildren@2x.png new file mode 100644 index 0000000000..1687dc24f6 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_overflowChildren@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_underflowChildren@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_underflowChildren@2x.png new file mode 100644 index 0000000000..2233813979 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_underflowChildren@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_wrappedChildren@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_wrappedChildren@2x.png new file mode 100644 index 0000000000..3d76853c01 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStaticLayoutSpecSnapshotTests/testSizingBehaviour_wrappedChildren@2x.png differ diff --git a/ObjectiveC.gcno b/ObjectiveC.gcno deleted file mode 100644 index 05bc771aa7..0000000000 Binary files a/ObjectiveC.gcno and /dev/null differ diff --git a/QuartzCore.gcno b/QuartzCore.gcno deleted file mode 100644 index 05bc771aa7..0000000000 Binary files a/QuartzCore.gcno and /dev/null differ diff --git a/README.md b/README.md index 926873e09a..2a249299a1 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ to implement node hierarchies or custom drawing. * Read the [Getting Started guide](http://asyncdisplaykit.org/guide/) * Get the [sample projects](https://github.com/facebook/AsyncDisplayKit/tree/master/examples) * Browse the [API reference](http://asyncdisplaykit.org/appledoc/) -* Watch the [NSLondon talk](http://vimeo.com/103589245) +* Watch the [NSLondon talk](http://vimeo.com/103589245) or the [NSSpain talk](https://www.youtube.com/watch?v=RY_X7l1g79Q) ## Testing diff --git a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj index 7f64fc5596..959990a70e 100644 --- a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */; settings = {ASSET_TAGS = (); }; }; + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; settings = {ASSET_TAGS = (); }; }; AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; @@ -16,6 +18,9 @@ /* Begin PBXFileReference section */ 2DBAEE96397BB913350C4530 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SupplementaryNode.h; sourceTree = ""; }; + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SupplementaryNode.m; sourceTree = ""; }; + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Launchboard.storyboard; sourceTree = ""; }; AC3C4A5E1A11F47200143C57 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; AC3C4A621A11F47200143C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AC3C4A631A11F47200143C57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -76,6 +81,8 @@ AC3C4A691A11F47200143C57 /* ViewController.m */, AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, AC3C4A611A11F47200143C57 /* Supporting Files */, + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */, + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */, ); indentWidth = 2; path = Sample; @@ -88,6 +95,7 @@ children = ( AC3C4A621A11F47200143C57 /* Info.plist */, AC3C4A631A11F47200143C57 /* main.m */, + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */, ); name = "Supporting Files"; sourceTree = ""; @@ -159,6 +167,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */, AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -204,6 +213,7 @@ buildActionMask = 2147483647; files = ( AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */, AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, AC3C4A641A11F47200143C57 /* main.m in Sources */, ); diff --git a/examples/ASCollectionView/Sample/Info.plist b/examples/ASCollectionView/Sample/Info.plist index a3664b0b15..eeb71a8d35 100644 --- a/examples/ASCollectionView/Sample/Info.plist +++ b/examples/ASCollectionView/Sample/Info.plist @@ -26,6 +26,8 @@ 1 LSRequiresIPhoneOS + UILaunchStoryboardName + Launchboard UIRequiredDeviceCapabilities armv7 diff --git a/examples/ASCollectionView/Sample/Launchboard.storyboard b/examples/ASCollectionView/Sample/Launchboard.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/examples/ASCollectionView/Sample/Launchboard.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/ASCollectionView/Sample/SupplementaryNode.h b/examples/ASCollectionView/Sample/SupplementaryNode.h new file mode 100644 index 0000000000..f75c929684 --- /dev/null +++ b/examples/ASCollectionView/Sample/SupplementaryNode.h @@ -0,0 +1,18 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface SupplementaryNode : ASCellNode + +- (instancetype)initWithText:(NSString *)text; + +@end diff --git a/examples/ASCollectionView/Sample/SupplementaryNode.m b/examples/ASCollectionView/Sample/SupplementaryNode.m new file mode 100644 index 0000000000..ca5579e9a6 --- /dev/null +++ b/examples/ASCollectionView/Sample/SupplementaryNode.m @@ -0,0 +1,55 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "SupplementaryNode.h" + +#import +#import +#import + +static CGFloat kInsets = 15.0; + +@implementation SupplementaryNode { + ASTextNode *_textNode; +} + +- (instancetype)initWithText:(NSString *)text +{ + self = [super init]; + if (self != nil) { + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedString = [[NSAttributedString alloc] initWithString:text + attributes:[self textAttributes]]; + [self addSubnode:_textNode]; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *center = [[ASCenterLayoutSpec alloc] init]; + center.centeringOptions = ASCenterLayoutSpecCenteringXY; + center.child = _textNode; + UIEdgeInsets insets = UIEdgeInsetsMake(kInsets, kInsets, kInsets, kInsets); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:center]; +} + +#pragma mark - Text Formatting + +- (NSDictionary *)textAttributes +{ + return @{ + NSFontAttributeName: [UIFont systemFontOfSize:18.0], + NSForegroundColorAttributeName: [UIColor whiteColor], + }; +} + +@end diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index f87e26f9d7..01350da1a3 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -12,8 +12,9 @@ #import "ViewController.h" #import +#import "SupplementaryNode.h" -@interface ViewController () +@interface ViewController () { ASCollectionView *_collectionView; } @@ -32,13 +33,17 @@ return nil; UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.headerReferenceSize = CGSizeMake(50.0, 50.0); + layout.footerReferenceSize = CGSizeMake(50.0, 50.0); _collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:YES]; _collectionView.asyncDataSource = self; _collectionView.asyncDelegate = self; _collectionView.backgroundColor = [UIColor whiteColor]; + [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; + return self; } @@ -73,9 +78,26 @@ return node; } +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + NSString *text = [kind isEqualToString:UICollectionElementKindSectionHeader] ? @"Header" : @"Footer"; + SupplementaryNode *node = [[SupplementaryNode alloc] initWithText:text]; + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + node.backgroundColor = [UIColor blueColor]; + } else { + node.backgroundColor = [UIColor redColor]; + } + return node; +} + - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return 300; + return 10; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 100; } - (void)collectionViewLockDataSource:(ASCollectionView *)collectionView @@ -95,7 +117,7 @@ [context completeBatchFetching:YES]; } -- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { +- (UIEdgeInsets)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { return UIEdgeInsetsMake(20.0, 20.0, 20.0, 20.0); } diff --git a/examples/Kittens/Sample/KittenNode.mm b/examples/Kittens/Sample/KittenNode.mm index 847a2629c7..f6035e03be 100644 --- a/examples/Kittens/Sample/KittenNode.mm +++ b/examples/Kittens/Sample/KittenNode.mm @@ -185,13 +185,22 @@ static const CGFloat kInnerPadding = 10.0f; - (void)toggleImageEnlargement { _isImageEnlarged = !_isImageEnlarged; - [self setNeedsLayout]; } - (void)toggleNodesSwap { _swappedTextAndImage = !_swappedTextAndImage; - [self setNeedsLayout]; + + [UIView animateWithDuration:0.15 animations:^{ + self.alpha = 0; + } completion:^(BOOL finished) { + [self setNeedsLayout]; + [self.view layoutIfNeeded]; + + [UIView animateWithDuration:0.15 animations:^{ + self.alpha = 1; + }]; + }]; } @end diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index 7989fe15ce..41630435aa 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -118,11 +118,10 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [_tableView deselectRowAtIndexPath:indexPath animated:YES]; - [_tableView beginUpdates]; // Assume only kitten nodes are selectable (see -tableView:shouldHighlightRowAtIndexPath:). KittenNode *node = (KittenNode *)[_tableView nodeForRowAtIndexPath:indexPath]; [node toggleImageEnlargement]; - [_tableView endUpdates]; + [_tableView relayoutRowAtIndexPath:indexPath withRowAnimation:UITableViewRowAnimationAutomatic]; } - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath diff --git a/examples/SynchronousKittens/Default-568h@2x.png b/examples/SynchronousKittens/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/examples/SynchronousKittens/Default-568h@2x.png differ diff --git a/examples/SynchronousKittens/Default-667h@2x.png b/examples/SynchronousKittens/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/examples/SynchronousKittens/Default-667h@2x.png differ diff --git a/examples/SynchronousKittens/Default-736h@3x.png b/examples/SynchronousKittens/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/examples/SynchronousKittens/Default-736h@3x.png differ diff --git a/examples/SynchronousKittens/Podfile b/examples/SynchronousKittens/Podfile new file mode 100644 index 0000000000..6c012e3c04 --- /dev/null +++ b/examples/SynchronousKittens/Podfile @@ -0,0 +1,3 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '8.0' +pod 'AsyncDisplayKit', :path => '../..' diff --git a/examples/SynchronousKittens/Sample.xcodeproj/project.pbxproj b/examples/SynchronousKittens/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a8ef05da55 --- /dev/null +++ b/examples/SynchronousKittens/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,377 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 05561CF919D4E77700CBA93C /* BlurbNode.m */; }; + 05561CFD19D4F94A00CBA93C /* KittenNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05561CFC19D4F94A00CBA93C /* KittenNode.mm */; }; + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 3EC0CDCBA10D483D9F386E5E /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */; }; + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05561CF819D4E77700CBA93C /* BlurbNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlurbNode.h; sourceTree = ""; }; + 05561CF919D4E77700CBA93C /* BlurbNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlurbNode.m; sourceTree = ""; }; + 05561CFB19D4F94A00CBA93C /* KittenNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KittenNode.h; sourceTree = ""; }; + 05561CFC19D4F94A00CBA93C /* KittenNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KittenNode.mm; sourceTree = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EC0CDCBA10D483D9F386E5E /* libPods.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05561CFB19D4F94A00CBA93C /* KittenNode.h */, + 05561CFC19D4F94A00CBA93C /* KittenNode.mm */, + 05561CF819D4E77700CBA93C /* BlurbNode.h */, + 05561CF919D4E77700CBA93C /* BlurbNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */, + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + ACCB3408566E7626721EF2D5 /* Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + ACCB3408566E7626721EF2D5 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05561CFD19D4F94A00CBA93C /* KittenNode.mm in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05561CFA19D4E77700CBA93C /* BlurbNode.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/examples/SynchronousKittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/SynchronousKittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/examples/SynchronousKittens/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/SynchronousKittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/SynchronousKittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/examples/SynchronousKittens/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata b/examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..d98549fd35 --- /dev/null +++ b/examples/SynchronousKittens/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/SynchronousKittens/Sample/AppDelegate.h b/examples/SynchronousKittens/Sample/AppDelegate.h new file mode 100644 index 0000000000..85855277e9 --- /dev/null +++ b/examples/SynchronousKittens/Sample/AppDelegate.h @@ -0,0 +1,20 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/examples/SynchronousKittens/Sample/AppDelegate.m b/examples/SynchronousKittens/Sample/AppDelegate.m new file mode 100644 index 0000000000..1dea563b77 --- /dev/null +++ b/examples/SynchronousKittens/Sample/AppDelegate.m @@ -0,0 +1,27 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/examples/SynchronousKittens/Sample/BlurbNode.h b/examples/SynchronousKittens/Sample/BlurbNode.h new file mode 100644 index 0000000000..57d8e30787 --- /dev/null +++ b/examples/SynchronousKittens/Sample/BlurbNode.h @@ -0,0 +1,19 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +/** + * Simple node that displays a placekitten.com attribution. + */ +@interface BlurbNode : ASCellNode + +@end diff --git a/examples/SynchronousKittens/Sample/BlurbNode.m b/examples/SynchronousKittens/Sample/BlurbNode.m new file mode 100644 index 0000000000..693ec0cd03 --- /dev/null +++ b/examples/SynchronousKittens/Sample/BlurbNode.m @@ -0,0 +1,122 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BlurbNode.h" +#import "AppDelegate.h" + +#import +#import + +#import +#import + +static CGFloat kTextPadding = 10.0f; +static NSString *kLinkAttributeName = @"PlaceKittenNodeLinkAttributeName"; + +@interface BlurbNode () +{ + ASTextNode *_textNode; +} + +@end + + +@implementation BlurbNode + +#pragma mark - +#pragma mark ASCellNode. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // create a text node + _textNode = [[ASTextNode alloc] init]; + + // configure the node to support tappable links + _textNode.delegate = self; + _textNode.userInteractionEnabled = YES; + _textNode.linkAttributeNames = @[ kLinkAttributeName ]; + + // generate an attributed string using the custom link attribute specified above + NSString *blurb = @"kittens courtesy placekitten.com \U0001F638"; + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:blurb]; + [string addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"HelveticaNeue-Light" size:16.0f] range:NSMakeRange(0, blurb.length)]; + [string addAttributes:@{ + kLinkAttributeName: [NSURL URLWithString:@"http://placekitten.com/"], + NSForegroundColorAttributeName: [UIColor grayColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDot), + } + range:[blurb rangeOfString:@"placekitten.com"]]; + _textNode.attributedString = string; + + // add it as a subnode, and we're done + [self addSubnode:_textNode]; + + return self; +} + +- (void)didLoad +{ + // enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h + self.layer.as_allowsHighlightDrawing = YES; + + [super didLoad]; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringX; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; + centerSpec.child = _textNode; + + UIEdgeInsets padding =UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + // called on a background thread. custom nodes must call -measure: on their subnodes in -calculateSizeThatFits: + CGSize measuredSize = [_textNode measure:CGSizeMake(constrainedSize.width - 2 * kTextPadding, + constrainedSize.height - 2 * kTextPadding)]; + return CGSizeMake(constrainedSize.width, measuredSize.height + 2 * kTextPadding); +} + +- (void)layout +{ + // called on the main thread. we'll use the stashed size from above, instead of blocking on text sizing + CGSize textNodeSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(roundf((self.calculatedSize.width - textNodeSize.width) / 2.0f), + kTextPadding, + textNodeSize.width, + textNodeSize.height); +} +#endif + +#pragma mark - +#pragma mark ASTextNodeDelegate methods. + +- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point +{ + // opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad + return YES; +} + +- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + // the node tapped a link, open it + [[UIApplication sharedApplication] openURL:URL]; +} + +@end diff --git a/examples/SynchronousKittens/Sample/Info.plist b/examples/SynchronousKittens/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/examples/SynchronousKittens/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/SynchronousKittens/Sample/KittenNode.h b/examples/SynchronousKittens/Sample/KittenNode.h new file mode 100644 index 0000000000..3cc23d5a44 --- /dev/null +++ b/examples/SynchronousKittens/Sample/KittenNode.h @@ -0,0 +1,24 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +/** + * Social media-style node that displays a kitten picture and a random length + * of lorem ipsum text. Uses a placekitten.com kitten of the specified size. + */ +@interface KittenNode : ASCellNode + +- (instancetype)initWithKittenOfSize:(CGSize)size; + +- (void)toggleImageEnlargement; + +@end diff --git a/examples/SynchronousKittens/Sample/KittenNode.mm b/examples/SynchronousKittens/Sample/KittenNode.mm new file mode 100644 index 0000000000..847a2629c7 --- /dev/null +++ b/examples/SynchronousKittens/Sample/KittenNode.mm @@ -0,0 +1,197 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "KittenNode.h" +#import "AppDelegate.h" + +#import + +#import +#import + +static const CGFloat kImageSize = 80.0f; +static const CGFloat kOuterPadding = 16.0f; +static const CGFloat kInnerPadding = 10.0f; + + +@interface KittenNode () +{ + CGSize _kittenSize; + + ASNetworkImageNode *_imageNode; + ASTextNode *_textNode; + ASDisplayNode *_divider; + BOOL _isImageEnlarged; + BOOL _swappedTextAndImage; +} + +@end + + +@implementation KittenNode + +// lorem ipsum text courtesy https://kittyipsum.com/ <3 ++ (NSArray *)placeholders +{ + static NSArray *placeholders = nil; + + static dispatch_once_t once; + dispatch_once(&once, ^{ + placeholders = @[ + @"Kitty ipsum dolor sit amet, purr sleep on your face lay down in your way biting, sniff tincidunt a etiam fluffy fur judging you stuck in a tree kittens.", + @"Lick tincidunt a biting eat the grass, egestas enim ut lick leap puking climb the curtains lick.", + @"Lick quis nunc toss the mousie vel, tortor pellentesque sunbathe orci turpis non tail flick suscipit sleep in the sink.", + @"Orci turpis litter box et stuck in a tree, egestas ac tempus et aliquam elit.", + @"Hairball iaculis dolor dolor neque, nibh adipiscing vehicula egestas dolor aliquam.", + @"Sunbathe fluffy fur tortor faucibus pharetra jump, enim jump on the table I don't like that food catnip toss the mousie scratched.", + @"Quis nunc nam sleep in the sink quis nunc purr faucibus, chase the red dot consectetur bat sagittis.", + @"Lick tail flick jump on the table stretching purr amet, rhoncus scratched jump on the table run.", + @"Suspendisse aliquam vulputate feed me sleep on your keyboard, rip the couch faucibus sleep on your keyboard tristique give me fish dolor.", + @"Rip the couch hiss attack your ankles biting pellentesque puking, enim suspendisse enim mauris a.", + @"Sollicitudin iaculis vestibulum toss the mousie biting attack your ankles, puking nunc jump adipiscing in viverra.", + @"Nam zzz amet neque, bat tincidunt a iaculis sniff hiss bibendum leap nibh.", + @"Chase the red dot enim puking chuf, tristique et egestas sniff sollicitudin pharetra enim ut mauris a.", + @"Sagittis scratched et lick, hairball leap attack adipiscing catnip tail flick iaculis lick.", + @"Neque neque sleep in the sink neque sleep on your face, climb the curtains chuf tail flick sniff tortor non.", + @"Ac etiam kittens claw toss the mousie jump, pellentesque rhoncus litter box give me fish adipiscing mauris a.", + @"Pharetra egestas sunbathe faucibus ac fluffy fur, hiss feed me give me fish accumsan.", + @"Tortor leap tristique accumsan rutrum sleep in the sink, amet sollicitudin adipiscing dolor chase the red dot.", + @"Knock over the lamp pharetra vehicula sleep on your face rhoncus, jump elit cras nec quis quis nunc nam.", + @"Sollicitudin feed me et ac in viverra catnip, nunc eat I don't like that food iaculis give me fish.", + ]; + }); + + return placeholders; +} + +- (instancetype)initWithKittenOfSize:(CGSize)size +{ + if (!(self = [super init])) + return nil; + + _kittenSize = size; + + // kitten image, with a solid background colour serving as placeholder + _imageNode = [[ASNetworkImageNode alloc] init]; + _imageNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + _imageNode.URL = [NSURL URLWithString:[NSString stringWithFormat:@"https://placekitten.com/%zd/%zd", + (NSInteger)roundl(_kittenSize.width), + (NSInteger)roundl(_kittenSize.height)]]; +// _imageNode.contentMode = UIViewContentModeCenter; + [_imageNode addTarget:self action:@selector(toggleNodesSwap) forControlEvents:ASControlNodeEventTouchUpInside]; + [self addSubnode:_imageNode]; + + // lorem ipsum text, plus some nice styling + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedString = [[NSAttributedString alloc] initWithString:[self kittyIpsum] + attributes:[self textStyle]]; + [self addSubnode:_textNode]; + + // hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + _divider.backgroundColor = [UIColor lightGrayColor]; + [self addSubnode:_divider]; + + return self; +} + +- (NSString *)kittyIpsum +{ + NSArray *placeholders = [KittenNode placeholders]; + u_int32_t ipsumCount = (u_int32_t)[placeholders count]; + u_int32_t location = arc4random_uniform(ipsumCount); + u_int32_t length = arc4random_uniform(ipsumCount - location); + + NSMutableString *string = [placeholders[location] mutableCopy]; + for (u_int32_t i = location + 1; i < location + length; i++) { + [string appendString:(i % 2 == 0) ? @"\n" : @" "]; + [string appendString:placeholders[i]]; + } + + return string; +} + +- (NSDictionary *)textStyle +{ + UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:12.0f]; + + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + style.paragraphSpacing = 0.5 * font.lineHeight; + style.hyphenationFactor = 1.0; + + return @{ NSFontAttributeName: font, + NSParagraphStyleAttributeName: style }; +} + +#if UseAutomaticLayout +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + _imageNode.preferredFrameSize = _isImageEnlarged ? CGSizeMake(2.0 * kImageSize, 2.0 * kImageSize) : CGSizeMake(kImageSize, kImageSize); + _textNode.flexShrink = YES; + + ASStackLayoutSpec *stackSpec = [[ASStackLayoutSpec alloc] init]; + stackSpec.direction = ASStackLayoutDirectionHorizontal; + stackSpec.spacing = kInnerPadding; + [stackSpec setChildren:!_swappedTextAndImage ? @[_imageNode, _textNode] : @[_textNode, _imageNode]]; + + ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; + insetSpec.insets = UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding); + insetSpec.child = stackSpec; + + return insetSpec; +} + +// With box model, you don't need to override this method, unless you want to add custom logic. +- (void)layout +{ + [super layout]; + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} +#else +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + CGSize imageSize = CGSizeMake(kImageSize, kImageSize); + CGSize textSize = [_textNode measure:CGSizeMake(constrainedSize.width - kImageSize - 2 * kOuterPadding - kInnerPadding, + constrainedSize.height)]; + + // ensure there's room for the text + CGFloat requiredHeight = MAX(textSize.height, imageSize.height); + return CGSizeMake(constrainedSize.width, requiredHeight + 2 * kOuterPadding); +} + +- (void)layout +{ + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); + + _imageNode.frame = CGRectMake(kOuterPadding, kOuterPadding, kImageSize, kImageSize); + + CGSize textSize = _textNode.calculatedSize; + _textNode.frame = CGRectMake(kOuterPadding + kImageSize + kInnerPadding, kOuterPadding, textSize.width, textSize.height); +} +#endif + +- (void)toggleImageEnlargement +{ + _isImageEnlarged = !_isImageEnlarged; + [self setNeedsLayout]; +} + +- (void)toggleNodesSwap +{ + _swappedTextAndImage = !_swappedTextAndImage; + [self setNeedsLayout]; +} + +@end diff --git a/examples/SynchronousKittens/Sample/ViewController.h b/examples/SynchronousKittens/Sample/ViewController.h new file mode 100644 index 0000000000..d0e9200d88 --- /dev/null +++ b/examples/SynchronousKittens/Sample/ViewController.h @@ -0,0 +1,16 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface ViewController : UIViewController + +@end diff --git a/examples/SynchronousKittens/Sample/ViewController.m b/examples/SynchronousKittens/Sample/ViewController.m new file mode 100644 index 0000000000..5d0b594879 --- /dev/null +++ b/examples/SynchronousKittens/Sample/ViewController.m @@ -0,0 +1,216 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "ViewController.h" + +#import +#import + +#import "BlurbNode.h" +#import "KittenNode.h" + + +static const NSInteger kLitterSize = 20; // intial number of kitten cells in ASTableView +static const NSInteger kLitterBatchSize = 10; // number of kitten cells to add to ASTableView +static const NSInteger kMaxLitterSize = 100; // max number of kitten cells allowed in ASTableView + +@interface ViewController () +{ + ASTableView *_tableView; + + // array of boxed CGSizes corresponding to placekitten.com kittens + NSMutableArray *_kittenDataSource; + + BOOL _dataSourceLocked; + NSIndexPath *_blurbNodeIndexPath; +} + +@property (nonatomic, strong) NSMutableArray *kittenDataSource; +@property (atomic, assign) BOOL dataSourceLocked; + +@end + + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain asyncDataFetching:YES]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator + _tableView.asyncDataSource = self; + _tableView.asyncDelegate = self; + + // populate our "data source" with some random kittens + _kittenDataSource = [self createLitterWithSize:kLitterSize]; + + _blurbNodeIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + self.title = @"Kittens"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit + target:self + action:@selector(toggleEditingMode)]; + + return self; +} + +- (NSMutableArray *)createLitterWithSize:(NSInteger)litterSize +{ + NSMutableArray *kittens = [NSMutableArray arrayWithCapacity:litterSize]; + for (NSInteger i = 0; i < litterSize; i++) { + + // placekitten.com will return the same kitten picture if the same pixel height & width are requested, + // so generate kittens with different width & height values. + u_int32_t deltaX = arc4random_uniform(10) - 5; + u_int32_t deltaY = arc4random_uniform(10) - 5; + CGSize size = CGSizeMake(350 + 2 * deltaX, 350 + 4 * deltaY); + + [kittens addObject:[NSValue valueWithCGSize:size]]; + } + return kittens; +} + +- (void)setKittenDataSource:(NSMutableArray *)kittenDataSource { + ASDisplayNodeAssert(!self.dataSourceLocked, @"Could not update data source when it is locked !"); + + _kittenDataSource = kittenDataSource; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_tableView]; + + [_tableView reloadDataImmediately]; +} + +- (void)viewWillLayoutSubviews +{ + _tableView.frame = self.view.bounds; +} + +- (BOOL)prefersStatusBarHidden +{ + return YES; +} + +- (void)toggleEditingMode +{ + [_tableView setEditing:!_tableView.editing animated:YES]; +} + + +#pragma mark - +#pragma mark ASTableView. + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [_tableView deselectRowAtIndexPath:indexPath animated:YES]; + [_tableView beginUpdates]; + // Assume only kitten nodes are selectable (see -tableView:shouldHighlightRowAtIndexPath:). + KittenNode *node = (KittenNode *)[_tableView nodeForRowAtIndexPath:indexPath]; + [node toggleImageEnlargement]; + [_tableView endUpdates]; +} + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // special-case the first row + if ([_blurbNodeIndexPath compare:indexPath] == NSOrderedSame) { + BlurbNode *node = [[BlurbNode alloc] init]; + return node; + } + + NSValue *size = _kittenDataSource[indexPath.row - 1]; + KittenNode *node = [[KittenNode alloc] initWithKittenOfSize:size.CGSizeValue]; + return node; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + // blurb node + kLitterSize kitties + return 1 + _kittenDataSource.count; +} + +- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable selection for kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableViewLockDataSource:(ASTableView *)tableView +{ + self.dataSourceLocked = YES; +} + +- (void)tableViewUnlockDataSource:(ASTableView *)tableView +{ + self.dataSourceLocked = NO; +} + +- (BOOL)shouldBatchFetchForTableView:(UITableView *)tableView +{ + return _kittenDataSource.count < kMaxLitterSize; +} + +- (void)tableView:(UITableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + NSLog(@"adding kitties"); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + sleep(1); + dispatch_async(dispatch_get_main_queue(), ^{ + + // populate a new array of random-sized kittens + NSArray *moarKittens = [self createLitterWithSize:kLitterBatchSize]; + + NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; + + // find number of kittens in the data source and create their indexPaths + NSInteger existingRows = _kittenDataSource.count + 1; + + for (NSInteger i = 0; i < moarKittens.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:existingRows + i inSection:0]]; + } + + // add new kittens to the data source & notify table of new indexpaths + [_kittenDataSource addObjectsFromArray:moarKittens]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + + [context completeBatchFetching:YES]; + + NSLog(@"kittens added"); + }); + }); +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable editing for Kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + // Assume only kitten nodes are editable (see -tableView:canEditRowAtIndexPath:). + [_kittenDataSource removeObjectAtIndex:indexPath.row - 1]; + [_tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } +} + +@end diff --git a/examples/SynchronousKittens/Sample/main.m b/examples/SynchronousKittens/Sample/main.m new file mode 100644 index 0000000000..ae9488711c --- /dev/null +++ b/examples/SynchronousKittens/Sample/main.m @@ -0,0 +1,20 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +}