From 38aac9d0191fa96b401a1e31ce77f61a9371a11e Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Mon, 30 Jan 2017 11:16:59 -0800 Subject: [PATCH] IGListKit Support II: Electric Boogaloo (#2942) * Reimplement IGListKit support in a cleaner way * Rename and fix some stuff * Fix supplementaries more * Update docs * Update test * Cleanup minor things * Tweak it * Indentation * Remove irrelevant changes * Break out cell into its own file * Fix indentation * Address feedback --- .travis.yml | 1 + .../ASDKListKit.xcodeproj/project.pbxproj | 394 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../contents.xcworkspacedata | 10 + .../ASListKitTestAdapterDataSource.h | 16 + .../ASListKitTestAdapterDataSource.m | 30 ++ ASDKListKit/ASDKListKitTests/ASListKitTests.m | 110 +++++ .../ASDKListKitTests/ASListTestCellNode.h | 13 + .../ASDKListKitTests/ASListTestCellNode.m | 13 + .../ASDKListKitTests/ASListTestObject.h | 22 + .../ASDKListKitTests/ASListTestObject.m | 49 +++ .../ASDKListKitTests/ASListTestSection.h | 18 + .../ASDKListKitTests/ASListTestSection.m | 58 +++ .../ASListTestSupplementaryNode.h | 13 + .../ASListTestSupplementaryNode.m | 13 + .../ASListTestSupplementarySource.h | 20 + .../ASListTestSupplementarySource.m | 24 ++ .../ASDKListKitTests/ASXCTExtensions.h | 36 ++ ASDKListKit/ASDKListKitTests/Info.plist | 22 + ASDKListKit/Podfile | 8 + AsyncDisplayKit.podspec | 6 + AsyncDisplayKit.xcodeproj/project.pbxproj | 72 +++- AsyncDisplayKit/ASCollectionNode.h | 10 + AsyncDisplayKit/ASCollectionNode.mm | 1 + AsyncDisplayKit/ASCollectionView.mm | 147 +++---- AsyncDisplayKit/ASCollectionViewProtocols.h | 2 +- AsyncDisplayKit/ASDisplayNode.mm | 11 +- .../ASIGListKitMethodImplementations.h | 66 +++ AsyncDisplayKit/ASSectionController.h | 69 +++ AsyncDisplayKit/ASSupplementaryNodeSource.h | 40 ++ AsyncDisplayKit/AsyncDisplayKit-Prefix.pch | 4 + AsyncDisplayKit/AsyncDisplayKit.h | 7 + .../Details/ASCollectionDataController.h | 2 +- .../Details/ASCollectionDataController.mm | 60 ++- .../ASCollectionViewFlowLayoutInspector.m | 22 +- .../Details/ASCollectionViewLayoutInspector.m | 1 + AsyncDisplayKit/Details/ASDataController.mm | 7 + .../Details/NSIndexSet+ASHelpers.h | 3 + .../Details/NSIndexSet+ASHelpers.m | 9 + .../Details/_ASCollectionViewCell.h | 17 + .../Details/_ASCollectionViewCell.m | 74 ++++ .../IGListAdapter+AsyncDisplayKit.h | 34 ++ .../IGListAdapter+AsyncDisplayKit.m | 45 ++ .../Private/ASCollectionInteropProtocols.h | 34 ++ .../Private/ASDataController+Subclasses.h | 23 +- .../Private/ASIGListAdapterBasedDataSource.h | 22 + .../Private/ASIGListAdapterBasedDataSource.m | 315 ++++++++++++++ AsyncDisplayKitTests/ASCollectionViewTests.mm | 4 +- AsyncDisplayKitTests/ASDisplayNodeTests.m | 9 +- build.sh | 15 +- .../ASCollectionView/Sample/ViewController.m | 2 +- examples/ASDKgram/Podfile | 5 +- .../ASDKgram/Sample.xcodeproj/project.pbxproj | 40 ++ .../contents.xcworkspacedata | 10 + .../Sample/ASCollectionSectionController.h | 28 ++ .../Sample/ASCollectionSectionController.m | 79 ++++ examples/ASDKgram/Sample/AppDelegate.m | 15 +- examples/ASDKgram/Sample/FeedHeaderNode.h | 13 + examples/ASDKgram/Sample/FeedHeaderNode.m | 35 ++ .../Sample/PhotoFeedListKitViewController.h | 14 + .../Sample/PhotoFeedListKitViewController.m | 106 +++++ examples/ASDKgram/Sample/PhotoFeedModel.h | 5 +- examples/ASDKgram/Sample/PhotoFeedModel.m | 29 +- .../Sample/PhotoFeedSectionController.h | 24 ++ .../Sample/PhotoFeedSectionController.m | 122 ++++++ examples/ASDKgram/Sample/PhotoModel.h | 4 +- examples/ASDKgram/Sample/PhotoModel.m | 18 +- .../Sample/RefreshingSectionControllerType.h | 19 + examples/ASDKgram/Sample/TailLoadingNode.h | 17 + examples/ASDKgram/Sample/TailLoadingNode.m | 35 ++ 70 files changed, 2446 insertions(+), 182 deletions(-) create mode 100644 ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj create mode 100644 ASDKListKit/ASDKListKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ASDKListKit/ASDKListKit.xcworkspace/contents.xcworkspacedata create mode 100644 ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.h create mode 100644 ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.m create mode 100644 ASDKListKit/ASDKListKitTests/ASListKitTests.m create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestCellNode.h create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestCellNode.m create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestObject.h create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestObject.m create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestSection.h create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestSection.m create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.h create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.m create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.h create mode 100644 ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.m create mode 100644 ASDKListKit/ASDKListKitTests/ASXCTExtensions.h create mode 100644 ASDKListKit/ASDKListKitTests/Info.plist create mode 100644 ASDKListKit/Podfile create mode 100644 AsyncDisplayKit/ASIGListKitMethodImplementations.h create mode 100644 AsyncDisplayKit/ASSectionController.h create mode 100644 AsyncDisplayKit/ASSupplementaryNodeSource.h create mode 100644 AsyncDisplayKit/Details/_ASCollectionViewCell.h create mode 100644 AsyncDisplayKit/Details/_ASCollectionViewCell.m create mode 100644 AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.h create mode 100644 AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.m create mode 100644 AsyncDisplayKit/Private/ASCollectionInteropProtocols.h create mode 100644 AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.h create mode 100644 AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.m create mode 100644 examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata create mode 100644 examples/ASDKgram/Sample/ASCollectionSectionController.h create mode 100644 examples/ASDKgram/Sample/ASCollectionSectionController.m create mode 100644 examples/ASDKgram/Sample/FeedHeaderNode.h create mode 100644 examples/ASDKgram/Sample/FeedHeaderNode.m create mode 100644 examples/ASDKgram/Sample/PhotoFeedListKitViewController.h create mode 100644 examples/ASDKgram/Sample/PhotoFeedListKitViewController.m create mode 100644 examples/ASDKgram/Sample/PhotoFeedSectionController.h create mode 100644 examples/ASDKgram/Sample/PhotoFeedSectionController.m create mode 100644 examples/ASDKgram/Sample/RefreshingSectionControllerType.h create mode 100644 examples/ASDKgram/Sample/TailLoadingNode.h create mode 100644 examples/ASDKgram/Sample/TailLoadingNode.m diff --git a/.travis.yml b/.travis.yml index 7502716d88..b493cff43c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_install: install: echo "<3" env: - MODE=tests + - MODE=tests_listkit - MODE=examples-pt1 - MODE=examples-pt2 - MODE=examples-pt3 diff --git a/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj b/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..d90f13803b --- /dev/null +++ b/ASDKListKit/ASDKListKit.xcodeproj/project.pbxproj @@ -0,0 +1,394 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + BD860CAB842324FDF0B3105C /* libPods-ASDKListKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FEACA22D54B3609C19BB4CE /* libPods-ASDKListKitTests.a */; }; + CC5532391E16F2A90011C01F /* ASListTestSupplementarySource.m in Sources */ = {isa = PBXBuildFile; fileRef = CC55322D1E16F2A90011C01F /* ASListTestSupplementarySource.m */; }; + CC55323A1E16F2A90011C01F /* ASListTestSupplementaryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC55322F1E16F2A90011C01F /* ASListTestSupplementaryNode.m */; }; + CC55323B1E16F2A90011C01F /* ASListKitTestAdapterDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532311E16F2A90011C01F /* ASListKitTestAdapterDataSource.m */; }; + CC55323C1E16F2A90011C01F /* ASListTestSection.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532331E16F2A90011C01F /* ASListTestSection.m */; }; + CC55323D1E16F2A90011C01F /* ASListTestCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532351E16F2A90011C01F /* ASListTestCellNode.m */; }; + CC55323E1E16F2A90011C01F /* ASListTestObject.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532371E16F2A90011C01F /* ASListTestObject.m */; }; + CC55323F1E16F2A90011C01F /* ASListKitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532381E16F2A90011C01F /* ASListKitTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1FEACA22D54B3609C19BB4CE /* libPods-ASDKListKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ASDKListKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + B1FDA57F88BB590E403D7BB8 /* Pods-ASDKListKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKListKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests.debug.xcconfig"; sourceTree = ""; }; + CC5532231E16EB9D0011C01F /* ASDKListKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASDKListKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CC5532281E16EB9D0011C01F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CC55322C1E16F2A90011C01F /* ASListTestSupplementarySource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestSupplementarySource.h; sourceTree = ""; }; + CC55322D1E16F2A90011C01F /* ASListTestSupplementarySource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestSupplementarySource.m; sourceTree = ""; }; + CC55322E1E16F2A90011C01F /* ASListTestSupplementaryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestSupplementaryNode.h; sourceTree = ""; }; + CC55322F1E16F2A90011C01F /* ASListTestSupplementaryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestSupplementaryNode.m; sourceTree = ""; }; + CC5532301E16F2A90011C01F /* ASListKitTestAdapterDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListKitTestAdapterDataSource.h; sourceTree = ""; }; + CC5532311E16F2A90011C01F /* ASListKitTestAdapterDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListKitTestAdapterDataSource.m; sourceTree = ""; }; + CC5532321E16F2A90011C01F /* ASListTestSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestSection.h; sourceTree = ""; }; + CC5532331E16F2A90011C01F /* ASListTestSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestSection.m; sourceTree = ""; }; + CC5532341E16F2A90011C01F /* ASListTestCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestCellNode.h; sourceTree = ""; }; + CC5532351E16F2A90011C01F /* ASListTestCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestCellNode.m; sourceTree = ""; }; + CC5532361E16F2A90011C01F /* ASListTestObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASListTestObject.h; sourceTree = ""; }; + CC5532371E16F2A90011C01F /* ASListTestObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListTestObject.m; sourceTree = ""; }; + CC5532381E16F2A90011C01F /* ASListKitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASListKitTests.m; sourceTree = ""; }; + CC55326C1E16F67A0011C01F /* ASXCTExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASXCTExtensions.h; sourceTree = ""; }; + D6BDED6F23A72F40F571EEF0 /* Pods-ASDKListKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKListKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CC5532201E16EB9D0011C01F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BD860CAB842324FDF0B3105C /* libPods-ASDKListKitTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 72C6817154F7AC11E373624A /* Pods */ = { + isa = PBXGroup; + children = ( + B1FDA57F88BB590E403D7BB8 /* Pods-ASDKListKitTests.debug.xcconfig */, + D6BDED6F23A72F40F571EEF0 /* Pods-ASDKListKitTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 74DAA5F5D522433F103348B7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1FEACA22D54B3609C19BB4CE /* libPods-ASDKListKitTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + CC5532181E16EB7A0011C01F = { + isa = PBXGroup; + children = ( + CC5532251E16EB9D0011C01F /* ASDKListKitTests */, + CC5532241E16EB9D0011C01F /* Products */, + 72C6817154F7AC11E373624A /* Pods */, + 74DAA5F5D522433F103348B7 /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + CC5532241E16EB9D0011C01F /* Products */ = { + isa = PBXGroup; + children = ( + CC5532231E16EB9D0011C01F /* ASDKListKitTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + CC5532251E16EB9D0011C01F /* ASDKListKitTests */ = { + isa = PBXGroup; + children = ( + CC55326D1E16F67D0011C01F /* Common */, + CC55326E1E170A740011C01F /* ListKit Fixtures */, + CC5532381E16F2A90011C01F /* ASListKitTests.m */, + CC5532281E16EB9D0011C01F /* Info.plist */, + ); + path = ASDKListKitTests; + sourceTree = ""; + }; + CC55326D1E16F67D0011C01F /* Common */ = { + isa = PBXGroup; + children = ( + CC55326C1E16F67A0011C01F /* ASXCTExtensions.h */, + ); + name = Common; + sourceTree = ""; + }; + CC55326E1E170A740011C01F /* ListKit Fixtures */ = { + isa = PBXGroup; + children = ( + CC55322C1E16F2A90011C01F /* ASListTestSupplementarySource.h */, + CC55322D1E16F2A90011C01F /* ASListTestSupplementarySource.m */, + CC55322E1E16F2A90011C01F /* ASListTestSupplementaryNode.h */, + CC55322F1E16F2A90011C01F /* ASListTestSupplementaryNode.m */, + CC5532301E16F2A90011C01F /* ASListKitTestAdapterDataSource.h */, + CC5532311E16F2A90011C01F /* ASListKitTestAdapterDataSource.m */, + CC5532321E16F2A90011C01F /* ASListTestSection.h */, + CC5532331E16F2A90011C01F /* ASListTestSection.m */, + CC5532341E16F2A90011C01F /* ASListTestCellNode.h */, + CC5532351E16F2A90011C01F /* ASListTestCellNode.m */, + CC5532361E16F2A90011C01F /* ASListTestObject.h */, + CC5532371E16F2A90011C01F /* ASListTestObject.m */, + ); + name = "ListKit Fixtures"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CC5532221E16EB9D0011C01F /* ASDKListKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CC5532291E16EB9D0011C01F /* Build configuration list for PBXNativeTarget "ASDKListKitTests" */; + buildPhases = ( + 614B24BFF3DA58512D2E2147 /* [CP] Check Pods Manifest.lock */, + CC55321F1E16EB9D0011C01F /* Sources */, + CC5532201E16EB9D0011C01F /* Frameworks */, + CC5532211E16EB9D0011C01F /* Resources */, + 989E6C194A1983B8B21AB82F /* [CP] Embed Pods Frameworks */, + 876CE14CAF6A87E34577E157 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ASDKListKitTests; + productName = ASDKListKitTests; + productReference = CC5532231E16EB9D0011C01F /* ASDKListKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CC5532191E16EB7A0011C01F /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0820; + TargetAttributes = { + CC5532221E16EB9D0011C01F = { + CreatedOnToolsVersion = 8.2.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = CC55321C1E16EB7A0011C01F /* Build configuration list for PBXProject "ASDKListKit" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CC5532181E16EB7A0011C01F; + productRefGroup = CC5532241E16EB9D0011C01F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CC5532221E16EB9D0011C01F /* ASDKListKitTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CC5532211E16EB9D0011C01F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 614B24BFF3DA58512D2E2147 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] 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; + }; + 876CE14CAF6A87E34577E157 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 989E6C194A1983B8B21AB82F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ASDKListKitTests/Pods-ASDKListKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CC55321F1E16EB9D0011C01F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CC55323E1E16F2A90011C01F /* ASListTestObject.m in Sources */, + CC5532391E16F2A90011C01F /* ASListTestSupplementarySource.m in Sources */, + CC55323D1E16F2A90011C01F /* ASListTestCellNode.m in Sources */, + CC55323B1E16F2A90011C01F /* ASListKitTestAdapterDataSource.m in Sources */, + CC55323C1E16F2A90011C01F /* ASListTestSection.m in Sources */, + CC55323F1E16F2A90011C01F /* ASListKitTests.m in Sources */, + CC55323A1E16F2A90011C01F /* ASListTestSupplementaryNode.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + CC55321D1E16EB7A0011C01F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Debug; + }; + CC55321E1E16EB7A0011C01F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Release; + }; + CC55322A1E16EB9D0011C01F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B1FDA57F88BB590E403D7BB8 /* Pods-ASDKListKitTests.debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + 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; + INFOPLIST_FILE = ASDKListKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = asyncdisplaykit.ASDKListKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + CC55322B1E16EB9D0011C01F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D6BDED6F23A72F40F571EEF0 /* Pods-ASDKListKitTests.release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = ASDKListKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = asyncdisplaykit.ASDKListKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CC55321C1E16EB7A0011C01F /* Build configuration list for PBXProject "ASDKListKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CC55321D1E16EB7A0011C01F /* Debug */, + CC55321E1E16EB7A0011C01F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CC5532291E16EB9D0011C01F /* Build configuration list for PBXNativeTarget "ASDKListKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CC55322A1E16EB9D0011C01F /* Debug */, + CC55322B1E16EB9D0011C01F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CC5532191E16EB7A0011C01F /* Project object */; +} diff --git a/ASDKListKit/ASDKListKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ASDKListKit/ASDKListKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..5f9c1bf23a --- /dev/null +++ b/ASDKListKit/ASDKListKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ASDKListKit/ASDKListKit.xcworkspace/contents.xcworkspacedata b/ASDKListKit/ASDKListKit.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..4d0db5485c --- /dev/null +++ b/ASDKListKit/ASDKListKit.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.h b/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.h new file mode 100644 index 0000000000..cbace9a735 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.h @@ -0,0 +1,16 @@ +// +// ASListKitTestAdapterDataSource.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface ASListKitTestAdapterDataSource : NSObject + +// array of numbers which is then passed to -[IGListTestSection setItems:] +@property (nonatomic, strong) NSArray *objects; + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.m b/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.m new file mode 100644 index 0000000000..e83dbf8870 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListKitTestAdapterDataSource.m @@ -0,0 +1,30 @@ +// +// ASListKitTestAdapterDataSource.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASListKitTestAdapterDataSource.h" +#import "ASListTestSection.h" + +@implementation ASListKitTestAdapterDataSource + +- (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter +{ + return self.objects; +} + +- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object +{ + ASListTestSection *section = [[ASListTestSection alloc] init]; + return section; +} + +- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter +{ + return nil; +} + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListKitTests.m b/ASDKListKit/ASDKListKitTests/ASListKitTests.m new file mode 100644 index 0000000000..5312b10874 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListKitTests.m @@ -0,0 +1,110 @@ +// +// ASListKitTests.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import +#import "ASListKitTestAdapterDataSource.h" +#import "ASXCTExtensions.h" +#import + +@interface ASListKitTests : XCTestCase + +@property (nonatomic, strong) ASCollectionNode *collectionNode; +@property (nonatomic, strong) UICollectionView *collectionView; +@property (nonatomic, strong) IGListAdapter *adapter; +@property (nonatomic, strong) ASListKitTestAdapterDataSource *dataSource; +@property (nonatomic, strong) UICollectionViewFlowLayout *layout; +@property (nonatomic, strong) UIWindow *window; +@property (nonatomic) NSInteger reloadDataCount; + +@end + +@implementation ASListKitTests + +- (void)setUp +{ + [super setUp]; + + [ASCollectionView swizzleInstanceMethod:@selector(reloadData) withReplacement:JGMethodReplacementProviderBlock { + return JGMethodReplacement(void, ASCollectionView *) { + JGOriginalImplementation(void); + _reloadDataCount++; + }; + }]; + + self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + + self.layout = [[UICollectionViewFlowLayout alloc] init]; + self.collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:self.layout]; + self.collectionNode.frame = self.window.bounds; + self.collectionView = self.collectionNode.view; + + [self.window addSubnode:self.collectionNode]; + + IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; + + self.dataSource = [[ASListKitTestAdapterDataSource alloc] init]; + self.adapter = [[IGListAdapter alloc] initWithUpdater:updater + viewController:nil + workingRangeSize:0]; + self.adapter.dataSource = self.dataSource; + [self.adapter setASDKCollectionNode:self.collectionNode]; + XCTAssertNotNil(self.adapter.collectionView, @"Adapter was not bound to collection view. You may have a stale copy of AsyncDisplayKit that was built without IG_LIST_KIT. Clean Builder Folder IMO."); +} + +- (void)tearDown +{ + [super tearDown]; + XCTAssert([ASCollectionView deswizzleAllMethods]); + self.reloadDataCount = 0; + self.window = nil; + self.collectionNode = nil; + self.collectionView = nil; + self.adapter = nil; + self.dataSource = nil; + self.layout = nil; +} + +- (void)test_whenAdapterUpdated_withObjectsOverflow_thatVisibleObjectsIsSubsetOfAllObjects +{ + // each section controller returns n items sized 100x10 + self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; + XCTestExpectation *e = [self expectationWithDescription:@"Data update completed"]; + + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + [e fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + self.collectionNode.view.contentOffset = CGPointMake(0, 30); + [self.collectionNode.view layoutIfNeeded]; + + + NSArray *visibleObjects = [[self.adapter visibleObjects] sortedArrayUsingSelector:@selector(compare:)]; + NSArray *expectedObjects = @[@3, @4, @5]; + XCTAssertEqualObjects(visibleObjects, expectedObjects); +} + +- (void)test_whenCollectionViewIsNotInAWindow_updaterDoesNotJustCallReloadData +{ + [self.collectionView removeFromSuperview]; + + [self.collectionView layoutIfNeeded]; + self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; + XCTestExpectation *e = [self expectationWithDescription:@"Data update completed"]; + + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + [e fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + [self.collectionView layoutIfNeeded]; + + XCTAssertEqual(self.reloadDataCount, 2); +} + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestCellNode.h b/ASDKListKit/ASDKListKitTests/ASListTestCellNode.h new file mode 100644 index 0000000000..b94de37c62 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestCellNode.h @@ -0,0 +1,13 @@ +// +// ASListTestCellNode.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface ASListTestCellNode : ASCellNode + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestCellNode.m b/ASDKListKit/ASDKListKitTests/ASListTestCellNode.m new file mode 100644 index 0000000000..fd82e6dc4f --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestCellNode.m @@ -0,0 +1,13 @@ +// +// ASListTestCellNode.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASListTestCellNode.h" + +@implementation ASListTestCellNode + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestObject.h b/ASDKListKit/ASDKListKitTests/ASListTestObject.h new file mode 100644 index 0000000000..8b5cd23bb5 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestObject.h @@ -0,0 +1,22 @@ +// +// ASListTestObject.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASListTestObject : NSObject + +- (instancetype)initWithKey:(id )key value:(id)value; + +@property (nonatomic, strong, readonly) id key; +@property (nonatomic, strong) id value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ASDKListKit/ASDKListKitTests/ASListTestObject.m b/ASDKListKit/ASDKListKitTests/ASListTestObject.m new file mode 100644 index 0000000000..ffd1216dd4 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestObject.m @@ -0,0 +1,49 @@ +// +// ASListTestObject.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASListTestObject.h" + +@implementation ASListTestObject + +- (instancetype)initWithKey:(id)key value:(id)value +{ + if (self = [super init]) { + _key = [key copy]; + _value = value; + } + return self; +} + +- (instancetype)copyWithZone:(NSZone *)zone +{ + return [[ASListTestObject alloc] initWithKey:self.key value:self.value]; +} + +#pragma mark - IGListDiffable + +- (id)diffIdentifier +{ + return self.key; +} + +- (BOOL)isEqualToDiffableObject:(id)object +{ + if (object == self) { + return YES; + } + if ([object isKindOfClass:[ASListTestObject class]]) { + id k1 = self.key; + id k2 = [object key]; + id v1 = self.value; + id v2 = [(ASListTestObject *)object value]; + return (v1 == v2 || [v1 isEqual:v2]) && (k1 == k2 || [k1 isEqual:k2]); + } + return NO; +} + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestSection.h b/ASDKListKit/ASDKListKitTests/ASListTestSection.h new file mode 100644 index 0000000000..1d10ddbe5b --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestSection.h @@ -0,0 +1,18 @@ +// +// ASListTestSection.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import + +@interface ASListTestSection : IGListSectionController + +@property (nonatomic) NSInteger itemCount; + +@property (nonatomic) NSInteger selectedItemIndex; + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestSection.m b/ASDKListKit/ASDKListKitTests/ASListTestSection.m new file mode 100644 index 0000000000..239819b14f --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestSection.m @@ -0,0 +1,58 @@ +// +// ASListTestSection.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASListTestSection.h" +#import "ASListTestCellNode.h" + +@implementation ASListTestSection + +- (instancetype)init +{ + if (self = [super init]) +{ + _selectedItemIndex = NSNotFound; + } + return self; +} + +- (NSInteger)numberOfItems +{ + return self.itemCount; +} + +- (CGSize)sizeForItemAtIndex:(NSInteger)index +{ + ASDisplayNodeFailAssert(@"Did not expect %@ to be called.", NSStringFromSelector(_cmd)); + return CGSizeMake(100, 10); +} + +ASIGSectionControllerCellForIndexImplementation + +- (void)didUpdateToObject:(id)object +{ + if ([object isKindOfClass:[NSNumber class]]) +{ + self.itemCount = [object integerValue]; + } +} + +- (void)didSelectItemAtIndex:(NSInteger)index +{ + self.selectedItemIndex = index; +} + +- (ASCellNodeBlock)nodeBlockForItemAtIndex:(NSInteger)index +{ + return ^{ + ASListTestCellNode *node = [[ASListTestCellNode alloc] init]; + node.style.preferredSize = CGSizeMake(100, 10); + return node; + }; +} + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.h b/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.h new file mode 100644 index 0000000000..55dcd6b70b --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.h @@ -0,0 +1,13 @@ +// +// ASListTestSupplementaryNode.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface ASListTestSupplementaryNode : ASCellNode + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.m b/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.m new file mode 100644 index 0000000000..f47f577ca6 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestSupplementaryNode.m @@ -0,0 +1,13 @@ +// +// ASListTestSupplementaryNode.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASListTestSupplementaryNode.h" + +@implementation ASListTestSupplementaryNode + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.h b/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.h new file mode 100644 index 0000000000..55f00d4a5a --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.h @@ -0,0 +1,20 @@ +// +// ASListTestSupplementarySource.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import + +@interface ASListTestSupplementarySource : NSObject + +@property (nonatomic, strong, readwrite) NSArray *supportedElementKinds; + +@property (nonatomic, weak) id collectionContext; + +@property (nonatomic, weak) IGListSectionController *sectionController; + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.m b/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.m new file mode 100644 index 0000000000..6197d2c4c8 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASListTestSupplementarySource.m @@ -0,0 +1,24 @@ +// +// ASListTestSupplementarySource.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 12/25/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASListTestSupplementarySource.h" +#import "ASListTestSupplementaryNode.h" + +@implementation ASListTestSupplementarySource + +ASIGSupplementarySourceViewForSupplementaryElementImplementation(self.sectionController) +ASIGSupplementarySourceSizeForSupplementaryElementImplementation + +- (ASCellNode *)nodeForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + ASListTestSupplementaryNode *node = [[ASListTestSupplementaryNode alloc] init]; + node.style.preferredSize = CGSizeMake(100, 10); + return node; +} + +@end diff --git a/ASDKListKit/ASDKListKitTests/ASXCTExtensions.h b/ASDKListKit/ASDKListKitTests/ASXCTExtensions.h new file mode 100644 index 0000000000..574b7a7175 --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/ASXCTExtensions.h @@ -0,0 +1,36 @@ +/** + * XCTest extensions for CGGeometry. + * + * Prefer these to XCTAssert(CGRectEqualToRect(...)) because you get output + * that tells you what went wrong. + * Could use NSValue, but using strings makes the description messages shorter. + */ + +#import + +#define ASXCTAssertEqualSizes(s0, s1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromCGSize(s0), @#s0, NSStringFromCGSize(s1), @#s1, __VA_ARGS__) + +#define ASXCTAssertNotEqualSizes(s0, s1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromCGSize(s0), @#s0, NSStringFromCGSize(s1), @#s1, __VA_ARGS__) + +#define ASXCTAssertEqualPoints(p0, p1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromCGPoint(p0), @#p0, NSStringFromCGPoint(p1), @#p1, __VA_ARGS__) + +#define ASXCTAssertNotEqualPoints(p0, p1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromCGPoint(p0), @#p0, NSStringFromCGPoint(p1), @#p1, __VA_ARGS__) + +#define ASXCTAssertEqualRects(r0, r1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromCGRect(r0), @#r0, NSStringFromCGRect(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertNotEqualRects(r0, r1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromCGRect(r0), @#r0, NSStringFromCGRect(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertEqualDimensions(r0, r1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromASDimension(r0), @#r0, NSStringFromASDimension(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertNotEqualDimensions(r0, r1, ...) \ + _XCTPrimitiveAssertNotEqualObjects(self, NSStringFromASDimension(r0), @#r0, NSStringFromASDimension(r1), @#r1, __VA_ARGS__) + +#define ASXCTAssertEqualSizeRanges(r0, r1, ...) \ + _XCTPrimitiveAssertEqualObjects(self, NSStringFromASSizeRange(r0), @#r0, NSStringFromASSizeRange(r1), @#r1, __VA_ARGS__) diff --git a/ASDKListKit/ASDKListKitTests/Info.plist b/ASDKListKit/ASDKListKitTests/Info.plist new file mode 100644 index 0000000000..6c6c23c43a --- /dev/null +++ b/ASDKListKit/ASDKListKitTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/ASDKListKit/Podfile b/ASDKListKit/Podfile new file mode 100644 index 0000000000..09aa4c8841 --- /dev/null +++ b/ASDKListKit/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' + +platform :ios, '8.0' +target 'ASDKListKitTests' do + pod 'AsyncDisplayKit/IGListKit', :path => '..' + pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' +end + diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 6f104c9b36..df73e927c1 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -47,6 +47,12 @@ Pod::Spec.new do |spec| pin.dependency 'PINRemoteImage/PINCache' pin.dependency 'AsyncDisplayKit/Core' end + + spec.subspec 'IGListKit' do |igl| + igl.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) IG_LIST_KIT=1' } + igl.dependency 'IGListKit', '2.1.0' + igl.dependency 'AsyncDisplayKit/Core' + end # Include optional PINRemoteImage module spec.default_subspec = 'PINRemoteImage' diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index c0c8ddb37c..05d00a230b 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -459,15 +459,30 @@ CC4C2A7A1D8902350039ACAB /* ASTraceEvent.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */; }; CC54A81C1D70079800296A24 /* ASDispatch.h in Headers */ = {isa = PBXBuildFile; fileRef = CC54A81B1D70077A00296A24 /* ASDispatch.h */; }; CC54A81E1D7008B300296A24 /* ASDispatchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC54A81D1D7008B300296A24 /* ASDispatchTests.m */; }; + CC6363E21E32C00800D8A8DE /* ASCollectionInteropProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = CC6363E11E32C00800D8A8DE /* ASCollectionInteropProtocols.h */; }; + CC6363E31E32C01900D8A8DE /* ASCollectionInteropProtocols.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CC6363E11E32C00800D8A8DE /* ASCollectionInteropProtocols.h */; }; CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; }; CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; }; CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC8525151E3FC253008EABE6 /* _ASCollectionViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CC8525131E3FC253008EABE6 /* _ASCollectionViewCell.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CC8525161E3FC253008EABE6 /* _ASCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CC8525141E3FC253008EABE6 /* _ASCollectionViewCell.m */; }; + CC8525171E3FC253008EABE6 /* _ASCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CC8525141E3FC253008EABE6 /* _ASCollectionViewCell.m */; }; + CC8525181E3FC316008EABE6 /* _ASCollectionViewCell.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CC8525131E3FC253008EABE6 /* _ASCollectionViewCell.h */; }; CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */; }; CC8B05D61D73836400F54286 /* ASPerformanceTestContext.m in Sources */ = {isa = PBXBuildFile; fileRef = CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */; }; CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */; }; CCA221D31D6FA7EF00AF6A0F /* ASViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.m */; }; CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */; }; + CCE04B1F1E313EA7006AEBBB /* ASSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCE04B221E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */; }; + CCE04B231E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m in Sources */ = {isa = PBXBuildFile; fileRef = CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */; }; + CCE04B241E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m in Sources */ = {isa = PBXBuildFile; fileRef = CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */; }; + CCE04B2C1E314A32006AEBBB /* ASSupplementaryNodeSource.h in Headers */ = {isa = PBXBuildFile; fileRef = CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCE04B2E1E314A9C006AEBBB /* ASSectionController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */; }; + CCE04B2F1E314A9C006AEBBB /* ASSupplementaryNodeSource.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */; }; + CCE04B301E314A9C006AEBBB /* IGListAdapter+AsyncDisplayKit.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */; }; CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; }; + CCF92DCD1E3155180019E9C6 /* ASIGListKitMethodImplementations.h in Headers */ = {isa = PBXBuildFile; fileRef = CCF92DCC1E3155180019E9C6 /* ASIGListKitMethodImplementations.h */; }; D785F6631A74327E00291744 /* ASScrollNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.mm */; }; DB55C2631C6408D6004EDCF5 /* _ASTransitionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.m */; }; DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -674,6 +689,7 @@ dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( + CC8525181E3FC316008EABE6 /* _ASCollectionViewCell.h in CopyFiles */, 690ED59D1E36D140000627C0 /* ASImageNode+tvOS.h in CopyFiles */, 690ED59C1E36D13A000627C0 /* ASControlNode+tvOS.h in CopyFiles */, 690ED58F1E36BCBF000627C0 /* ASLayoutElementStylePrivate.h in CopyFiles */, @@ -682,6 +698,10 @@ 692BE8D81E36B66500C86D87 /* ASLayoutSpecPrivate.h in CopyFiles */, 6947B0C71E36B5B60007C478 /* ASStackPositionedLayout.h in CopyFiles */, 6947B0C61E36B5A90007C478 /* ASStackUnpositionedLayout.h in CopyFiles */, + CC6363E31E32C01900D8A8DE /* ASCollectionInteropProtocols.h in CopyFiles */, + CCE04B2E1E314A9C006AEBBB /* ASSectionController.h in CopyFiles */, + CCE04B2F1E314A9C006AEBBB /* ASSupplementaryNodeSource.h in CopyFiles */, + CCE04B301E314A9C006AEBBB /* IGListAdapter+AsyncDisplayKit.h in CopyFiles */, 693DA5141E25373100F66DF4 /* ASDimensionDeprecated.h in CopyFiles */, 693DA50F1E2536A600F66DF4 /* ASDimensionInternal.h in CopyFiles */, 68C2155C1DE11AA80019C4BC /* ASObjectDescriptionHelpers.h in CopyFiles */, @@ -967,7 +987,7 @@ 257754BC1BEE458E00737CA5 /* ASTextNodeTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextNodeTypes.h; path = TextKit/ASTextNodeTypes.h; sourceTree = ""; }; 257754BD1BEE458E00737CA5 /* ASTextNodeWordKerner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASTextNodeWordKerner.m; path = TextKit/ASTextNodeWordKerner.m; sourceTree = ""; }; 25E327541C16819500A2170C /* ASPagerNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASPagerNode.h; sourceTree = ""; }; - 25E327551C16819500A2170C /* ASPagerNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASPagerNode.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 25E327551C16819500A2170C /* ASPagerNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASPagerNode.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 = ""; }; 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASBasicImageDownloaderInternal.h; sourceTree = ""; }; @@ -1158,15 +1178,23 @@ CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableView+Undeprecated.h"; sourceTree = ""; }; CC54A81B1D70077A00296A24 /* ASDispatch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASDispatch.h; sourceTree = ""; }; CC54A81D1D7008B300296A24 /* ASDispatchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDispatchTests.m; sourceTree = ""; }; + CC6363E11E32C00800D8A8DE /* ASCollectionInteropProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionInteropProtocols.h; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = ""; }; CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = ""; }; + CC8525131E3FC253008EABE6 /* _ASCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionViewCell.h; sourceTree = ""; }; + CC8525141E3FC253008EABE6 /* _ASCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _ASCollectionViewCell.m; sourceTree = ""; }; CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ASCellNode+Internal.h"; path = "AsyncDisplayKit/ASCellNode+Internal.h"; sourceTree = SOURCE_ROOT; }; CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPerformanceTestContext.h; sourceTree = ""; }; CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPerformanceTestContext.m; sourceTree = ""; }; CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextNodePerformanceTests.m; sourceTree = ""; }; CCA221D21D6FA7EF00AF6A0F /* ASViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASViewControllerTests.m; sourceTree = ""; }; CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeSnapshotTests.m; sourceTree = ""; }; + CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSectionController.h; sourceTree = ""; }; + CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+AsyncDisplayKit.h"; sourceTree = ""; }; + CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = ""; }; + CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSupplementaryNodeSource.h; sourceTree = ""; }; + CCF92DCC1E3155180019E9C6 /* ASIGListKitMethodImplementations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIGListKitMethodImplementations.h; sourceTree = ""; }; D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = ""; }; @@ -1316,6 +1344,7 @@ 058D09B1195D04C000B7D73C /* AsyncDisplayKit */ = { isa = PBXGroup; children = ( + CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */, DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */, DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */, 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */, @@ -1485,6 +1514,8 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + CC8525131E3FC253008EABE6 /* _ASCollectionViewCell.h */, + CC8525141E3FC253008EABE6 /* _ASCollectionViewCell.m */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -1577,6 +1608,7 @@ isa = PBXGroup; children = ( 6947B0BB1E36B4E30007C478 /* Layout */, + CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, @@ -1784,6 +1816,34 @@ name = "Supporting Files"; sourceTree = ""; }; + CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */ = { + isa = PBXGroup; + children = ( + CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */, + CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */, + CCF92DCE1E315FC50019E9C6 /* IGListKit Support */, + ); + name = "Collection Data Adapter"; + sourceTree = ""; + }; + CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */ = { + isa = PBXGroup; + children = ( + CC6363E11E32C00800D8A8DE /* ASCollectionInteropProtocols.h */, + ); + name = "Collection Data Adapter"; + sourceTree = ""; + }; + CCF92DCE1E315FC50019E9C6 /* IGListKit Support */ = { + isa = PBXGroup; + children = ( + CCF92DCC1E3155180019E9C6 /* ASIGListKitMethodImplementations.h */, + CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */, + CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */, + ); + name = "IGListKit Support"; + sourceTree = ""; + }; DE89C1691DCEB9CC00D49D74 /* Debug */ = { isa = PBXGroup; children = ( @@ -1855,11 +1915,13 @@ 254C6B731BF94DF4003EC431 /* ASTextKitCoreTextAdditions.h in Headers */, A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */, 254C6B7A1BF94DF4003EC431 /* ASTextKitRenderer.h in Headers */, + CCE04B1F1E313EA7006AEBBB /* ASSectionController.h in Headers */, 69CB62AC1CB8165900024920 /* _ASDisplayViewAccessiblity.h in Headers */, 68355B3F1CB57A64001D4E68 /* ASPINRemoteImageDownloader.h in Headers */, 254C6B7C1BF94DF4003EC431 /* ASTextKitRenderer+TextChecking.h in Headers */, 34EFC7611B701C9C00AD841F /* ASBackgroundLayoutSpec.h in Headers */, 68AF37DB1CBEF4D80077BF76 /* ASImageNode+AnimatedImagePrivate.h in Headers */, + CCE04B221E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h in Headers */, B35062591B010F070018CF92 /* ASBaseDefines.h in Headers */, B35062131B010EFD0018CF92 /* ASBasicImageDownloader.h in Headers */, B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */, @@ -1883,6 +1945,7 @@ 9C70F20F1CDBE9FF007D6C76 /* ASLayoutManager.h in Headers */, B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */, 6947B0C31E36B5040007C478 /* ASStackPositionedLayout.h in Headers */, + CC6363E21E32C00800D8A8DE /* ASCollectionInteropProtocols.h in Headers */, B35061F81B010EFD0018CF92 /* ASControlNode.h in Headers */, B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, @@ -1901,6 +1964,7 @@ B350621B1B010EFD0018CF92 /* ASFlowLayoutController.h in Headers */, B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */, C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */, + CCF92DCD1E3155180019E9C6 /* ASIGListKitMethodImplementations.h in Headers */, AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */, 254C6B7F1BF94DF4003EC431 /* ASTextKitTruncating.h in Headers */, @@ -1935,6 +1999,7 @@ DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */, B35062241B010EFD0018CF92 /* ASMutableAttributedStringBuilder.h in Headers */, B13CA0F81C519EBA00E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h in Headers */, + CCE04B2C1E314A32006AEBBB /* ASSupplementaryNodeSource.h in Headers */, 8BBBAB8C1CEBAF1700107FC6 /* ASDefaultPlaybackButton.h in Headers */, B35062061B010EFD0018CF92 /* ASNetworkImageNode.h in Headers */, 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */, @@ -1973,6 +2038,7 @@ 9C6BB3B31B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h in Headers */, 34EFC7731B701D0700AD841F /* ASAbsoluteLayoutSpec.h in Headers */, 254C6B781BF94DF4003EC431 /* ASTextKitContext.h in Headers */, + CC8525151E3FC253008EABE6 /* _ASCollectionViewCell.h in Headers */, B350620A1B010EFD0018CF92 /* ASTableView.h in Headers */, B350620C1B010EFD0018CF92 /* ASTableViewProtocols.h in Headers */, B350620D1B010EFD0018CF92 /* ASTextNode.h in Headers */, @@ -2227,6 +2293,7 @@ AC6456091B0A335000CF11B8 /* ASCellNode.mm in Sources */, DE8BEAC31C2DF3FC00D57C12 /* ASDelegateProxy.m in Sources */, ACF6ED1D1B17843500DA7C62 /* ASCenterLayoutSpec.mm in Sources */, + CC8525161E3FC253008EABE6 /* _ASCollectionViewCell.m in Sources */, 9F98C0251DBDF2A300476D92 /* ASControlTargetAction.m in Sources */, 18C2ED801B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */, 92DD2FE41BF4B97E0074C9DD /* ASMapNode.mm in Sources */, @@ -2274,6 +2341,7 @@ AC6145431D8AFD4F003D62A2 /* ASSection.m in Sources */, 058D0A1B195D050800B7D73C /* ASMutableAttributedStringBuilder.m in Sources */, 055B9FA91A1C154B00035D6D /* ASNetworkImageNode.mm in Sources */, + CCE04B231E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m in Sources */, AEB7B01B1C5962EA00662EF4 /* ASDefaultPlayButton.m in Sources */, CC3B20851C3F76D600798563 /* ASPendingStateController.mm in Sources */, ACF6ED2C1B17843500DA7C62 /* ASOverlayLayoutSpec.mm in Sources */, @@ -2418,6 +2486,7 @@ DE8BEAC41C2DF3FC00D57C12 /* ASDelegateProxy.m in Sources */, 9C70F2081CDAA3C6007D6C76 /* ASEnvironment.mm in Sources */, B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, + CC8525171E3FC253008EABE6 /* _ASCollectionViewCell.m in Sources */, B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */, AC47D9421B3B891B00AAEE9D /* ASCellNode.mm in Sources */, 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */, @@ -2465,6 +2534,7 @@ 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */, DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, + CCE04B241E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, diff --git a/AsyncDisplayKit/ASCollectionNode.h b/AsyncDisplayKit/ASCollectionNode.h index dad0bd76c6..60b54f78ed 100644 --- a/AsyncDisplayKit/ASCollectionNode.h +++ b/AsyncDisplayKit/ASCollectionNode.h @@ -501,6 +501,16 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable id)collectionNode:(ASCollectionNode *)collectionNode contextForSection:(NSInteger)section; +/** + * Asks the data source to provide an array of supplementary element kinds that exist in a given section. + * + * @param collectionNode The sender. + * @param section The index of the section to provide supplementary kinds for. + * + * @return The supplementary element kinds that exist in the given section, if any. + */ +- (NSArray *)collectionNode:(ASCollectionNode *)collectionNode supplementaryElementKindsInSection:(NSInteger)section; + /** * Similar to -collectionView:cellForItemAtIndexPath:. * diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index ca9905e224..da0c906c3a 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -13,6 +13,7 @@ #import "ASCollectionInternal.h" #import "ASCollectionViewLayoutFacilitatorProtocol.h" #import "ASCollectionNode.h" +#import "ASCollectionNode+Beta.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASEnvironmentInternal.h" diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 842bafbf48..3998bdb406 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -8,12 +8,18 @@ // of patent rights can be found in the PATENTS file in the same directory. // +#import "ASCollectionView.h" + +#import + +#import "_ASCollectionViewCell.h" #import "ASAssert.h" #import "ASAvailability.h" #import "ASBatchFetching.h" #import "ASDelegateProxy.h" #import "ASCellNode+Internal.h" #import "ASCollectionDataController.h" +#import "ASCollectionInternal.h" #import "ASCollectionViewLayoutController.h" #import "ASCollectionViewFlowLayoutInspector.h" #import "ASDisplayNodeExtras.h" @@ -27,6 +33,7 @@ #import "ASSectionContext.h" #import "ASCollectionView+Undeprecated.h" #import "_ASHierarchyChangeSet.h" +#import "ASCollectionInteropProtocols.h" /** * A macro to get self.collectionNode and assign it to a local variable, or return @@ -62,79 +69,6 @@ static const NSUInteger kASCollectionViewAnimationNone = UITableViewRowAnimation /// Used for all cells and supplementaries. UICV keys by supp-kind+reuseID so this is plenty. static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; -#pragma mark - -#pragma mark ASCellNode<->UICollectionViewCell bridging. - -@class _ASCollectionViewCell; - -@interface _ASCollectionViewCell : UICollectionViewCell -@property (nonatomic, weak) ASCellNode *node; -@property (nonatomic, strong) UICollectionViewLayoutAttributes *layoutAttributes; -@end - -@implementation _ASCollectionViewCell - -- (void)setNode:(ASCellNode *)node -{ - ASDisplayNodeAssertMainThread(); - node.layoutAttributes = _layoutAttributes; - _node = node; - self.backgroundColor = node.backgroundColor; - self.clipsToBounds = node.clipsToBounds; - [node __setSelectedFromUIKit:self.selected]; - [node __setHighlightedFromUIKit:self.highlighted]; -} - -- (void)setSelected:(BOOL)selected -{ - [super setSelected:selected]; - [_node __setSelectedFromUIKit:selected]; -} - -- (void)setHighlighted:(BOOL)highlighted -{ - [super setHighlighted:highlighted]; - [_node __setHighlightedFromUIKit:highlighted]; -} - -- (void)setLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes -{ - _layoutAttributes = layoutAttributes; - _node.layoutAttributes = layoutAttributes; -} - -- (void)prepareForReuse -{ - self.layoutAttributes = nil; - - // Need to clear node pointer before UIKit calls setSelected:NO / setHighlighted:NO on its cells - self.node = nil; - [super prepareForReuse]; -} - -/** - * In the initial case, this is called by UICollectionView during cell dequeueing, before - * we get a chance to assign a node to it, so we must be sure to set these layout attributes - * on our node when one is next assigned to us in @c setNode: . Since there may be cases when we _do_ already - * have our node assigned e.g. during a layout update for existing cells, we also attempt - * to update it now. - */ -- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes -{ - self.layoutAttributes = layoutAttributes; -} - -/** - * Keep our node filling our content view. - */ -- (void)layoutSubviews -{ - [super layoutSubviews]; - self.node.frame = self.contentView.bounds; -} - -@end - #pragma mark - #pragma mark ASCollectionView. @@ -243,8 +177,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; unsigned int collectionNodeWillBeginBatchFetch:1; unsigned int collectionNodeWillDisplaySupplementaryElement:1; unsigned int collectionNodeDidEndDisplayingSupplementaryElement:1; - unsigned int shouldBatchFetchForCollectionNode:1; + // Whether this delegate conforms to ASCollectionDataSourceInterop + unsigned int interop:1; } _asyncDelegateFlags; struct { @@ -256,9 +191,13 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; unsigned int collectionNodeNodeForItem:1; unsigned int collectionNodeNodeBlockForItem:1; unsigned int collectionNodeNodeForSupplementaryElement:1; + unsigned int collectionNodeSupplementaryElementKindsInSection:1; unsigned int numberOfSectionsInCollectionNode:1; unsigned int collectionNodeNumberOfItemsInSection:1; unsigned int collectionNodeContextForSection:1; + + // Whether this data source conforms to ASCollectionDataSourceInterop + unsigned int interop:1; } _asyncDataSourceFlags; struct { @@ -267,8 +206,6 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; } _layoutInspectorFlags; } -@property (nonatomic, weak) ASCollectionNode *collectionNode; - @end @interface ASCollectionNode () @@ -426,14 +363,14 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (void)setDataSource:(id)dataSource { - // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. - ASDisplayNodeAssert(dataSource == nil, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property."); + // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. We also allow this when we're doing interop. + ASDisplayNodeAssert(_asyncDelegateFlags.interop || dataSource == nil, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property."); } - (void)setDelegate:(id)delegate { - // Our UIScrollView superclass sets its delegate to nil on dealloc. Only assert if we get a non-nil value here. - ASDisplayNodeAssert(delegate == nil, @"ASCollectionView uses asyncDelegate, not UICollectionView's delegate property."); + // Our UIScrollView superclass sets its delegate to nil on dealloc. Only assert if we get a non-nil value here. We also allow this when we're doing interop. + ASDisplayNodeAssert(_asyncDelegateFlags.interop || delegate == nil, @"ASCollectionView uses asyncDelegate, not UICollectionView's delegate property."); } - (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy @@ -464,8 +401,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; if (asyncDataSource == nil) { _asyncDataSource = nil; _proxyDataSource = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; - - memset(&_asyncDataSourceFlags, 0, sizeof(_asyncDataSourceFlags)); + _asyncDataSourceFlags = {}; + } else { _asyncDataSource = asyncDataSource; _proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; @@ -482,7 +419,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; _asyncDataSourceFlags.collectionNodeNumberOfItemsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:numberOfItemsInSection:)]; _asyncDataSourceFlags.collectionNodeContextForSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:contextForSection:)]; _asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeForSupplementaryElementOfKind:atIndexPath:)]; - + _asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:supplementaryElementKindsInSection:)]; + _asyncDataSourceFlags.interop = [_asyncDataSource conformsToProtocol:@protocol(ASCollectionDataSourceInterop)]; ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection || _asyncDataSourceFlags.collectionViewNumberOfItemsInSection, @"Data source must implement collectionNode:numberOfItemsInSection:"); ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNodeBlockForItem @@ -520,8 +458,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; if (asyncDelegate == nil) { _asyncDelegate = nil; _proxyDelegate = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; - - memset(&_asyncDelegateFlags, 0, sizeof(_asyncDelegateFlags)); + _asyncDataSourceFlags = {}; } else { _asyncDelegate = asyncDelegate; _proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; @@ -561,6 +498,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; _asyncDelegateFlags.collectionNodeShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldShowMenuForItemAtIndexPath:)]; _asyncDelegateFlags.collectionNodeCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:canPerformAction:forItemAtIndexPath:sender:)]; _asyncDelegateFlags.collectionNodePerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:performAction:forItemAtIndexPath:sender:)]; + _asyncDelegateFlags.interop = [_asyncDelegate conformsToProtocol:@protocol(ASCollectionDelegateInterop)]; } super.delegate = (id)_proxyDelegate; @@ -939,7 +877,13 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { - UICollectionReusableView *view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; + UICollectionReusableView *view; + if (_asyncDataSource && _asyncDataSourceFlags.interop) { + view = [(id)_asyncDataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + } else { + view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; + } + ASCellNode *node = [_dataController supplementaryNodeOfKind:kind atIndexPath:indexPath]; ASDisplayNodeAssert(node != nil, @"Supplementary node should exist. Kind = %@, indexPath = %@, collectionDataSource = %@", kind, indexPath, self); [_rangeController configureContentView:view forCellNode:node]; @@ -948,7 +892,12 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - _ASCollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; + _ASCollectionViewCell *cell; + if (_asyncDataSource && _asyncDataSourceFlags.interop) { + cell = [(id)_asyncDataSource collectionView:collectionView cellForItemAtIndexPath:indexPath]; + } else { + cell = [self dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; + } ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; cell.node = node; @@ -979,7 +928,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); - if (_asyncDelegateFlags.collectionNodeWillDisplayItem) { + if (_asyncDelegateFlags.interop) { + [(id)_asyncDelegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; + } else if (_asyncDelegateFlags.collectionNodeWillDisplayItem) { if (ASCollectionNode *collectionNode = self.collectionNode) { [_asyncDelegate collectionNode:collectionNode willDisplayItemWithNode:cellNode]; } @@ -1004,7 +955,9 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; ASCellNode *cellNode = [cell node]; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); - if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { + if (_asyncDelegateFlags.interop) { + [(id)_asyncDelegate collectionView:collectionView didEndDisplayingCell:cell forItemAtIndexPath:indexPath]; + } else if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { if (ASCollectionNode *collectionNode = self.collectionNode) { [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; } @@ -1544,10 +1497,20 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; return node; } -// TODO: Lock this -- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController sections:(NSIndexSet *)sections { - return [_registeredSupplementaryKinds allObjects]; + if (_asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection) { + NSMutableSet *kinds = [NSMutableSet set]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, @[]); + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { + NSArray *kindsForSection = [_asyncDataSource collectionNode:collectionNode supplementaryElementKindsInSection:section]; + [kinds addObjectsFromArray:kindsForSection]; + }]; + return [kinds allObjects]; + } else { + // TODO: Lock this + return [_registeredSupplementaryKinds allObjects]; + } } - (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath diff --git a/AsyncDisplayKit/ASCollectionViewProtocols.h b/AsyncDisplayKit/ASCollectionViewProtocols.h index 793d697744..892a64d387 100644 --- a/AsyncDisplayKit/ASCollectionViewProtocols.h +++ b/AsyncDisplayKit/ASCollectionViewProtocols.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView ASDISPLAYNODE_DEPRECATED_MSG("Implement -numberOfSectionsInCollectionNode: instead."); -- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement - collectionNode:viewForSupplementaryElementOfKind:atIndexPath: instead."); +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED_MSG("Implement - collectionNode:nodeForSupplementaryElementOfKind:atIndexPath: instead."); @end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 7a60081817..ddeefe51e1 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -389,11 +389,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) { ASDN::MutexLocker l(__instanceLock__); if ([self _isNodeLoaded]) { - ASDisplayNodeFailAssert(@"Attempt to call %@ on node after it was loaded. Node: %@", NSStringFromSelector(_cmd), self); - return; - } - - if (_onDidLoadBlocks == nil) { + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexUnlocker l(__instanceLock__); + body(self); + } else if (_onDidLoadBlocks == nil) { _onDidLoadBlocks = [NSMutableArray arrayWithObject:body]; } else { [_onDidLoadBlocks addObject:body]; @@ -663,11 +662,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDN::MutexLocker l(__instanceLock__); ASDisplayNodeLogEvent(self, @"didLoad"); + [self didLoad]; for (ASDisplayNodeDidLoadBlock block in _onDidLoadBlocks) { block(self); } _onDidLoadBlocks = nil; - [self didLoad]; } - (void)didLoad diff --git a/AsyncDisplayKit/ASIGListKitMethodImplementations.h b/AsyncDisplayKit/ASIGListKitMethodImplementations.h new file mode 100644 index 0000000000..0caac0434a --- /dev/null +++ b/AsyncDisplayKit/ASIGListKitMethodImplementations.h @@ -0,0 +1,66 @@ +// +// ASIGListKitMethodImplementations.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/19/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +/** + * If you are using AsyncDisplayKit with IGListKit, you should use + * these macros to provide implementations of methods like + * -cellForItemAtIndex: that don't apply when used with AsyncDisplayKit. + * + * Your section controllers should also conform to @c ASSectionController and your + * supplementary view sources should conform to @c ASSupplementaryNodeSource. + */ + +#if IG_LIST_KIT + +#import + +/** + * The implementation of viewForSupplementaryElementOfKind that connects + * IGSupplementaryViewSource to AsyncDisplayKit. Add this into the .m file + * for your `ASIGListSupplementaryViewSource` and implement the ASDK-specific + * method `nodeForSupplementaryElementOfKind:` to provide your node. + * + * @param sectionController The section controller this supplementary source is + * working on behalf of. For example, `self` or `self.sectionController`. + */ +#define ASIGSupplementarySourceViewForSupplementaryElementImplementation(sectionController) \ +- (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index { \ + return [self.collectionContext dequeueReusableSupplementaryViewOfKind:elementKind forSectionController:sectionController class:[UICollectionReusableView class] atIndex:index]; \ +} + +/** + * The implementation of sizeForSupplementaryViewOfKind that connects + * IGSupplementaryViewSource to AsyncDisplayKit. Add this into the .m file + * for your `ASIGListSupplementaryViewSource` and implement the ASDK-specific + * method `nodeForSupplementaryElementOfKind:` to provide your node which should + * size itself. You can set `node.style.preferredSize` if you want to fix the size. + * + * @param sectionController The section controller this supplementary source is + * working on behalf of. For example, `self` or `self.sectionController`. + */ +#define ASIGSupplementarySourceSizeForSupplementaryElementImplementation \ +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index {\ + ASDisplayNodeFailAssert(@"Did not expect %@ to be called.", NSStringFromSelector(_cmd)); \ + return CGSizeZero; \ +} + + +#define ASIGSectionControllerCellForIndexImplementation \ +- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index\ +{\ + return [self.collectionContext dequeueReusableCellOfClass:[_ASCollectionViewCell class] forSectionController:self atIndex:index]; \ +}\ + +#define ASIGSectionControllerSizeForItemImplementation \ +- (CGSize)sizeForItemAtIndex:(NSInteger)index \ +{\ + ASDisplayNodeFailAssert(@"Did not expect %@ to be called.", NSStringFromSelector(_cmd)); \ + return CGSizeZero;\ +} + +#endif // IG_LIST_KIT diff --git a/AsyncDisplayKit/ASSectionController.h b/AsyncDisplayKit/ASSectionController.h new file mode 100644 index 0000000000..dbf39d93c0 --- /dev/null +++ b/AsyncDisplayKit/ASSectionController.h @@ -0,0 +1,69 @@ +// +// ASSectionController.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/19/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A protocol that your section controllers should conform to, + * in addition to IGListSectionType, in order to be used with AsyncDisplayKit. + * + * @note Your supplementary view source should conform to @c ASSupplementaryNodeSource. + */ +@protocol ASSectionController + +/** + * A method to provide the node block for the item at the given index. + * The node block you return will be run asynchronously off the main thread, + * so it's important to retrieve any objects from your section _outside_ the block + * because by the time the block is run, the array may have changed. + * + * @param index The index of the item. + * @return A block to be run concurrently to build the node for this item. + * @see collectionNode:nodeBlockForItemAtIndexPath: + */ +- (ASCellNodeBlock)nodeBlockForItemAtIndex:(NSInteger)index; + +@optional + +/** + * Asks the section controller whether it should batch fetch because the user is + * near the end of the current data set. + * + * @discussion Use this method to conditionally fetch batches. Example use cases are: limiting the total number of + * objects that can be fetched or no network connection. + * + * If not implemented, the assumed return value is @c YES. + */ +- (BOOL)shouldBatchFetch; + +/** + * Asks the section controller to begin fetching more content (tail loading) because + * the user is near the end of the current data set. + * + * @param context A context object that must be notified when the batch fetch is completed. + * + * @discussion You must eventually call -completeBatchFetching: with an argument of YES in order to receive future + * notifications to do batch fetches. This method is called on a background queue. + */ +- (void)beginBatchFetchWithContext:(ASBatchContext *)context; + +/** + * A method to provide the size range used for measuring the item + * at the given index. + * + * @param index The index of the item. + * @return A size range used for asynchronously measuring the node at this index. + * @see collectionNode:constrainedSizeForItemAtIndexPath: + */ +- (ASSizeRange)sizeRangeForItemAtIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASSupplementaryNodeSource.h b/AsyncDisplayKit/ASSupplementaryNodeSource.h new file mode 100644 index 0000000000..ea629b748b --- /dev/null +++ b/AsyncDisplayKit/ASSupplementaryNodeSource.h @@ -0,0 +1,40 @@ +// +// ASSupplementaryNodeSource.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/19/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASSupplementaryNodeSource + +/** + * A method to provide the node for the item at the given index. + * + * @param elementKind The kind of supplementary element. + * @param index The index of the item. + * @return A node for the supplementary element. + * @see collectionNode:nodeForSupplementaryElementOfKind:atIndexPath: + */ +- (ASCellNode *)nodeForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index; + +@optional + +/** + * A method to provide the size range used for measuring the supplementary + * element of the given kind at the given index. + * + * @param elementKind The kind of supplementary element. + * @param index The index of the item. + * @return A size range used for asynchronously measuring the node. + * @see collectionNode:constrainedSizeForSupplementaryElementOfKind:atIndexPath: + */ +- (ASSizeRange)sizeRangeForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch index 0062314263..64ec2a6a4e 100644 --- a/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch +++ b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch @@ -17,3 +17,7 @@ // a user does not include the framework in the link binary with build step). #define PIN_REMOTE_IMAGE __has_include() #endif + +#ifndef IG_LIST_KIT +#define IG_LIST_KIT __has_include() +#endif diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index 79ef39325d..c4fe087322 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -34,6 +34,13 @@ #import #import +#import +#import +#if IG_LIST_KIT +#import +#import +#endif + #import #import diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.h b/AsyncDisplayKit/Details/ASCollectionDataController.h index 34e7e6f872..5d7e4c6346 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.h +++ b/AsyncDisplayKit/Details/ASCollectionDataController.h @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; -- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController; +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController sections:(NSIndexSet *)sections; - (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm index bdbdd9801f..b56333a753 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.mm +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -17,6 +17,7 @@ #import "ASIndexedNodeContext.h" #import "ASSection.h" #import "ASSectionContext.h" +#import "NSIndexSet+ASHelpers.h" //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) @@ -26,6 +27,13 @@ NSInteger _nextSectionID; NSMutableArray *_sections; NSArray *_pendingSections; + + /** + * supplementaryKinds can only be accessed on the main thread + * and so we set this in the -prepare stage, and then read it during the -will + * stage of each update operation. + */ + NSArray *_supplementaryKindsForPendingOperation; } - (id)collectionDataSource; @@ -59,7 +67,7 @@ [_sections removeAllObjects]; [self _populatePendingSectionsFromDataSource:sections]; - for (NSString *kind in [self supplementaryKinds]) { + for (NSString *kind in [self supplementaryKindsInSections:sections]) { LOG(@"Populating elements of kind: %@", kind); NSMutableArray *contexts = [NSMutableArray array]; [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableContexts:contexts]; @@ -101,7 +109,7 @@ ASDisplayNodeAssertMainThread(); [self _populatePendingSectionsFromDataSource:sections]; - for (NSString *kind in [self supplementaryKinds]) { + for (NSString *kind in [self supplementaryKindsInSections:sections]) { LOG(@"Populating elements of kind: %@, for sections: %@", kind, sections); NSMutableArray *contexts = [NSMutableArray array]; [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableContexts:contexts]; @@ -127,46 +135,29 @@ [_pendingNodeContexts removeAllObjects]; } +- (void)prepareForDeleteSections:(NSIndexSet *)sections +{ + _supplementaryKindsForPendingOperation = [self supplementaryKindsInSections:sections]; +} + - (void)willDeleteSections:(NSIndexSet *)sections { [_sections removeObjectsAtIndexes:sections]; - - for (NSString *kind in [self supplementaryKinds]) { + + for (NSString *kind in _supplementaryKindsForPendingOperation) { NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; [self deleteSectionsOfKind:kind atIndexSet:sections completion:nil]; } -} - -- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection -{ - ASSection *movedSection = [_sections objectAtIndex:section]; - [_sections removeObjectAtIndex:section]; - [_sections insertObject:movedSection atIndex:newSection]; - - NSIndexSet *sectionAsIndexSet = [NSIndexSet indexSetWithIndex:section]; - for (NSString *kind in [self supplementaryKinds]) { - NSMutableArray *editingNodes = [self editingNodesOfKind:kind]; - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(editingNodes, sectionAsIndexSet); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths); - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - - // update the section of indexpaths - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - for (NSIndexPath *indexPath in indexPaths) { - NSUInteger newItem = [indexPath indexAtPosition:indexPath.length - 1]; - NSIndexPath *mappedIndexPath = [NSIndexPath indexPathForItem:newItem inSection:newSection]; - [updatedIndexPaths addObject:mappedIndexPath]; - } - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; - } + _supplementaryKindsForPendingOperation = nil; } - (void)prepareForInsertRowsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); - for (NSString *kind in [self supplementaryKinds]) { + NSIndexSet *sections = [NSIndexSet as_sectionsFromIndexPaths:indexPaths]; + for (NSString *kind in [self supplementaryKindsInSections:sections]) { LOG(@"Populating elements of kind: %@, for index paths: %@", kind, indexPaths); NSMutableArray *contexts = [NSMutableArray array]; [self _populateSupplementaryNodesOfKind:kind atIndexPaths:indexPaths mutableContexts:contexts]; @@ -188,7 +179,9 @@ - (void)prepareForDeleteRowsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); - for (NSString *kind in [self supplementaryKinds]) { + NSIndexSet *sections = [NSIndexSet as_sectionsFromIndexPaths:indexPaths]; + _supplementaryKindsForPendingOperation = [self supplementaryKindsInSections:sections]; + for (NSString *kind in _supplementaryKindsForPendingOperation) { NSMutableArray *contexts = [NSMutableArray array]; [self _populateSupplementaryNodesOfKind:kind atIndexPaths:indexPaths mutableContexts:contexts]; _pendingNodeContexts[kind] = contexts; @@ -197,7 +190,7 @@ - (void)willDeleteRowsAtIndexPaths:(NSArray *)indexPaths { - for (NSString *kind in [self supplementaryKinds]) { + for (NSString *kind in _supplementaryKindsForPendingOperation) { NSArray *deletedIndexPaths = ASIndexPathsInMultidimensionalArrayIntersectingIndexPaths([self editingNodesOfKind:kind], indexPaths); [self deleteNodesOfKind:kind atIndexPaths:deletedIndexPaths completion:nil]; @@ -216,6 +209,7 @@ }]; } [_pendingNodeContexts removeAllObjects]; + _supplementaryKindsForPendingOperation = nil; } - (void)_populatePendingSectionsFromDataSource:(NSIndexSet *)sectionIndexes @@ -323,9 +317,9 @@ #pragma mark - Private Helpers -- (NSArray *)supplementaryKinds +- (NSArray *)supplementaryKindsInSections:(NSIndexSet *)sections { - return [self.collectionDataSource supplementaryNodeKindsInDataController:self]; + return [self.collectionDataSource supplementaryNodeKindsInDataController:self sections:sections]; } - (id)collectionDataSource diff --git a/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m index c6f50778d1..a318ff859d 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m +++ b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m @@ -66,24 +66,32 @@ - (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { + ASSizeRange result = ASSizeRangeUnconstrained; if (_delegateFlags.implementsConstrainedSizeForItemAtIndexPath) { - return [collectionView.asyncDelegate collectionNode:collectionView.collectionNode constrainedSizeForItemAtIndexPath:indexPath]; + result = [collectionView.asyncDelegate collectionNode:collectionView.collectionNode constrainedSizeForItemAtIndexPath:indexPath]; } else if (_delegateFlags.implementsConstrainedSizeForNodeAtIndexPathDeprecated) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - return [collectionView.asyncDelegate collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]; + result = [collectionView.asyncDelegate collectionView:collectionView constrainedSizeForNodeAtIndexPath:indexPath]; #pragma clang diagnostic pop } else { // With 2.0 `collectionView:constrainedSizeForNodeAtIndexPath:` was moved to the delegate. Assert if not implemented on the delegate but on the data source ASDisplayNodeAssert([collectionView.asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)] == NO, @"collectionView:constrainedSizeForNodeAtIndexPath: was moved from the ASCollectionDataSource to the ASCollectionDelegate."); } - - CGSize itemSize = _layout.itemSize; - if (CGSizeEqualToSize(itemSize, kDefaultItemSize) == NO) { - return ASSizeRangeMake(itemSize, itemSize); + + // If we got no size range: + if (ASSizeRangeEqualToSizeRange(result, ASSizeRangeUnconstrained)) { + // Use itemSize if they set it. + CGSize itemSize = _layout.itemSize; + if (CGSizeEqualToSize(itemSize, kDefaultItemSize) == NO) { + result = ASSizeRangeMake(itemSize, itemSize); + } else { + // Compute constraint from scroll direction otherwise. + result = NodeConstrainedSizeForScrollDirection(collectionView); + } } - return NodeConstrainedSizeForScrollDirection(collectionView); + return result; } - (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m b/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m index 960619b464..d8ce211c66 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m @@ -10,6 +10,7 @@ #import "ASCollectionView.h" #import "ASCollectionView+Undeprecated.h" +#import "ASCollectionInternal.h" #pragma mark - Helper Functions diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 8c31a565ee..eafe89ea9b 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -719,6 +719,8 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat [_nodeContexts[ASDataControllerRowNodeKind] removeObjectsAtIndexes:sections]; dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + [self prepareForDeleteSections:sections]; dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ [self willDeleteSections:sections]; @@ -745,6 +747,11 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat // Optional template hook for subclasses (See ASDataController+Subclasses.h) } +- (void)prepareForDeleteSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + - (void)willInsertSections:(NSIndexSet *)sections { // Optional template hook for subclasses (See ASDataController+Subclasses.h) diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h index 179e685639..d6d3238831 100644 --- a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h @@ -22,4 +22,7 @@ - (NSString *)as_smallDescription; +/// Returns all the section indexes contained in the index paths array. ++ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray *)indexPaths; + @end diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m index de3314c07a..db7052a514 100644 --- a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m @@ -77,4 +77,13 @@ return result; } ++ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray *)indexPaths +{ + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + for (NSIndexPath *indexPath in indexPaths) { + [result addIndex:indexPath.section]; + } + return result; +} + @end diff --git a/AsyncDisplayKit/Details/_ASCollectionViewCell.h b/AsyncDisplayKit/Details/_ASCollectionViewCell.h new file mode 100644 index 0000000000..c4b5abefd5 --- /dev/null +++ b/AsyncDisplayKit/Details/_ASCollectionViewCell.h @@ -0,0 +1,17 @@ +// +// _ASCollectionViewCell.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/30/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +@class ASCellNode; + +@interface _ASCollectionViewCell : UICollectionViewCell +@property (nonatomic, weak) ASCellNode *node; +@property (nonatomic, strong) UICollectionViewLayoutAttributes *layoutAttributes; +@end + diff --git a/AsyncDisplayKit/Details/_ASCollectionViewCell.m b/AsyncDisplayKit/Details/_ASCollectionViewCell.m new file mode 100644 index 0000000000..1b731d22c8 --- /dev/null +++ b/AsyncDisplayKit/Details/_ASCollectionViewCell.m @@ -0,0 +1,74 @@ +// +// _ASCollectionViewCell.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/30/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import "_ASCollectionViewCell.h" +#import "ASCellNode+Internal.h" +#import + +@implementation _ASCollectionViewCell + +- (void)setNode:(ASCellNode *)node +{ + ASDisplayNodeAssertMainThread(); + node.layoutAttributes = _layoutAttributes; + _node = node; + self.backgroundColor = node.backgroundColor; + self.clipsToBounds = node.clipsToBounds; + [node __setSelectedFromUIKit:self.selected]; + [node __setHighlightedFromUIKit:self.highlighted]; +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [_node __setSelectedFromUIKit:selected]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [_node __setHighlightedFromUIKit:highlighted]; +} + +- (void)setLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + _layoutAttributes = layoutAttributes; + _node.layoutAttributes = layoutAttributes; +} + +- (void)prepareForReuse +{ + self.layoutAttributes = nil; + + // Need to clear node pointer before UIKit calls setSelected:NO / setHighlighted:NO on its cells + self.node = nil; + [super prepareForReuse]; +} + +/** + * In the initial case, this is called by UICollectionView during cell dequeueing, before + * we get a chance to assign a node to it, so we must be sure to set these layout attributes + * on our node when one is next assigned to us in @c setNode: . Since there may be cases when we _do_ already + * have our node assigned e.g. during a layout update for existing cells, we also attempt + * to update it now. + */ +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + self.layoutAttributes = layoutAttributes; +} + +/** + * Keep our node filling our content view. + */ +- (void)layoutSubviews +{ + [super layoutSubviews]; + self.node.frame = self.contentView.bounds; +} + +@end diff --git a/AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.h b/AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.h new file mode 100644 index 0000000000..ae19e74008 --- /dev/null +++ b/AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.h @@ -0,0 +1,34 @@ +// +// IGListAdapter+AsyncDisplayKit.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/19/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#if IG_LIST_KIT + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionNode; + +@interface IGListAdapter (AsyncDisplayKit) + +/** + * Connect this list adapter to the given collection node. + * + * @param collectionNode The collection node to drive with this list adapter. + * + * @note This method may only be called once per list adapter, + * and it must be called on the main thread. -[UIViewController init] + * is a good place to call it. This method does not retain the collection node. + */ +- (void)setASDKCollectionNode:(ASCollectionNode *)collectionNode; + +@end + +NS_ASSUME_NONNULL_END + +#endif // IG_LIST_KIT diff --git a/AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.m b/AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.m new file mode 100644 index 0000000000..46b5ac1736 --- /dev/null +++ b/AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.m @@ -0,0 +1,45 @@ +// +// IGListAdapter+AsyncDisplayKit.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/19/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#if IG_LIST_KIT + +#import "IGListAdapter+AsyncDisplayKit.h" +#import "ASIGListAdapterBasedDataSource.h" +#import "ASAssert.h" +#import + +@implementation IGListAdapter (AsyncDisplayKit) + +- (void)setASDKCollectionNode:(ASCollectionNode *)collectionNode +{ + ASDisplayNodeAssertMainThread(); + + // Attempt to retrieve previous data source. + ASIGListAdapterBasedDataSource *dataSource = objc_getAssociatedObject(self, _cmd); + // Bomb if we already made one. + if (dataSource != nil) { + ASDisplayNodeFailAssert(@"Attempt to call %@ multiple times on the same list adapter. Not currently allowed!", NSStringFromSelector(_cmd)); + return; + } + + // Make a data source and retain it. + dataSource = [[ASIGListAdapterBasedDataSource alloc] initWithListAdapter:self]; + objc_setAssociatedObject(self, _cmd, dataSource, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // Attach the data source to the collection node. + collectionNode.dataSource = dataSource; + collectionNode.delegate = dataSource; + __weak IGListAdapter *weakSelf = self; + [collectionNode onDidLoad:^(__kindof ASCollectionNode * _Nonnull collectionNode) { + weakSelf.collectionView = collectionNode.view; + }]; +} + +@end + +#endif // IG_LIST_KIT diff --git a/AsyncDisplayKit/Private/ASCollectionInteropProtocols.h b/AsyncDisplayKit/Private/ASCollectionInteropProtocols.h new file mode 100644 index 0000000000..a9c1c3274b --- /dev/null +++ b/AsyncDisplayKit/Private/ASCollectionInteropProtocols.h @@ -0,0 +1,34 @@ +// +// ASCollectionDataSourceInterop.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/20/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Protocols that allow the data source/delegate extra hooks, + * to facilitate interop e.g. with IGListKit. + */ + +@protocol ASCollectionDataSourceInterop + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +@end + +@protocol ASCollectionDelegateInterop + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath; + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Private/ASDataController+Subclasses.h b/AsyncDisplayKit/Private/ASDataController+Subclasses.h index cf2f1b5b91..141ce0c9f7 100644 --- a/AsyncDisplayKit/Private/ASDataController+Subclasses.h +++ b/AsyncDisplayKit/Private/ASDataController+Subclasses.h @@ -113,6 +113,17 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray *nodes, NS */ - (void)willInsertSections:(NSIndexSet *)sections; +/** + * Notifies the subclass to perform setup before sections are deleted 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)prepareForDeleteSections:(NSIndexSet *)sections; + /** * Notifies the subclass that the data controller will delete sections at the given positions * @@ -124,18 +135,6 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray *nodes, NS */ - (void)willDeleteSections:(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; - /** * Notifies the subclass to perform setup before rows are inserted in the data controller. * diff --git a/AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.h b/AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.h new file mode 100644 index 0000000000..7dc0ba6e15 --- /dev/null +++ b/AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.h @@ -0,0 +1,22 @@ +// +// ASIGListAdapterBasedDataSource.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/19/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#if IG_LIST_KIT + +#import +#import +#import "ASCollectionInteropProtocols.h" + +AS_SUBCLASSING_RESTRICTED +@interface ASIGListAdapterBasedDataSource : NSObject + +- (instancetype)initWithListAdapter:(IGListAdapter *)listAdapter; + +@end + +#endif diff --git a/AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.m b/AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.m new file mode 100644 index 0000000000..7f0cfbd022 --- /dev/null +++ b/AsyncDisplayKit/Private/ASIGListAdapterBasedDataSource.m @@ -0,0 +1,315 @@ +// +// ASIGListAdapterBasedDataSource.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 1/19/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#if IG_LIST_KIT + +#import "ASIGListAdapterBasedDataSource.h" +#import +#import + +typedef IGListSectionController ASIGSectionController; + +/// The optional methods that a class implements from ASSectionController. +/// Note: Bitfields are not supported by NSValue so we can't use them. +typedef struct { + BOOL sizeRangeForItem; + BOOL shouldBatchFetch; + BOOL beginBatchFetchWithContext; +} ASSectionControllerOverrides; + +/// The optional methods that a class implements from ASSupplementaryNodeSource. +/// Note: Bitfields are not supported by NSValue so we can't use them. +typedef struct { + BOOL sizeRangeForSupplementary; +} ASSupplementarySourceOverrides; + +@protocol ASIGSupplementaryNodeSource +@end + +@interface ASIGListAdapterBasedDataSource () +@property (nonatomic, weak, readonly) IGListAdapter *listAdapter; +@property (nonatomic, readonly) id delegate; +@property (nonatomic, readonly) id dataSource; + +/** + * The section controller that we will forward beginBatchFetchWithContext: to. + * Since shouldBatchFetch: is called on main, we capture the last section controller in there, + * and then we use it and clear it in beginBatchFetchWithContext: (on default queue). + * + * It is safe to use it without a lock in this limited way, since those two methods will + * never execute in parallel.6 + */ +@property (nonatomic, weak) ASIGSectionController *sectionControllerForBatchFetching; +@end + +@implementation ASIGListAdapterBasedDataSource + +- (instancetype)initWithListAdapter:(IGListAdapter *)listAdapter +{ + if (self = [super init]) { + [ASIGListAdapterBasedDataSource setASCollectionViewSuperclass]; + [ASIGListAdapterBasedDataSource configureUpdater:listAdapter.updater]; + + ASDisplayNodeAssert([listAdapter conformsToProtocol:@protocol(UICollectionViewDataSource)], @"Expected IGListAdapter to conform to UICollectionViewDataSource."); + ASDisplayNodeAssert([listAdapter conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)], @"Expected IGListAdapter to conform to UICollectionViewDelegateFlowLayout."); + _listAdapter = listAdapter; + } + return self; +} + +- (id)dataSource +{ + return (id)_listAdapter; +} + +- (id)delegate +{ + return (id)_listAdapter; +} + +#pragma mark - ASCollectionDelegate + +- (void)collectionNode:(ASCollectionNode *)collectionNode didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionNode.view didSelectItemAtIndexPath:indexPath]; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + [self.delegate scrollViewDidScroll:scrollView]; +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + [self.delegate scrollViewWillBeginDragging:scrollView]; +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate +{ + [self.delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; +} + +- (BOOL)shouldBatchFetchForCollectionNode:(ASCollectionNode *)collectionNode +{ + NSInteger sectionCount = [self numberOfSectionsInCollectionNode:collectionNode]; + if (sectionCount == 0) { + return NO; + } + + // If they implement shouldBatchFetch, call it. Otherwise, just say YES if they implement beginBatchFetch. + ASIGSectionController *ctrl = [self sectionControllerForSection:sectionCount - 1]; + ASSectionControllerOverrides o = [ASIGListAdapterBasedDataSource overridesForSectionControllerClass:ctrl.class]; + BOOL result = (o.shouldBatchFetch ? [ctrl shouldBatchFetch] : o.beginBatchFetchWithContext); + if (result) { + self.sectionControllerForBatchFetching = ctrl; + } + return result; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + ASIGSectionController *ctrl = self.sectionControllerForBatchFetching; + self.sectionControllerForBatchFetching = nil; + [ctrl beginBatchFetchWithContext:context]; +} + +/** + * Note: It is not documented that ASCollectionNode will forward these UIKit delegate calls if they are implemented. + * It is not considered harmful to do so, and adding them to documentation will confuse most users, who should + * instead using the ASCollectionDelegate callbacks. + */ +#pragma mark - ASCollectionDelegateInterop + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionView didEndDisplayingCell:cell forItemAtIndexPath:indexPath]; +} + +#pragma mark - ASCollectionDelegateFlowLayout + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForHeaderInSection:(NSInteger)section +{ + id src = [self supplementaryElementSourceForSection:section]; + if ([ASIGListAdapterBasedDataSource overridesForSupplementarySourceClass:[src class]].sizeRangeForSupplementary) { + return [src sizeRangeForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndex:0]; + } else { + return ASSizeRangeZero; + } +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForFooterInSection:(NSInteger)section +{ + id src = [self supplementaryElementSourceForSection:section]; + if ([ASIGListAdapterBasedDataSource overridesForSupplementarySourceClass:[src class]].sizeRangeForSupplementary) { + return [src sizeRangeForSupplementaryElementOfKind:UICollectionElementKindSectionFooter atIndex:0]; + } else { + return ASSizeRangeZero; + } +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section +{ + return [self.delegate collectionView:collectionView layout:collectionViewLayout insetForSectionAtIndex:section]; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section +{ + return [self.delegate collectionView:collectionView layout:collectionViewLayout minimumLineSpacingForSectionAtIndex:section]; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section +{ + return [self.delegate collectionView:collectionView layout:collectionViewLayout minimumInteritemSpacingForSectionAtIndex:section]; +} + +#pragma mark - ASCollectionDataSource + +- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section +{ + return [self.dataSource collectionView:collectionNode.view numberOfItemsInSection:section]; +} + +- (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode +{ + return [self.dataSource numberOfSectionsInCollectionView:collectionNode.view]; +} + +- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return [[self sectionControllerForSection:indexPath.section] nodeBlockForItemAtIndex:indexPath.item]; +} + +- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASIGSectionController *ctrl = [self sectionControllerForSection:indexPath.section]; + if ([ASIGListAdapterBasedDataSource overridesForSectionControllerClass:ctrl.class].sizeRangeForItem) { + return [ctrl sizeRangeForItemAtIndex:indexPath.item]; + } else { + return ASSizeRangeUnconstrained; + } +} + +- (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + return [[self supplementaryElementSourceForSection:indexPath.section] nodeForSupplementaryElementOfKind:kind atIndex:indexPath.item]; +} + +- (NSArray *)collectionNode:(ASCollectionNode *)collectionNode supplementaryElementKindsInSection:(NSInteger)section +{ + return [[self supplementaryElementSourceForSection:section] supportedElementKinds]; +} + +#pragma mark - ASCollectionDataSourceInterop + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + return [self.dataSource collectionView:collectionView cellForItemAtIndexPath:indexPath]; +} + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + return [self.dataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; +} + +#pragma mark - Helpers + +- (id)supplementaryElementSourceForSection:(NSInteger)section +{ + ASIGSectionController *ctrl = [self sectionControllerForSection:section]; + id src = (id)ctrl.supplementaryViewSource; + ASDisplayNodeAssert(src == nil || [src conformsToProtocol:@protocol(ASSupplementaryNodeSource)], @"Supplementary view source should conform to %@", NSStringFromProtocol(@protocol(ASSupplementaryNodeSource))); + return src; +} + +- (ASIGSectionController *)sectionControllerForSection:(NSInteger)section +{ + id object = [_listAdapter objectAtSection:section]; + ASIGSectionController *ctrl = (ASIGSectionController *)[_listAdapter sectionControllerForObject:object]; + ASDisplayNodeAssert([ctrl conformsToProtocol:@protocol(ASSectionController)], @"Expected section controller to conform to %@. Controller: %@", NSStringFromProtocol(@protocol(ASSectionController)), ctrl); + return ctrl; +} + +/** + * Set ASCollectionView's superclass to IGListCollectionView. + * Scary! If IGListKit removed the subclassing restriction, we could + * use #if in the @interface to choose the superclass based on + * whether we have IGListKit available. + */ ++ (void)setASCollectionViewSuperclass +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + class_setSuperclass([ASCollectionView class], [IGListCollectionView class]); + }); +#pragma clang diagnostic pop +} + +/// Ensure updater won't call reloadData on us. ++ (void)configureUpdater:(id)updater +{ + // Cast to NSObject will be removed after https://github.com/Instagram/IGListKit/pull/435 + if ([(id)updater isKindOfClass:[IGListAdapterUpdater class]]) { + [(IGListAdapterUpdater *)updater setAllowsBackgroundReloading:NO]; + } else { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSLog(@"WARNING: Use of non-%@ updater with AsyncDisplayKit is discouraged. Updater: %@", NSStringFromClass([IGListAdapterUpdater class]), updater); + }); + } +} + ++ (ASSupplementarySourceOverrides)overridesForSupplementarySourceClass:(Class)c +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [[NSCache alloc] init]; + }); + NSValue *obj = [cache objectForKey:c]; + ASSupplementarySourceOverrides o; + if (obj == nil) { + o.sizeRangeForSupplementary = [c instancesRespondToSelector:@selector(sizeRangeForSupplementaryElementOfKind:atIndex:)]; + obj = [NSValue valueWithBytes:&o objCType:@encode(ASSupplementarySourceOverrides)]; + [cache setObject:obj forKey:c]; + } else { + [obj getValue:&o]; + } + return o; +} + ++ (ASSectionControllerOverrides)overridesForSectionControllerClass:(Class)c +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [[NSCache alloc] init]; + }); + NSValue *obj = [cache objectForKey:c]; + ASSectionControllerOverrides o; + if (obj == nil) { + o.sizeRangeForItem = [c instancesRespondToSelector:@selector(sizeRangeForItemAtIndex:)]; + o.beginBatchFetchWithContext = [c instancesRespondToSelector:@selector(beginBatchFetchWithContext:)]; + o.shouldBatchFetch = [c instancesRespondToSelector:@selector(shouldBatchFetch)]; + obj = [NSValue valueWithBytes:&o objCType:@encode(ASSectionControllerOverrides)]; + [cache setObject:obj forKey:c]; + } else { + [obj getValue:&o]; + } + return o; +} + +@end + +#endif // IG_LIST_KIT diff --git a/AsyncDisplayKitTests/ASCollectionViewTests.mm b/AsyncDisplayKitTests/ASCollectionViewTests.mm index 077a07125b..57570490a7 100644 --- a/AsyncDisplayKitTests/ASCollectionViewTests.mm +++ b/AsyncDisplayKitTests/ASCollectionViewTests.mm @@ -164,7 +164,7 @@ @interface ASCollectionView (InternalTesting) -- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController; +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController sections:(nonnull NSIndexSet *)sections; @end @@ -219,7 +219,7 @@ UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; [collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; - XCTAssertEqualObjects([collectionView supplementaryNodeKindsInDataController:nil], @[UICollectionElementKindSectionHeader]); + XCTAssertEqualObjects([collectionView supplementaryNodeKindsInDataController:nil sections:[NSIndexSet indexSetWithIndex:0]], @[UICollectionElementKindSectionHeader]); } - (void)testReloadIfNeeded diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index 689da37e4e..8504598a17 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -2144,11 +2144,16 @@ static bool stringContainsPointer(NSString *description, id p) { XCTAssertNoThrow([node.view layoutIfNeeded]); } -- (void)testThatOnDidLoadThrowsIfCalledOnLoaded +- (void)testThatOnDidLoadThrowsIfCalledOnLoadedOffMain { ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; [node view]; - XCTAssertThrows([node onDidLoad:^(ASDisplayNode * _Nonnull node) { }]); + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [NSThread detachNewThreadWithBlock:^{ + XCTAssertThrows([node onDidLoad:^(ASDisplayNode * _Nonnull node) { }]); + dispatch_semaphore_signal(sem); + }]; + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); } - (void)testThatOnDidLoadWorks diff --git a/build.sh b/build.sh index 4a3e043815..478386d4c5 100755 --- a/build.sh +++ b/build.sh @@ -30,7 +30,20 @@ if [ "$MODE" = "tests" ]; then -scheme AsyncDisplayKit \ -sdk "$SDK" \ -destination "$PLATFORM" \ - build test | xcpretty $FORMATTER + build-for-testing test | xcpretty $FORMATTER + trap - EXIT + exit 0 +fi + +if [ "$MODE" = "tests_listkit" ]; then + echo "Building & testing AsyncDisplayKit+IGListKit." + pod install --project-directory=ASDKListKit + set -o pipefail && xcodebuild \ + -workspace ASDKListKit/ASDKListKit.xcworkspace \ + -scheme ASDKListKitTests \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build-for-testing test | xcpretty $FORMATTER trap - EXIT exit 0 fi diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index e4c94105dd..d1718e2f9f 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -21,7 +21,7 @@ #import "SupplementaryNode.h" #import "ItemNode.h" -@interface ViewController () +@interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @property (nonatomic, strong) NSArray *data; diff --git a/examples/ASDKgram/Podfile b/examples/ASDKgram/Podfile index 5c30ce798e..c9b4d0ee61 100644 --- a/examples/ASDKgram/Podfile +++ b/examples/ASDKgram/Podfile @@ -1,6 +1,7 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '7.0' +platform :ios, '8.0' target 'Sample' do - pod 'AsyncDisplayKit', :path => '../..' + pod 'AsyncDisplayKit/IGListKit', :path => '../..' + pod 'AsyncDisplayKit/PINRemoteImage', :path => '../..' end diff --git a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj index dbbd72d54c..1c9c4b3f57 100644 --- a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj +++ b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj @@ -31,6 +31,11 @@ 768843931CAA37EF00D8629E /* UserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7688437B1CAA37EF00D8629E /* UserModel.m */; }; 768843961CAA37EF00D8629E /* Utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 7688437E1CAA37EF00D8629E /* Utilities.m */; }; B13424EE6D36C2EC5D1030B6 /* libPods-Sample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AD5DDA0A29B0F32AA5CC47BA /* libPods-Sample.a */; }; + CC00D1571E15912F004E5502 /* PhotoFeedListKitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CC00D1561E15912F004E5502 /* PhotoFeedListKitViewController.m */; }; + CC5369AC1E15925200FAD348 /* PhotoFeedSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5369AB1E15925200FAD348 /* PhotoFeedSectionController.m */; }; + CC5532171E15CC1E0011C01F /* ASCollectionSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5532161E15CC1E0011C01F /* ASCollectionSectionController.m */; }; + CC6350BB1E1C482D002BC613 /* TailLoadingNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */; }; + CC85250F1E36B392008EABE6 /* FeedHeaderNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -80,6 +85,17 @@ 7688437F1CAA37EF00D8629E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 97A9B1BAF4265967672F9EA3 /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; AD5DDA0A29B0F32AA5CC47BA /* libPods-Sample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Sample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + CC00D1551E15912F004E5502 /* PhotoFeedListKitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoFeedListKitViewController.h; sourceTree = ""; }; + CC00D1561E15912F004E5502 /* PhotoFeedListKitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoFeedListKitViewController.m; sourceTree = ""; }; + CC5369AA1E15925200FAD348 /* PhotoFeedSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoFeedSectionController.h; sourceTree = ""; }; + CC5369AB1E15925200FAD348 /* PhotoFeedSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoFeedSectionController.m; sourceTree = ""; }; + CC5532111E159D770011C01F /* RefreshingSectionControllerType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RefreshingSectionControllerType.h; sourceTree = ""; }; + CC5532151E15CC1E0011C01F /* ASCollectionSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionSectionController.h; sourceTree = ""; }; + CC5532161E15CC1E0011C01F /* ASCollectionSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionSectionController.m; sourceTree = ""; }; + CC6350B91E1C482D002BC613 /* TailLoadingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TailLoadingNode.h; sourceTree = ""; }; + CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TailLoadingNode.m; sourceTree = ""; }; + CC85250D1E36B392008EABE6 /* FeedHeaderNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedHeaderNode.h; sourceTree = ""; }; + CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedHeaderNode.m; sourceTree = ""; }; D09B5DF0BFB37583DE8F3142 /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -169,6 +185,7 @@ children = ( 767A5F161CAA3D96004CDA8D /* UIKit */, 767A5F151CAA3D90004CDA8D /* ASDK */, + CC00D1581E159132004E5502 /* ASDK-ListKit */, ); name = Controller; sourceTree = ""; @@ -239,6 +256,10 @@ 767A5F1A1CAA3DBF004CDA8D /* ASDK */ = { isa = PBXGroup; children = ( + CC85250D1E36B392008EABE6 /* FeedHeaderNode.h */, + CC85250E1E36B392008EABE6 /* FeedHeaderNode.m */, + CC6350B91E1C482D002BC613 /* TailLoadingNode.h */, + CC6350BA1E1C482D002BC613 /* TailLoadingNode.m */, 7688435B1CAA37EF00D8629E /* PhotoCellNode.h */, 768843731CAA37EF00D8629E /* PhotoCellNode.m */, 768843541CAA37EF00D8629E /* CommentsNode.h */, @@ -247,6 +268,20 @@ name = ASDK; sourceTree = ""; }; + CC00D1581E159132004E5502 /* ASDK-ListKit */ = { + isa = PBXGroup; + children = ( + CC5532111E159D770011C01F /* RefreshingSectionControllerType.h */, + CC00D1551E15912F004E5502 /* PhotoFeedListKitViewController.h */, + CC00D1561E15912F004E5502 /* PhotoFeedListKitViewController.m */, + CC5369AA1E15925200FAD348 /* PhotoFeedSectionController.h */, + CC5369AB1E15925200FAD348 /* PhotoFeedSectionController.m */, + CC5532151E15CC1E0011C01F /* ASCollectionSectionController.h */, + CC5532161E15CC1E0011C01F /* ASCollectionSectionController.m */, + ); + name = "ASDK-ListKit"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -378,15 +413,20 @@ 768843831CAA37EF00D8629E /* CommentsNode.m in Sources */, 768843961CAA37EF00D8629E /* Utilities.m in Sources */, 768843931CAA37EF00D8629E /* UserModel.m in Sources */, + CC5532171E15CC1E0011C01F /* ASCollectionSectionController.m in Sources */, 768843801CAA37EF00D8629E /* AppDelegate.m in Sources */, 768843811CAA37EF00D8629E /* CommentFeedModel.m in Sources */, 7688438E1CAA37EF00D8629E /* PhotoFeedNodeController.m in Sources */, + CC6350BB1E1C482D002BC613 /* TailLoadingNode.m in Sources */, + CC85250F1E36B392008EABE6 /* FeedHeaderNode.m in Sources */, 768843841CAA37EF00D8629E /* CommentView.m in Sources */, 768843881CAA37EF00D8629E /* LocationModel.m in Sources */, 768843901CAA37EF00D8629E /* PhotoModel.m in Sources */, 768843911CAA37EF00D8629E /* PhotoTableViewCell.m in Sources */, + CC00D1571E15912F004E5502 /* PhotoFeedListKitViewController.m in Sources */, 7688438B1CAA37EF00D8629E /* PhotoCellNode.m in Sources */, 7688438D1CAA37EF00D8629E /* PhotoFeedModel.m in Sources */, + CC5369AC1E15925200FAD348 /* PhotoFeedSectionController.m in Sources */, 768843851CAA37EF00D8629E /* ImageURLModel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/ASDKgram/Sample/ASCollectionSectionController.h b/examples/ASDKgram/Sample/ASCollectionSectionController.h new file mode 100644 index 0000000000..f69791bc76 --- /dev/null +++ b/examples/ASDKgram/Sample/ASCollectionSectionController.h @@ -0,0 +1,28 @@ +// +// ASCollectionSectionController.h +// Sample +// +// Created by Adlai Holler on 12/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASCollectionSectionController : IGListSectionController + +/** + * The items managed by this section controller. + */ +@property (nonatomic, strong, readonly) NSArray> *items; + +- (void)setItems:(NSArray> *)newItems + animated:(BOOL)animated + completion:(nullable void(^)())completion; + +- (NSInteger)numberOfItems; + +@end + +NS_ASSUME_NONNULL_END diff --git a/examples/ASDKgram/Sample/ASCollectionSectionController.m b/examples/ASDKgram/Sample/ASCollectionSectionController.m new file mode 100644 index 0000000000..27284a1e99 --- /dev/null +++ b/examples/ASDKgram/Sample/ASCollectionSectionController.m @@ -0,0 +1,79 @@ +// +// ASCollectionSectionController.m +// Sample +// +// Created by Adlai Holler on 12/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASCollectionSectionController.h" +#import + +@interface ASCollectionSectionController () +@property (nonatomic, strong, readonly) dispatch_queue_t diffingQueue; + +/// The items that have been diffed and are waiting to be submitted to the collection view. +/// Should always be accessed on the diffing queue, and should never be accessed +/// before the initial items are read (in -numberOfItems). +@property (nonatomic, copy) NSArray *pendingItems; + +@property (nonatomic) BOOL initialItemsRead; +@end + +@implementation ASCollectionSectionController +@synthesize diffingQueue = _diffingQueue; + +- (NSInteger)numberOfItems +{ + if (_initialItemsRead == NO) { + _pendingItems = self.items; + _initialItemsRead = YES; + } + return self.items.count; +} + +- (dispatch_queue_t)diffingQueue +{ + if (_diffingQueue == nil) { + _diffingQueue = dispatch_queue_create("ASCollectionSectionController.diffingQueue", DISPATCH_QUEUE_SERIAL); + } + return _diffingQueue; +} + +- (void)setItems:(NSArray *)newItems animated:(BOOL)animated completion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + newItems = [newItems copy]; + if (!self.initialItemsRead) { + _items = newItems; + if (completion) { + completion(); + } + return; + } + + BOOL wasEmpty = (self.items.count == 0); + + dispatch_async(self.diffingQueue, ^{ + IGListIndexSetResult *result = IGListDiff(self.pendingItems, newItems, IGListDiffPointerPersonality); + self.pendingItems = newItems; + dispatch_async(dispatch_get_main_queue(), ^{ + id ctx = self.collectionContext; + [ctx performBatchAnimated:animated updates:^{ + [ctx insertInSectionController:(id)self atIndexes:result.inserts]; + [ctx deleteInSectionController:(id)self atIndexes:result.deletes]; + _items = newItems; + } completion:^(BOOL finished) { + if (completion) { + completion(); + } + // WORKAROUND for https://github.com/Instagram/IGListKit/issues/378 + if (wasEmpty) { + [(IGListAdapter *)ctx performUpdatesAnimated:NO completion:nil]; + } + }]; + }); + }); +} + +@end diff --git a/examples/ASDKgram/Sample/AppDelegate.m b/examples/ASDKgram/Sample/AppDelegate.m index fe04d8e776..ce3c4a332c 100644 --- a/examples/ASDKgram/Sample/AppDelegate.m +++ b/examples/ASDKgram/Sample/AppDelegate.m @@ -20,6 +20,7 @@ #import "AppDelegate.h" #import "PhotoFeedViewController.h" #import "PhotoFeedNodeController.h" +#import "PhotoFeedListKitViewController.h" #import "WindowWithStatusBarUnderlay.h" #import "Utilities.h" @@ -37,13 +38,19 @@ _window = [[WindowWithStatusBarUnderlay alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; _window.backgroundColor = [UIColor whiteColor]; - // UIKit Home Feed viewController & navController + // ASDK Home Feed viewController & navController PhotoFeedNodeController *asdkHomeFeedVC = [[PhotoFeedNodeController alloc] init]; UINavigationController *asdkHomeFeedNavCtrl = [[UINavigationController alloc] initWithRootViewController:asdkHomeFeedVC]; asdkHomeFeedNavCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"ASDK" image:[UIImage imageNamed:@"home"] tag:0]; asdkHomeFeedNavCtrl.hidesBarsOnSwipe = YES; - - // ASDK Home Feed viewController & navController + + // ListKit Home Feed viewController & navController + PhotoFeedListKitViewController *listKitHomeFeedVC = [[PhotoFeedListKitViewController alloc] init]; + UINavigationController *listKitHomeFeedNavCtrl = [[UINavigationController alloc] initWithRootViewController:listKitHomeFeedVC]; + listKitHomeFeedNavCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"ListKit" image:[UIImage imageNamed:@"home"] tag:0]; + listKitHomeFeedNavCtrl.hidesBarsOnSwipe = YES; + + // UIKit Home Feed viewController & navController PhotoFeedViewController *uikitHomeFeedVC = [[PhotoFeedViewController alloc] init]; UINavigationController *uikitHomeFeedNavCtrl = [[UINavigationController alloc] initWithRootViewController:uikitHomeFeedVC]; uikitHomeFeedNavCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"UIKit" image:[UIImage imageNamed:@"home"] tag:0]; @@ -51,7 +58,7 @@ // UITabBarController UITabBarController *tabBarController = [[UITabBarController alloc] init]; - tabBarController.viewControllers = @[uikitHomeFeedNavCtrl, asdkHomeFeedNavCtrl]; + tabBarController.viewControllers = @[uikitHomeFeedNavCtrl, asdkHomeFeedNavCtrl, listKitHomeFeedNavCtrl]; tabBarController.selectedViewController = asdkHomeFeedNavCtrl; tabBarController.delegate = self; [[UITabBar appearance] setTintColor:[UIColor darkBlueColor]]; diff --git a/examples/ASDKgram/Sample/FeedHeaderNode.h b/examples/ASDKgram/Sample/FeedHeaderNode.h new file mode 100644 index 0000000000..b440a05f52 --- /dev/null +++ b/examples/ASDKgram/Sample/FeedHeaderNode.h @@ -0,0 +1,13 @@ +// +// FeedHeaderNode.h +// Sample +// +// Created by Adlai Holler on 1/23/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +@interface FeedHeaderNode : ASCellNode + +@end diff --git a/examples/ASDKgram/Sample/FeedHeaderNode.m b/examples/ASDKgram/Sample/FeedHeaderNode.m new file mode 100644 index 0000000000..e0c8798aa2 --- /dev/null +++ b/examples/ASDKgram/Sample/FeedHeaderNode.m @@ -0,0 +1,35 @@ +// +// FeedHeaderNode.m +// Sample +// +// Created by Adlai Holler on 1/23/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import "FeedHeaderNode.h" +#import "Utilities.h" + +static UIEdgeInsets kFeedHeaderInset = { .top = 20, .bottom = 20, .left = 10, .right = 10 }; + +@interface FeedHeaderNode () +@property (nonatomic, strong, readonly) ASTextNode *textNode; +@end + +@implementation FeedHeaderNode + +- (instancetype)init +{ + if (self = [super init]) { + _textNode = [[ASTextNode alloc] init]; + self.automaticallyManagesSubnodes = YES; + _textNode.attributedText = [NSAttributedString attributedStringWithString:@"Latest Posts" fontSize:18 color:[UIColor darkGrayColor] firstWordColor:nil]; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:kFeedHeaderInset child:_textNode]; +} + +@end diff --git a/examples/ASDKgram/Sample/PhotoFeedListKitViewController.h b/examples/ASDKgram/Sample/PhotoFeedListKitViewController.h new file mode 100644 index 0000000000..7e5c64886d --- /dev/null +++ b/examples/ASDKgram/Sample/PhotoFeedListKitViewController.h @@ -0,0 +1,14 @@ +// +// PhotoFeedListKitViewController.h +// Sample +// +// Created by Adlai Holler on 12/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import "AppDelegate.h" + +@interface PhotoFeedListKitViewController : ASViewController + +@end diff --git a/examples/ASDKgram/Sample/PhotoFeedListKitViewController.m b/examples/ASDKgram/Sample/PhotoFeedListKitViewController.m new file mode 100644 index 0000000000..5aa50732b7 --- /dev/null +++ b/examples/ASDKgram/Sample/PhotoFeedListKitViewController.m @@ -0,0 +1,106 @@ +// +// PhotoFeedListKitViewController.m +// Sample +// +// Created by Adlai Holler on 12/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "PhotoFeedListKitViewController.h" +#import +#import "PhotoFeedModel.h" +#import "PhotoFeedSectionController.h" +#import "RefreshingSectionControllerType.h" + +@interface PhotoFeedListKitViewController () +@property (nonatomic, strong) IGListAdapter *listAdapter; +@property (nonatomic, strong) PhotoFeedModel *photoFeed; +@property (nonatomic, strong, readonly) ASCollectionNode *collectionNode; +@property (nonatomic, strong, readonly) UIActivityIndicatorView *spinner; +@property (nonatomic, strong, readonly) UIRefreshControl *refreshCtrl; +@end + +@implementation PhotoFeedListKitViewController +@synthesize spinner = _spinner; + +- (instancetype)init +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionNode *node = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; + if (self = [super initWithNode:node]) { + CGRect screenRect = [[UIScreen mainScreen] bounds]; + CGFloat screenScale = [[UIScreen mainScreen] scale]; + CGSize screenWidthImageSize = CGSizeMake(screenRect.size.width * screenScale, screenRect.size.width * screenScale); + _photoFeed = [[PhotoFeedModel alloc] initWithPhotoFeedModelType:PhotoFeedModelTypePopular imageSize:screenWidthImageSize]; + + IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; + _listAdapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:self workingRangeSize:0]; + _listAdapter.dataSource = self; + [_listAdapter setASDKCollectionNode:self.collectionNode]; + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.collectionNode.view.alwaysBounceVertical = YES; + _refreshCtrl = [[UIRefreshControl alloc] init]; + [_refreshCtrl addTarget:self action:@selector(refreshFeed) forControlEvents:UIControlEventValueChanged]; + [self.collectionNode.view addSubview:_refreshCtrl]; + _spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; +} + +- (ASCollectionNode *)collectionNode +{ + return self.node; +} + +- (void)resetAllData +{ + // nop, not used currently +} + +- (void)refreshFeed +{ + // Ask the first section controller to do the refreshing. + id secCtrl = [self.listAdapter sectionControllerForObject:self.photoFeed]; + if ([secCtrl conformsToProtocol:@protocol(RefreshingSectionControllerType)]) { + [secCtrl refreshContentWithCompletion:^{ + [self.refreshCtrl endRefreshing]; + }]; + } +} + +- (UIActivityIndicatorView *)spinner +{ + if (_spinner == nil) { + _spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [_spinner startAnimating]; + } + return _spinner; +} + +#pragma mark - IGListAdapterDataSource + +- (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter +{ + return @[ self.photoFeed ]; +} + +- (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter +{ + return self.spinner; +} + +- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object +{ + if ([object isKindOfClass:[PhotoFeedModel class]]) { + return [[PhotoFeedSectionController alloc] init]; + } else { + ASDisplayNodeFailAssert(@"Only supports objects of class PhotoFeedModel."); + return nil; + } +} + +@end diff --git a/examples/ASDKgram/Sample/PhotoFeedModel.h b/examples/ASDKgram/Sample/PhotoFeedModel.h index b0d937013b..0690cfb10a 100644 --- a/examples/ASDKgram/Sample/PhotoFeedModel.h +++ b/examples/ASDKgram/Sample/PhotoFeedModel.h @@ -18,6 +18,7 @@ // #import "PhotoModel.h" +#import typedef NS_ENUM(NSInteger, PhotoFeedModelType) { PhotoFeedModelTypePopular, @@ -25,11 +26,13 @@ typedef NS_ENUM(NSInteger, PhotoFeedModelType) { PhotoFeedModelTypeUserPhotos }; -@interface PhotoFeedModel : NSObject +@interface PhotoFeedModel : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithPhotoFeedModelType:(PhotoFeedModelType)type imageSize:(CGSize)size NS_DESIGNATED_INITIALIZER; +@property (nonatomic, readonly) NSArray *photos; + - (NSUInteger)totalNumberOfPhotos; - (NSUInteger)numberOfItemsInFeed; - (PhotoModel *)objectAtIndex:(NSUInteger)index; diff --git a/examples/ASDKgram/Sample/PhotoFeedModel.m b/examples/ASDKgram/Sample/PhotoFeedModel.m index 7fb661205f..27cf185e9d 100644 --- a/examples/ASDKgram/Sample/PhotoFeedModel.m +++ b/examples/ASDKgram/Sample/PhotoFeedModel.m @@ -47,13 +47,6 @@ NSUInteger _userID; } -#pragma mark - Properties - -- (NSMutableArray *)photos -{ - return _photos; -} - #pragma mark - Lifecycle - (instancetype)initWithPhotoFeedModelType:(PhotoFeedModelType)type imageSize:(CGSize)size @@ -92,6 +85,11 @@ #pragma mark - Instance Methods +- (NSArray *)photos +{ + return [_photos copy]; +} + - (NSUInteger)totalNumberOfPhotos { return _totalItems; @@ -186,10 +184,13 @@ // early return if reached end of pages if (_totalPages) { if (_currentPage == _totalPages) { + if (block){ + block(@[]); + } return; } } - + NSUInteger numPhotos = (numResults < 100) ? numResults : 100; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -246,4 +247,16 @@ }); } +#pragma mark - IGListDiffable + +- (id)diffIdentifier +{ + return self; +} + +- (BOOL)isEqualToDiffableObject:(id)object +{ + return self == object; +} + @end diff --git a/examples/ASDKgram/Sample/PhotoFeedSectionController.h b/examples/ASDKgram/Sample/PhotoFeedSectionController.h new file mode 100644 index 0000000000..37ca7a8f92 --- /dev/null +++ b/examples/ASDKgram/Sample/PhotoFeedSectionController.h @@ -0,0 +1,24 @@ +// +// PhotoFeedSectionController.h +// Sample +// +// Created by Adlai Holler on 12/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import +#import "RefreshingSectionControllerType.h" +#import "ASCollectionSectionController.h" + +@class PhotoFeedModel; + +NS_ASSUME_NONNULL_BEGIN + +@interface PhotoFeedSectionController : ASCollectionSectionController + +@property (nonatomic, strong, nullable) PhotoFeedModel *photoFeed; + +@end + +NS_ASSUME_NONNULL_END diff --git a/examples/ASDKgram/Sample/PhotoFeedSectionController.m b/examples/ASDKgram/Sample/PhotoFeedSectionController.m new file mode 100644 index 0000000000..a9343dc01f --- /dev/null +++ b/examples/ASDKgram/Sample/PhotoFeedSectionController.m @@ -0,0 +1,122 @@ +// +// PhotoFeedSectionController.m +// Sample +// +// Created by Adlai Holler on 12/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "PhotoFeedSectionController.h" +#import "PhotoFeedModel.h" +#import "PhotoModel.h" +#import "PhotoCellNode.h" +#import "TailLoadingNode.h" +#import "FeedHeaderNode.h" + +@interface PhotoFeedSectionController () +@property (nonatomic, strong) NSString *paginatingSpinner; +@end + +@implementation PhotoFeedSectionController + +- (instancetype)init +{ + if (self = [super init]) { + _paginatingSpinner = @"Paginating Spinner"; + self.supplementaryViewSource = self; + } + return self; +} + +#pragma mark - IGListSectionType + +- (void)didUpdateToObject:(id)object +{ + _photoFeed = object; + [self setItems:_photoFeed.photos animated:NO completion:nil]; +} + + +ASIGSectionControllerSizeForItemImplementation; +ASIGSectionControllerCellForIndexImplementation; + +- (void)didSelectItemAtIndex:(NSInteger)index +{ + // nop +} + +#pragma mark - ASSectionController + +- (ASCellNodeBlock)nodeBlockForItemAtIndex:(NSInteger)index +{ + id object = self.items[index]; + // this will be executed on a background thread - important to make sure it's thread safe + ASCellNode *(^nodeBlock)() = nil; + if (object == _paginatingSpinner) { + nodeBlock = ^{ + return [[TailLoadingNode alloc] init]; + }; + } else if ([object isKindOfClass:[PhotoModel class]]) { + PhotoModel *photoModel = object; + nodeBlock = ^{ + PhotoCellNode *cellNode = [[PhotoCellNode alloc] initWithPhotoObject:photoModel]; + return cellNode; + }; + } + + return nodeBlock; +} + +- (void)beginBatchFetchWithContext:(ASBatchContext *)context +{ + // Immediately add the loading spinner if needed. + if (self.items.count > 0) { + NSArray *newItems = [self.items arrayByAddingObject:_paginatingSpinner]; + [self setItems:newItems animated:NO completion:nil]; + } + + // Start the fetch, then update the items (removing the spinner) when they are loaded. + [_photoFeed requestPageWithCompletionBlock:^(NSArray *newPhotos){ + [self setItems:_photoFeed.photos animated:NO completion:^{ + [context completeBatchFetching:YES]; + }]; + } numResultsToReturn:20]; +} + +#pragma mark - RefreshingSectionControllerType + +- (void)refreshContentWithCompletion:(void(^)())completion +{ + [_photoFeed refreshFeedWithCompletionBlock:^(NSArray *addedItems) { + [self setItems:_photoFeed.photos animated:YES completion:completion]; + } numResultsToReturn:4]; +} + +#pragma mark - ASSupplementaryNodeSource + +- (ASCellNode *)nodeForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + ASDisplayNodeAssert([elementKind isEqualToString:UICollectionElementKindSectionHeader], nil); + return [[FeedHeaderNode alloc] init]; +} + +- (ASSizeRange)sizeRangeForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index +{ + if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) { + return ASSizeRangeUnconstrained; + } else { + return ASSizeRangeZero; + } +} + +#pragma mark - IGListSupplementaryViewSource + +- (NSArray *)supportedElementKinds +{ + return @[ UICollectionElementKindSectionHeader ]; +} + +ASIGSupplementarySourceViewForSupplementaryElementImplementation(self); +ASIGSupplementarySourceSizeForSupplementaryElementImplementation; + +@end diff --git a/examples/ASDKgram/Sample/PhotoModel.h b/examples/ASDKgram/Sample/PhotoModel.h index bfa9f944e7..daa35e94e7 100644 --- a/examples/ASDKgram/Sample/PhotoModel.h +++ b/examples/ASDKgram/Sample/PhotoModel.h @@ -17,12 +17,12 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -#import "CoreGraphics/CoreGraphics.h" #import "UserModel.h" #import "LocationModel.h" #import "CommentFeedModel.h" +#import -@interface PhotoModel : NSObject +@interface PhotoModel : NSObject @property (nonatomic, strong, readonly) NSURL *URL; @property (nonatomic, strong, readonly) NSString *photoID; diff --git a/examples/ASDKgram/Sample/PhotoModel.m b/examples/ASDKgram/Sample/PhotoModel.m index db424b8232..575a7049a4 100644 --- a/examples/ASDKgram/Sample/PhotoModel.m +++ b/examples/ASDKgram/Sample/PhotoModel.m @@ -83,11 +83,7 @@ - (NSAttributedString *)likesAttributedStringWithFontSize:(CGFloat)size { - NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; - [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; - NSString *formattedLikesNumber = [formatter stringFromNumber:[[NSNumber alloc] initWithUnsignedInteger:self.likesCount]]; - - NSString *likesString = [NSString stringWithFormat:@"♥︎ %@ likes", formattedLikesNumber]; + NSString *likesString = [NSString stringWithFormat:@"♥︎ %lu likes", (unsigned long)_likesCount]; return [NSAttributedString attributedStringWithString:likesString fontSize:size color:[UIColor darkBlueColor] firstWordColor:nil]; } @@ -102,4 +98,14 @@ return [NSString stringWithFormat:@"%@ - %@", _photoID, _descriptionText]; } -@end \ No newline at end of file +- (id)diffIdentifier +{ + return self.photoID; +} + +- (BOOL)isEqualToDiffableObject:(id)object +{ + return [self isEqual:object]; +} + +@end diff --git a/examples/ASDKgram/Sample/RefreshingSectionControllerType.h b/examples/ASDKgram/Sample/RefreshingSectionControllerType.h new file mode 100644 index 0000000000..d2ed2ee712 --- /dev/null +++ b/examples/ASDKgram/Sample/RefreshingSectionControllerType.h @@ -0,0 +1,19 @@ +// +// RefreshingSectionControllerType.h +// Sample +// +// Created by Adlai Holler on 12/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol RefreshingSectionControllerType + +- (void)refreshContentWithCompletion:(nullable void(^)())completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/examples/ASDKgram/Sample/TailLoadingNode.h b/examples/ASDKgram/Sample/TailLoadingNode.h new file mode 100644 index 0000000000..177a938e10 --- /dev/null +++ b/examples/ASDKgram/Sample/TailLoadingNode.h @@ -0,0 +1,17 @@ +// +// TailLoadingNode.h +// Sample +// +// Created by Adlai Holler on 1/3/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +/** + * A node that shows a UIActivityIndicatorView, useful for putting at the end of a + * list while the next page is loading. + */ +@interface TailLoadingNode : ASCellNode + +@end diff --git a/examples/ASDKgram/Sample/TailLoadingNode.m b/examples/ASDKgram/Sample/TailLoadingNode.m new file mode 100644 index 0000000000..9f096db706 --- /dev/null +++ b/examples/ASDKgram/Sample/TailLoadingNode.m @@ -0,0 +1,35 @@ +// +// TailLoadingNode.m +// Sample +// +// Created by Adlai Holler on 1/3/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import "TailLoadingNode.h" + +@interface TailLoadingNode () +@property (nonatomic, strong) ASDisplayNode *activityIndicatorNode; +@end + +@implementation TailLoadingNode + +- (instancetype)init +{ + if (self = [super init]) { + _activityIndicatorNode = [[ASDisplayNode alloc] initWithViewBlock:^{ + UIActivityIndicatorView *v = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [v startAnimating]; + return v; + }]; + self.style.height = ASDimensionMake(100); + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionMinimumXY child:self.activityIndicatorNode]; +} + +@end