From 3c8d4e951790247505c4f9393f9136b2b457dc97 Mon Sep 17 00:00:00 2001 From: Nadine Salter Date: Thu, 13 Nov 2014 18:59:18 -0800 Subject: [PATCH] ASMultiplexImageNode. Initial open-source release of ASMultiplexImageNode. Documentation and example code forthcoming. Note: ASMultiplexImageNode requires Xcode 6 to compile. Tests are now compiled against the iOS 8 SDK and run on iOS 7.1 and iOS 8. --- .travis.yml | 11 +- AsyncDisplayKit.podspec | 3 + AsyncDisplayKit.xcodeproj/project.pbxproj | 44 +- AsyncDisplayKit/ASMultiplexImageNode.h | 197 ++++++ AsyncDisplayKit/ASMultiplexImageNode.mm | 652 ++++++++++++++++++ AsyncDisplayKit/AsyncDisplayKit.h | 2 + .../{Private => Details}/ASImageProtocols.h | 0 .../ASMultiplexImageNodeTests.m | 350 ++++++++++ .../TestResources/logo-square.png | Bin 0 -> 66422 bytes Base/ASAvailability.h | 40 ++ Base/ASLog.h | 15 + 11 files changed, 1304 insertions(+), 10 deletions(-) create mode 100644 AsyncDisplayKit/ASMultiplexImageNode.h create mode 100644 AsyncDisplayKit/ASMultiplexImageNode.mm rename AsyncDisplayKit/{Private => Details}/ASImageProtocols.h (100%) create mode 100644 AsyncDisplayKitTests/ASMultiplexImageNodeTests.m create mode 100755 AsyncDisplayKitTests/TestResources/logo-square.png create mode 100644 Base/ASAvailability.h create mode 100644 Base/ASLog.h diff --git a/.travis.yml b/.travis.yml index a40689733c..428d6c20c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,8 @@ before_install: - brew update - brew reinstall xctool - gem update cocoapods -xcode_workspace: AsyncDisplayKit.xcworkspace -xcode_scheme: AsyncDisplayKit -xcode_sdk: - - iphonesimulator7.0 - - iphonesimulator7.1 - - iphonesimulator8.0 + - xcrun simctl list +env: + - TEST_OS=7.1 + - TEST_OS=8.0 +script: xctool -workspace AsyncDisplayKit.xcworkspace -scheme AsyncDisplayKit -sdk iphonesimulator8.0 -destination "platform=iOS Simulator,OS=${TEST_OS},name=iPhone 5" build test diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index c05542e90d..1937c066e5 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -20,6 +20,9 @@ Pod::Spec.new do |spec| 'Base/*.{h,m}' ] + spec.frameworks = 'AssetsLibrary' + spec.weak_frameworks = 'Photos' + # ASDealloc2MainObject must be compiled with MRR spec.requires_arc = true spec.exclude_files = ['AsyncDisplayKit/Details/ASDealloc2MainObject.m'] diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 710ca19756..042ca1778e 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -7,6 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 0515EA211A15769900BA8B9A /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 0515EA221A1576A100BA8B9A /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; }; + 0516FA3C1A15563400B4EBED /* ASAvailability.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3A1A15563400B4EBED /* ASAvailability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0516FA3D1A15563400B4EBED /* ASLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3B1A15563400B4EBED /* ASLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0516FA411A1563D200B4EBED /* ASMultiplexImageNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */; }; + 051943131A1575630030A7D0 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; }; + 051943151A1575670030A7D0 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 052EE06B1A15A0D8002C6279 /* TestResources in Resources */ = {isa = PBXBuildFile; fileRef = 052EE06A1A15A0D8002C6279 /* TestResources */; }; 055F1A3419ABD3E3004DAFF1 /* ASTableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 055F1A3519ABD3E3004DAFF1 /* ASTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 055F1A3319ABD3E3004DAFF1 /* ASTableView.m */; }; 055F1A3819ABD413004DAFF1 /* ASRangeController.h in Headers */ = {isa = PBXBuildFile; fileRef = 055F1A3619ABD413004DAFF1 /* ASRangeController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -111,7 +121,6 @@ 058D0A7B195D05F900B7D73C /* ASDisplayNodeInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; 058D0A7C195D05F900B7D73C /* ASImageNode+CGExtras.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */; settings = {ATTRIBUTES = (Private, ); }; }; 058D0A7D195D05F900B7D73C /* ASImageNode+CGExtras.m in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */; settings = {ATTRIBUTES = (Private, ); }; }; - 058D0A7E195D05F900B7D73C /* ASImageProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A0F195D050800B7D73C /* ASImageProtocols.h */; settings = {ATTRIBUTES = (Private, ); }; }; 058D0A7F195D05F900B7D73C /* ASSentinel.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A10195D050800B7D73C /* ASSentinel.h */; settings = {ATTRIBUTES = (Private, ); }; }; 058D0A80195D05F900B7D73C /* ASSentinel.m in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A11195D050800B7D73C /* ASSentinel.m */; settings = {ATTRIBUTES = (Private, ); }; }; 058D0A81195D05F900B7D73C /* ASThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A12195D050800B7D73C /* ASThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -120,6 +129,7 @@ 058D0A84195D060300B7D73C /* ASDisplayNodeExtraIvars.h in Headers */ = {isa = PBXBuildFile; fileRef = 058D0A45195D058D00B7D73C /* ASDisplayNodeExtraIvars.h */; settings = {ATTRIBUTES = (Public, ); }; }; 05A6D05A19D0EB64002DD95E /* ASDealloc2MainObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; 05A6D05B19D0EB64002DD95E /* ASDealloc2MainObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 05F20AA41A15733C00DCA68A /* ASImageProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 6BDC61F61979037800E50D21 /* AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -148,6 +158,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0516FA3A1A15563400B4EBED /* ASAvailability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAvailability.h; sourceTree = ""; }; + 0516FA3B1A15563400B4EBED /* ASLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLog.h; sourceTree = ""; }; + 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMultiplexImageNode.h; sourceTree = ""; }; + 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMultiplexImageNode.mm; sourceTree = ""; }; + 051943121A1575630030A7D0 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + 051943141A1575670030A7D0 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMultiplexImageNodeTests.m; sourceTree = ""; }; + 052EE06A1A15A0D8002C6279 /* TestResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TestResources; sourceTree = ""; }; 053011A719B9882B00A9F2D0 /* ASRangeControllerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRangeControllerInternal.h; sourceTree = ""; }; 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableView.h; sourceTree = ""; }; 055F1A3319ABD3E3004DAFF1 /* ASTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableView.m; sourceTree = ""; }; @@ -219,7 +237,6 @@ 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeInternal.h; sourceTree = ""; }; 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+CGExtras.h"; sourceTree = ""; }; 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASImageNode+CGExtras.m"; sourceTree = ""; }; - 058D0A0F195D050800B7D73C /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; 058D0A10195D050800B7D73C /* ASSentinel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSentinel.h; sourceTree = ""; }; 058D0A11195D050800B7D73C /* ASSentinel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASSentinel.m; sourceTree = ""; }; 058D0A12195D050800B7D73C /* ASThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASThread.h; sourceTree = ""; }; @@ -239,6 +256,7 @@ 058D0A45195D058D00B7D73C /* ASDisplayNodeExtraIvars.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeExtraIvars.h; sourceTree = ""; }; 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASDealloc2MainObject.h; path = ../Details/ASDealloc2MainObject.h; sourceTree = ""; }; 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASDealloc2MainObject.m; path = ../Details/ASDealloc2MainObject.m; sourceTree = ""; }; + 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewTests.m; sourceTree = ""; }; 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncDisplayKit.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 = ""; }; @@ -251,6 +269,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 051943151A1575670030A7D0 /* Photos.framework in Frameworks */, + 051943131A1575630030A7D0 /* AssetsLibrary.framework in Frameworks */, 058D09B0195D04C000B7D73C /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -259,6 +279,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0515EA221A1576A100BA8B9A /* AssetsLibrary.framework in Frameworks */, + 0515EA211A15769900BA8B9A /* Photos.framework in Frameworks */, 058D09BE195D04C000B7D73C /* XCTest.framework in Frameworks */, 058D09C1195D04C000B7D73C /* UIKit.framework in Frameworks */, 058D09C4195D04C000B7D73C /* libAsyncDisplayKit.a in Frameworks */, @@ -293,6 +315,8 @@ 058D09AE195D04C000B7D73C /* Frameworks */ = { isa = PBXGroup; children = ( + 051943141A1575670030A7D0 /* Photos.framework */, + 051943121A1575630030A7D0 /* AssetsLibrary.framework */, 058D09AF195D04C000B7D73C /* Foundation.framework */, 058D09BD195D04C000B7D73C /* XCTest.framework */, 058D09C0195D04C000B7D73C /* UIKit.framework */, @@ -317,6 +341,8 @@ 058D09E0195D050800B7D73C /* ASTextNode.mm */, 058D09DD195D050800B7D73C /* ASImageNode.h */, 058D09DE195D050800B7D73C /* ASImageNode.mm */, + 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */, + 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */, 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */, 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */, 055F1A3319ABD3E3004DAFF1 /* ASTableView.m */, @@ -347,12 +373,14 @@ 058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */, 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, + 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */, 058D0A33195D057000B7D73C /* ASTextNodeCoreTextAdditionsTests.m */, 058D0A34195D057000B7D73C /* ASTextNodeRendererTests.m */, 058D0A35195D057000B7D73C /* ASTextNodeShadowerTests.m */, 058D0A36195D057000B7D73C /* ASTextNodeTests.m */, 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */, + 052EE06A1A15A0D8002C6279 /* TestResources */, 058D09C6195D04C000B7D73C /* Supporting Files */, ); path = AsyncDisplayKitTests; @@ -370,6 +398,7 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -433,7 +462,6 @@ 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */, - 058D0A0F195D050800B7D73C /* ASImageProtocols.h */, 053011A719B9882B00A9F2D0 /* ASRangeControllerInternal.h */, 058D0A10195D050800B7D73C /* ASSentinel.h */, 058D0A11195D050800B7D73C /* ASSentinel.m */, @@ -445,8 +473,10 @@ isa = PBXGroup; children = ( 058D0A43195D058D00B7D73C /* ASAssert.h */, + 0516FA3A1A15563400B4EBED /* ASAvailability.h */, 058D0A44195D058D00B7D73C /* ASBaseDefines.h */, 058D0A45195D058D00B7D73C /* ASDisplayNodeExtraIvars.h */, + 0516FA3B1A15563400B4EBED /* ASLog.h */, ); path = Base; sourceTree = SOURCE_ROOT; @@ -468,6 +498,7 @@ buildActionMask = 2147483647; files = ( 05A6D05A19D0EB64002DD95E /* ASDealloc2MainObject.h in Headers */, + 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */, 058D0A47195D05CB00B7D73C /* ASControlNode.h in Headers */, 058D0A48195D05CB00B7D73C /* ASControlNode.m in Headers */, 058D0A49195D05CB00B7D73C /* ASControlNode+Subclasses.h in Headers */, @@ -516,8 +547,11 @@ 058D0A6F195D05EC00B7D73C /* UIView+ASConvenience.h in Headers */, 058D0A70195D05EC00B7D73C /* UIView+ASConvenience.m in Headers */, 058D0A82195D060300B7D73C /* ASAssert.h in Headers */, + 0516FA3C1A15563400B4EBED /* ASAvailability.h in Headers */, + 0516FA3D1A15563400B4EBED /* ASLog.h in Headers */, 058D0A83195D060300B7D73C /* ASBaseDefines.h in Headers */, 058D0A84195D060300B7D73C /* ASDisplayNodeExtraIvars.h in Headers */, + 05F20AA41A15733C00DCA68A /* ASImageProtocols.h in Headers */, 058D0A71195D05F800B7D73C /* _AS-objc-internal.h in Headers */, 058D0A72195D05F800B7D73C /* _ASCoreAnimationExtras.h in Headers */, 058D0A73195D05F800B7D73C /* _ASCoreAnimationExtras.mm in Headers */, @@ -531,7 +565,6 @@ 058D0A7B195D05F900B7D73C /* ASDisplayNodeInternal.h in Headers */, 058D0A7C195D05F900B7D73C /* ASImageNode+CGExtras.h in Headers */, 058D0A7D195D05F900B7D73C /* ASImageNode+CGExtras.m in Headers */, - 058D0A7E195D05F900B7D73C /* ASImageProtocols.h in Headers */, 058D0A7F195D05F900B7D73C /* ASSentinel.h in Headers */, 058D0A80195D05F900B7D73C /* ASSentinel.m in Headers */, 058D0A81195D05F900B7D73C /* ASThread.h in Headers */, @@ -611,6 +644,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 052EE06B1A15A0D8002C6279 /* TestResources in Resources */, 058D09CA195D04C000B7D73C /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -681,6 +715,7 @@ 05A6D05B19D0EB64002DD95E /* ASDealloc2MainObject.m in Sources */, 058D0A17195D050800B7D73C /* ASTextNode.mm in Sources */, 058D0A27195D050800B7D73C /* _ASPendingState.m in Sources */, + 0516FA411A1563D200B4EBED /* ASMultiplexImageNode.mm in Sources */, 058D0A16195D050800B7D73C /* ASImageNode.mm in Sources */, 058D0A29195D050800B7D73C /* ASDisplayNode+DebugTiming.mm in Sources */, 058D0A22195D050800B7D73C /* _ASAsyncTransaction.m in Sources */, @@ -698,6 +733,7 @@ 058D0A3F195D057000B7D73C /* ASTextNodeShadowerTests.m in Sources */, 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */, + 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */, 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */, 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */, 058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */, diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h new file mode 100644 index 0000000000..ff2918a984 --- /dev/null +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -0,0 +1,197 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@protocol ASMultiplexImageNodeDelegate; +@protocol ASMultiplexImageNodeDataSource; + +extern NSString *const ASMultiplexImageNodeErrorDomain; + +/** + * ASMultiplexImageNode error codes. + */ +typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { + /** + * Indicates that the data source didn't provide a source for an image identifier. + */ + ASMultiplexImageNodeErrorCodeNoSourceForImage = 0, + + /** + * Indicates that the best image identifier changed before a download for a worse identifier began. + */ + ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged, +}; + + +/** + * @abstract ASMultiplexImageNode is an image node that can load and display multiple versions of an image. For + * example, it can display a low-resolution version of an image while the high-resolution version is loading. + * + * @discussion ASMultiplexImageNode begins loading images when its property is set. For each image + * identifier, the data source can either return a UIImage directly, or a URL the image node should load. + */ +@interface ASMultiplexImageNode : ASImageNode + +/** + * @abstract The designated initializer. + * @param cache The object that implements a cache of images for the image node. + * @param downloader The object that implements image downloading for the image node. + * @discussion If `cache` is nil, the receiver will not attempt to retrieve images from a cache before downloading them. + * @returns An initialized ASMultiplexImageNode. + */ +- (instancetype)initWithCache:(id)cache downloader:(id)downloader; +- (instancetype)init NS_UNAVAILABLE; + +/** + * @abstract The delegate, which must conform to the protocol. + */ +@property (nonatomic, readwrite, weak) id delegate; + +/** + * @abstract The data source, which must conform to the protocol. + * @discussion This value is required for ASMultiplexImageNode to load images. + */ +@property (nonatomic, readwrite, weak) id dataSource; + +/** + * @abstract Whether the receiver should download more than just its highest-quality image. Defaults to NO. + * + * @discussion ASMultiplexImageNode immediately loads and displays the first image specified in (its + * highest-quality image). If that image is not immediately available or cached, the node can download and display + * lesser-quality images. Set `downloadsIntermediateImages` to YES to enable this behaviour. + */ +@property (nonatomic, readwrite, assign) BOOL downloadsIntermediateImages; + +/** + * @abstract An array of identifiers representing various versions of an image for ASMultiplexImageNode to display. + * + * @discussion An identifier can be any object that conforms to NSObject and NSCopying. The array should be in + * decreasing order of image quality -- that is, the first identifier in the array represents the best version. + * + * @see for more information on the image loading process. + */ +@property (nonatomic, readwrite, copy) NSArray *imageIdentifiers; + +/** + * @abstract Notify the receiver that its data source has new UIImages or NSURLs available for . + * + * @discussion If a higher-quality image than is currently displayed is now available, it will be loaded. + */ +- (void)reloadImageIdentifierSources; + +/** + * @abstract The identifier for the last image that the receiver loaded, or nil. + * + * @discussion This value may differ from if the image hasn't yet been displayed. + */ +@property (nonatomic, readonly) id loadedImageIdentifier; + +/** + * @abstract The identifier for the image that the receiver is currently displaying, or nil. + */ +@property (nonatomic, readonly) id displayedImageIdentifier; + +@end + + +#pragma mark - +@protocol ASMultiplexImageNodeDelegate + +@optional +/** + * @abstract Notification that the image node began downloading an image. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that is downloading. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode didStartDownloadOfImageWithIdentifier:(id)imageIdentifier; + +/** + * @abstract Notification that the image node's download progressed. + * @param imageNode The sender. + * @param downloadProgress The progress of the download. Value is between 0.0 and 1.0. + * @param imageIdentifier The identifier for the image that is downloading. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode + didUpdateDownloadProgress:(CGFloat)downloadProgress + forImageWithIdentifier:(id)imageIdentifier; + +/** + * @abstract Notification that the image node's download has finished. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that finished downloading. + * @param error The error that occurred while downloading, if one occurred; nil otherwise. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode +didFinishDownloadingImageWithIdentifier:(id)imageIdentifier + error:(NSError *)error; + +/** + * @abstract Notification that the image node's image was updated. + * @param imageNode The sender. + * @param image The new image, ready for display. + * @param imageIdentifier The identifier for `image`. + * @param previousImage The old, previously-loaded image. + * @param previousImageIdentifier The identifier for `previousImage`. + * @note This method does not indicate that `image` has been displayed. + * @see <[ASMultiplexImageNodeDelegate multiplexImageNode:didDisplayUpdatedImage:withIdentifier:]>. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode + didUpdateImage:(UIImage *)image + withIdentifier:(id)imageIdentifier + fromImage:(UIImage *)previousImage + withIdentifier:(id)previousImageIdentifier; + +/** + * @abstract Notification that the image node displayed a new image. + * @param imageNode The sender. + * @param image The new image, now being displayed. + * @param imageIdentifier The identifier for `image`. + * @discussion This method is only called when `image` changes, and not on subsequent redisplays of the same image. + */ +- (void)multiplexImageNode:(ASMultiplexImageNode *)imageNode + didDisplayUpdatedImage:(UIImage *)image + withIdentifier:(id)imageIdentifier; + +/** + * @abstract Notification that the image node finished displaying an image. + * @param imageNode The sender. + * @discussion This method is called every time an image is displayed, whether or not it has changed. + */ +- (void)multiplexImageNodeDidFinishDisplay:(ASMultiplexImageNode *)imageNode; + +@end + + +#pragma mark - +@protocol ASMultiplexImageNodeDataSource + +@optional +/** + * @abstract An image for the specified identifier. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that should be returned. + * @discussion If the image is already available to the data source, this method should be used in lieu of providing the + * URL to the image via -multiplexImageNode:URLForImageIdentifier:. + * @returns A UIImage corresponding to `imageIdentifier`, or nil if none is available. + */ +- (UIImage *)multiplexImageNode:(ASMultiplexImageNode *)imageNode imageForImageIdentifier:(id)imageIdentifier; + +/** + * @abstract An image URL for the specified identifier. + * @param imageNode The sender. + * @param imageIdentifier The identifier for the image that will be downloaded. + * @discussion Supported URLs include assets-library, Photo framework URLs (ph://), HTTP, HTTPS, and FTP URLs. If the + * image is already available to the data source, it should be provided via <[ASMultiplexImageNodeDataSource + * multiplexImageNode:imageForImageIdentifier:]> instead. + * @returns An NSURL for the image identified by `imageIdentifier`, or nil if none is available. + */ +- (NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(id)imageIdentifier; + +@end diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm new file mode 100644 index 0000000000..7c3ae9684f --- /dev/null +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -0,0 +1,652 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASMultiplexImageNode.h" + +#import + +#import + +#import + +#import "ASAvailability.h" +#import "ASBaseDefines.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASLog.h" + +#if !AS_IOS8_SDK_OR_LATER +#error ASMultiplexImageNode can be used on iOS 7, but must be linked against the iOS 8 SDK. +#endif + +NSString *const ASMultiplexImageNodeErrorDomain = @"ASMultiplexImageNodeErrorDomain"; + +static NSString *const kAssetsLibraryURLScheme = @"assets-library"; +static NSString *const kPHAssetURLScheme = @"ph"; +static NSString *const kPHAssetURLPrefix = @"ph://"; + +/** + @abstract Signature for the block to be performed after an image has loaded. + @param image The image that was loaded, or nil if no image was loaded. + @param imageIdentifier The identifier of the image that was loaded, or nil if no image was loaded. + @param error An error describing why an image couldn't be loaded, if it failed to load; nil otherwise. + */ +typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdentifier, NSError *error); + +@interface ASMultiplexImageNode () +{ +@private + // Core. + id _cache; + id _downloader; + + __weak id _delegate; + struct { + unsigned int downloadStart:1; + unsigned int downloadProgress:1; + unsigned int downloadFinish:1; + unsigned int updatedImageDisplayFinish:1; + unsigned int updatedImage:1; + unsigned int displayFinish:1; + } _delegateFlags; + + __weak id _dataSource; + struct { + unsigned int image:1; + unsigned int URL:1; + } _dataSourceFlags; + + // Image flags. + BOOL _downloadsIntermediateImages; // Defaults to NO. + OSSpinLock _imageIdentifiersLock; + NSArray *_imageIdentifiers; + id _loadedImageIdentifier; + id _loadingImageIdentifier; + id _displayedImageIdentifier; + + // Networking. + id _downloadIdentifier; + BOOL _canceledImageDownload; +} + +//! @abstract Read-write redeclaration of property declared in ASMultiplexImageNode.h. +@property (nonatomic, readwrite, copy) id loadedImageIdentifier; + +//! @abstract The image identifier that's being loaded by _loadNextImageWithCompletion:. +@property (nonatomic, readwrite, copy) id loadingImageIdentifier; + +/** + @abstract Indicates whether a change from one array of image identifiers to another should prompt the receiver to update its loaded image. + @param loadedIdentifier The image identifier that's currently loaded, or nil if no identifier is loaded yet. + @param loadingImageIdentifier The image identifier that's currently loading, or nil if no identifier is loading. + @param oldIdentifiers An array of image identifiers that were previously managed, or nil if no identifiers were managed before. + @param newIdentifiers The array of new image identifiers. + @result YES if the receiver should update its loaded image as a consequence of the newly assigned identifiers; NO otherwise. + */ +static inline BOOL _shouldUpdateAfterChangedImageIdentifiers(id loadedIdentifier, id loadingImageIdentifier, NSArray *oldIdentifiers, NSArray *newIdentifiers); + +/** + @abstract Returns the next image identifier that should be downloaded. + @discussion This method obeys and reflects the value of `downloadsIntermediateImages`. + @result The next image identifier, from `_imageIdentifiers`, that should be downloaded, or nil if no image should be downloaded next. + */ +- (id)_nextImageIdentifierToDownload; + +/** + @abstract Returns the best image that is immediately available from our datasource without downloading or hitting the cache. + @param imageIdentifierOut Upon return, the image identifier for the returned image; nil otherwise. + @discussion This method exclusively uses the data source's -multiplexImageNode:imageForIdentifier: method to return images. It does not fetch from the cache or kick off downloading. + @result The best UIImage available immediately; nil if no image is immediately available. + */ +- (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierOut; + +/** + @abstract Loads and displays the next image in the receiver's loading sequence. + @discussion This method obeys `downloadsIntermediateImages`. This method has no effect if nothing further should be loaded, as indicated by `_nextImageIdentifierToDownload`. This method will load the next image from the data-source, if possible; otherwise, the session's image cache will be queried for the desired image, and as a last resort, the image will be downloaded. + */ +- (void)_loadNextImage; + +/** + @abstract Fetches the image corresponding to the given imageIdentifier from the given URL from the session's image cache. + @param imageIdentifier The identifier for the image to be fetched. May not be nil. + @param imageURL The URL of the image to fetch. May not be nil. + @param completionBlock The block to be performed when the image has been fetched from the cache, if possible. May not be nil. + @param image The image fetched from the cache, if any. + @discussion This method queries both the session's in-memory and on-disk caches (with preference for the in-memory cache). + */ +- (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock; + +/** + @abstract Loads the image corresponding to the given assetURL from the device's Assets Library. + @param imageIdentifier The identifier for the image to be loaded. May not be nil. + @param assetURL The assets-library URL (e.g., "assets-library://identifier") of the image to load, from ALAsset. May not be nil. + @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil. + @param image The image that was loaded. May be nil if no image could be downloaded. + @param error An error describing why the load failed, if it failed; nil otherwise. + */ +- (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock; + +/** + @abstract Loads the image corresponding to the given assetURL from the Photos framework. + @param imageIdentifier The identifier for the image to be loaded. May not be nil. + @param assetURL The photos framework URL (e.g., "ph://identifier") of the image to load, from PHAsset. May not be nil. + @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil. + @param image The image that was loaded. May be nil if no image could be downloaded. + @param error An error describing why the load failed, if it failed; nil otherwise. + */ +- (void)_loadPHAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock; + +/** + @abstract Downloads the image corresponding to the given imageIdentifier from the given URL. + @param imageIdentifier The identifier for the image to be downloaded. May not be nil. + @param imageURL The URL of the image to downloaded. May not be nil. + @param completionBlock The block to be performed when the image has been downloaded, if possible. May not be nil. + @param image The image that was downloaded. May be nil if no image could be downloaded. + @param error An error describing why the download failed, if it failed; nil otherwise. + */ +- (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image, NSError *error))completionBlock; + +@end + +@implementation ASMultiplexImageNode + +#pragma mark - Getting Started / Tearing Down +- (instancetype)initWithCache:(id)cache downloader:(id)downloader +{ + if (!(self = [super init])) + return nil; + + _cache = cache; + _downloader = downloader; + + return self; +} + +- (instancetype)init +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +#pragma mark - ASDisplayNode Overrides +- (void)didExitHierarchy +{ + [super didExitHierarchy]; // This actually clears the contents, so we need to do this first for our displayedImageIdentifier to be meaningful. + [self _setDisplayedImageIdentifier:nil withImage:nil]; + + if (_downloadIdentifier) { + [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; + _downloadIdentifier = nil; + _canceledImageDownload = YES; + } +} + +- (void)willEnterHierarchy +{ + [super willEnterHierarchy]; + + if(_canceledImageDownload) { + [self _updatedImageIdentifiers]; + _canceledImageDownload = NO; + } +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + // We may now be displaying the loaded identifier, if they're different. + UIImage *displayedImage = self.image; + if (displayedImage) { + if (![_displayedImageIdentifier isEqual:_loadedImageIdentifier]) + [self _setDisplayedImageIdentifier:_loadedImageIdentifier withImage:displayedImage]; + + // Delegateify + if (_delegateFlags.displayFinish) { + if ([NSThread isMainThread]) + [_delegate multiplexImageNodeDidFinishDisplay:self]; + else { + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + [strongSelf.delegate multiplexImageNodeDidFinishDisplay:strongSelf]; + }); + } + } + } +} + +#pragma mark - Core + +- (void)setDelegate:(id )delegate +{ + if (_delegate == delegate) + return; + + _delegate = delegate; + _delegateFlags.downloadStart = [_delegate respondsToSelector:@selector(multiplexImageNode:didStartDownloadOfImageWithIdentifier:)]; + _delegateFlags.downloadProgress = [_delegate respondsToSelector:@selector(multiplexImageNode:didUpdateDownloadProgress:forImageWithIdentifier:)]; + _delegateFlags.downloadFinish = [_delegate respondsToSelector:@selector(multiplexImageNode:didFinishDownloadingImageWithIdentifier:error:)]; + _delegateFlags.updatedImageDisplayFinish = [_delegate respondsToSelector:@selector(multiplexImageNode:didDisplayUpdatedImage:withIdentifier:)]; + _delegateFlags.updatedImage = [_delegate respondsToSelector:@selector(multiplexImageNode:didUpdateImage:withIdentifier:fromImage:withIdentifier:)]; + _delegateFlags.displayFinish = [_delegate respondsToSelector:@selector(multiplexImageNodeDidFinishDisplay:)]; +} + + +- (void)setDataSource:(id )dataSource +{ + if (_dataSource == dataSource) + return; + + _dataSource = dataSource; + _dataSourceFlags.image = [_dataSource respondsToSelector:@selector(multiplexImageNode:imageForImageIdentifier:)]; + _dataSourceFlags.URL = [_dataSource respondsToSelector:@selector(multiplexImageNode:URLForImageIdentifier:)]; +} + +#pragma mark - + +#pragma mark - + +- (NSArray *)imageIdentifiers +{ + OSSpinLockLock(&_imageIdentifiersLock); + NSArray *imageIdentifiers = [_imageIdentifiers copy]; + OSSpinLockUnlock(&_imageIdentifiersLock); + return imageIdentifiers; +} + +- (void)setImageIdentifiers:(NSArray *)imageIdentifiers +{ + OSSpinLockLock(&_imageIdentifiersLock); + + if (_imageIdentifiers == imageIdentifiers) { + OSSpinLockUnlock(&_imageIdentifiersLock); + return; + } + + NSArray *oldImageIdentifiers = [[NSArray alloc] initWithArray:_imageIdentifiers]; + + _imageIdentifiers = [imageIdentifiers copy]; + + // Kick off image loading and display, if the identifiers have substantively changed. + BOOL shouldUpdateAfterChangedImageIdentifiers = _shouldUpdateAfterChangedImageIdentifiers(self.loadedImageIdentifier, self.loadingImageIdentifier, oldImageIdentifiers, _imageIdentifiers); + + OSSpinLockUnlock(&_imageIdentifiersLock); + + if (shouldUpdateAfterChangedImageIdentifiers) { + [self _updatedImageIdentifiers]; + } +} + +- (void)reloadImageIdentifierSources +{ + [self _updatedImageIdentifiers]; +} + +#pragma mark - + + +#pragma mark - Core Internal +- (void)_setDisplayedImageIdentifier:(id)displayedImageIdentifier withImage:(UIImage *)image +{ + if (_displayedImageIdentifier == displayedImageIdentifier) + return; + + _displayedImageIdentifier = [displayedImageIdentifier copy]; + + // Delegateify. + // Note that we're using the params here instead of self.image and _displayedImageIdentifier because those can change before the async block below executes. + if (_delegateFlags.updatedImageDisplayFinish) { + if ([NSThread isMainThread]) + [_delegate multiplexImageNode:self didDisplayUpdatedImage:image withIdentifier:displayedImageIdentifier]; + else { + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + [strongSelf.delegate multiplexImageNode:strongSelf didDisplayUpdatedImage:image withIdentifier:displayedImageIdentifier]; + }); + } + } +} + +- (void)_setDownloadIdentifier:(id)downloadIdentifier +{ + if (_downloadIdentifier == downloadIdentifier) + return; + + [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; + _downloadIdentifier = downloadIdentifier; +} + +#pragma mark - Image Loading Machinery +static inline BOOL _shouldUpdateAfterChangedImageIdentifiers(id loadedIdentifier, id loadingImageIdentifier, NSArray *oldIdentifiers, NSArray *newIdentifiers) +{ + // If we're loading an identifier, we need to update if it isn't permitted to load anymore. + if (loadingImageIdentifier) + return ![newIdentifiers containsObject:loadingImageIdentifier]; + + // We're not loading, so we need to update unless we've already loaded the best identifier. + return ![[newIdentifiers firstObject] isEqual:loadedIdentifier]; +} + +- (void)_updatedImageIdentifiers +{ + // Kill any in-flight downloads. + [self _setDownloadIdentifier:nil]; + + // Grab the best possible image we can load right now. + id bestImmediatelyAvailableImageIdentifier = nil; + UIImage *bestImmediatelyAvailableImage = [self _bestImmediatelyAvailableImageFromDataSource:&bestImmediatelyAvailableImageIdentifier]; + ASMultiplexImageNodeLogDebug(@"[%p] Best immediately available image identifier is %@", self, bestImmediatelyAvailableImageIdentifier); + + // Load it. This kicks off cache fetching/downloading, as appropriate. + [self _finishedLoadingImage:bestImmediatelyAvailableImage forIdentifier:bestImmediatelyAvailableImageIdentifier error:nil]; +} + +- (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierOut +{ + OSSpinLockLock(&_imageIdentifiersLock); + + // If we don't have any identifiers to load or don't implement the image DS method, bail. + if ([_imageIdentifiers count] == 0 || !_dataSourceFlags.image) { + OSSpinLockUnlock(&_imageIdentifiersLock); + return nil; + } + + // Grab the best available image from the data source. + for (id imageIdentifier in _imageIdentifiers) { + UIImage *image = [_dataSource multiplexImageNode:self imageForImageIdentifier:imageIdentifier]; + if (image) { + if (imageIdentifierOut) { + *imageIdentifierOut = [imageIdentifier copy]; + } + + OSSpinLockUnlock(&_imageIdentifiersLock); + return image; + } + } + + OSSpinLockUnlock(&_imageIdentifiersLock); + return nil; +} + + +#pragma mark - +- (id)_nextImageIdentifierToDownload +{ + OSSpinLockLock(&_imageIdentifiersLock); + + // If we've already loaded the best identifier, we've got nothing else to do. + id bestImageIdentifier = ([_imageIdentifiers count] > 0) ? _imageIdentifiers[0] : nil; + if (!bestImageIdentifier || [_loadedImageIdentifier isEqual:bestImageIdentifier]) { + OSSpinLockUnlock(&_imageIdentifiersLock); + return nil; + } + + id nextImageIdentifierToDownload = nil; + + // If we're not supposed to download intermediate images, load the best identifier we've got. + if (!_downloadsIntermediateImages) { + nextImageIdentifierToDownload = bestImageIdentifier; + } + // Otherwise, load progressively. + else { + NSUInteger loadedIndex = [_imageIdentifiers indexOfObject:_loadedImageIdentifier]; + + // If nothing has loaded yet, load the worst identifier. + if (loadedIndex == NSNotFound) { + nextImageIdentifierToDownload = [_imageIdentifiers lastObject]; + } + // Otherwise, load the next best identifier (if there is one) + else if (loadedIndex > 0) { + nextImageIdentifierToDownload = _imageIdentifiers[loadedIndex - 1]; + } + } + + OSSpinLockUnlock(&_imageIdentifiersLock); + + return nextImageIdentifierToDownload; +} + +- (void)_loadNextImage +{ + // Determine the next identifier to load (if any). + id nextImageIdentifier = [self _nextImageIdentifierToDownload]; + if (!nextImageIdentifier) { + [self _finishedLoadingImage:nil forIdentifier:nil error:nil]; + return; + } + + self.loadingImageIdentifier = nextImageIdentifier; + + __weak __typeof__(self) weakSelf = self; + ASMultiplexImageLoadCompletionBlock finishedLoadingBlock = ^(UIImage *image, id imageIdentifier, NSError *error) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + // Only nil out the loading identifier if the loading identifier hasn't changed. + if ([strongSelf.loadingImageIdentifier isEqual:nextImageIdentifier]) { + strongSelf.loadingImageIdentifier = nil; + } + [strongSelf _finishedLoadingImage:image forIdentifier:imageIdentifier error:error]; + }; + + ASMultiplexImageNodeLogDebug(@"[%p] Loading next image, ident: %@", self, nextImageIdentifier); + + // Ask our data-source if it's got this image. + if (_dataSourceFlags.image) { + UIImage *image = [_dataSource multiplexImageNode:self imageForImageIdentifier:nextImageIdentifier]; + if (image) { + ASMultiplexImageNodeLogDebug(@"[%p] Acquired next image (%@) from data-source", self, nextImageIdentifier); + finishedLoadingBlock(image, nextImageIdentifier, nil); + return; + } + } + + NSURL *nextImageURL = (_dataSourceFlags.URL) ? [_dataSource multiplexImageNode:self URLForImageIdentifier:nextImageIdentifier] : nil; + // If we fail to get a URL for the image, we have no source and can't proceed. + if (!nextImageURL) { + ASMultiplexImageNodeLogError(@"[%p] Could not acquire URL for next image (%@). Bailing.", self, nextImageIdentifier); + finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeNoSourceForImage userInfo:nil]); + return; + } + + // If it's an assets-library URL, we need to fetch it from the assets library. + if ([[nextImageURL scheme] isEqualToString:kAssetsLibraryURLScheme]) { + // Load the asset. + [self _loadALAssetWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) { + ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from asset library", weakSelf, nextImageIdentifier); + finishedLoadingBlock(downloadedImage, nextImageIdentifier, error); + }]; + } + // Likewise, if it's a iOS 8 Photo asset, we need to fetch it accordingly. + else if (AS_AT_LEAST_IOS8 && [[nextImageURL scheme] isEqualToString:kPHAssetURLScheme]) { + [self _loadPHAssetWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *image, NSError *error) { + ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from Photos Framework", weakSelf, nextImageIdentifier); + finishedLoadingBlock(image, nextImageIdentifier, error); + }]; + } + else // Otherwise, it's a web URL that we can download. + { + // First, check the cache. + [self _fetchImageWithIdentifierFromCache:nextImageIdentifier URL:nextImageURL completion:^(UIImage *imageFromCache) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + // If we had a cache-hit, we're done. + if (imageFromCache) { + ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from cache", strongSelf, nextImageIdentifier); + finishedLoadingBlock(imageFromCache, nextImageIdentifier, nil); + return; + } + + // If the next image to load has changed, bail. + if (![[strongSelf _nextImageIdentifierToDownload] isEqual:nextImageIdentifier]) { + finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged userInfo:nil]); + return; + } + + // Otherwise, we've got to download it. + [strongSelf _downloadImageWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) { + ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from download", strongSelf, nextImageIdentifier); + finishedLoadingBlock(downloadedImage, nextImageIdentifier, error); + }]; + }]; + } +} + +- (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock +{ + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(assetURL, @"assetURL is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init]; + + [assetLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) { + ALAssetRepresentation *representation = [asset defaultRepresentation]; + CGImageRef coreGraphicsImage = [representation fullScreenImage]; + + UIImage *downloadedImage = (coreGraphicsImage ? [UIImage imageWithCGImage:coreGraphicsImage] : nil); + completionBlock(downloadedImage, nil); + } failureBlock:^(NSError *error) { + completionBlock(nil, error); + }]; +} + +- (void)_loadPHAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock +{ + ASDisplayNodeAssert(AS_AT_LEAST_IOS8, @"PhotosKit is unavailable on iOS 7."); + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(assetURL, @"assetURL is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + // Get the PHAsset itself. + ASDisplayNodeAssertTrue([[assetURL absoluteString] hasPrefix:kPHAssetURLPrefix]); + NSString *assetIdentifier = [[assetURL absoluteString] substringFromIndex:[kPHAssetURLPrefix length]]; + PHFetchResult *assetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetIdentifier] options:nil]; + if ([assetFetchResult count] == 0) { + // Error. + completionBlock(nil, nil); + return; + } + + // Get the best image we can. + PHAsset *imageAsset = [assetFetchResult firstObject]; + + PHImageRequestOptions *requestOptions = [[PHImageRequestOptions alloc] init]; + requestOptions.version = PHImageRequestOptionsVersionCurrent; + requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + requestOptions.resizeMode = PHImageRequestOptionsResizeModeNone; + + [[PHImageManager defaultManager] requestImageForAsset:imageAsset + targetSize:CGSizeMake(2048.0, 2048.0) // Ideally we would use PHImageManagerMaximumSize and kill the options, but we get back nil when requesting images of video assets. rdar://18447788 + contentMode:PHImageContentModeDefault + options:requestOptions + resultHandler:^(UIImage *image, NSDictionary *info) { + completionBlock(image, info[PHImageErrorKey]); + }]; +} + +- (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock +{ + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(imageURL, @"imageURL is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + if (_cache) { + [_cache fetchCachedImageWithURL:imageURL callbackQueue:dispatch_get_main_queue() completion:^(CGImageRef coreGraphicsImageFromCache) { + UIImage *imageFromCache = (coreGraphicsImageFromCache ? [UIImage imageWithCGImage:coreGraphicsImageFromCache] : nil); + completionBlock(imageFromCache); + }]; + } + // If we don't have a cache, just fail immediately. + else { + completionBlock(nil); + } +} + +- (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image, NSError *error))completionBlock +{ + ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); + ASDisplayNodeAssertNotNil(imageURL, @"imageURL is required"); + ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); + + // Delegate (start) + if (_delegateFlags.downloadStart) + [_delegate multiplexImageNode:self didStartDownloadOfImageWithIdentifier:imageIdentifier]; + + __weak __typeof__(self) weakSelf = self; + void (^downloadProgressBlock)(CGFloat) = nil; + if (_delegateFlags.downloadProgress) { + downloadProgressBlock = ^(CGFloat progress) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + [strongSelf.delegate multiplexImageNode:strongSelf didUpdateDownloadProgress:progress forImageWithIdentifier:imageIdentifier]; + }; + } + + // Download! + [self _setDownloadIdentifier:[_downloader downloadImageWithURL:imageURL + callbackQueue:dispatch_get_main_queue() + downloadProgressBlock:downloadProgressBlock + completion:^(CGImageRef coreGraphicsImage, NSError *error) { + // We dereference iVars directly, so we can't have weakSelf going nil on us. + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + UIImage *downloadedImage = (coreGraphicsImage ? [UIImage imageWithCGImage:coreGraphicsImage] : nil); + completionBlock(downloadedImage, error); + + // Delegateify. + if (strongSelf->_delegateFlags.downloadFinish) + [strongSelf->_delegate multiplexImageNode:weakSelf didFinishDownloadingImageWithIdentifier:imageIdentifier error:error]; + }]]; +} + +#pragma mark - +- (void)_finishedLoadingImage:(UIImage *)image forIdentifier:(id)imageIdentifier error:(NSError *)error +{ + // If we failed to load, we stop the loading process. + // Note that if we bailed before we began downloading because the best identifier changed, we don't bail, but rather just begin loading the best image identifier. + if (error && error.code != ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged) + return; + + OSSpinLockLock(&_imageIdentifiersLock); + NSUInteger imageIdentifierCount = [_imageIdentifiers count]; + OSSpinLockUnlock(&_imageIdentifiersLock); + + // Update our image if we got one, or if we're not supposed to display one at all. + // We explicitly perform this check because our datasource often doesn't give back immediately available images, even though we might have downloaded one already. + // Because we seed this call with bestImmediatelyAvailableImageFromDataSource, we must be careful not to trample an existing image. + if (image || imageIdentifierCount == 0) { + ASMultiplexImageNodeLogDebug(@"[%p] loaded -> displaying (%@, %@)", self, imageIdentifier, image); + id previousIdentifier = self.loadedImageIdentifier; + UIImage *previousImage = self.image; + + self.loadedImageIdentifier = imageIdentifier; + self.image = image; + + if (_delegateFlags.updatedImage) { + [_delegate multiplexImageNode:self didUpdateImage:image withIdentifier:imageIdentifier fromImage:previousImage withIdentifier:previousIdentifier]; + } + + } + + // Load our next image, if we have one to load. + if ([self _nextImageIdentifierToDownload]) + [self _loadNextImage]; +} + +@end diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index fbc44409fa..4e0cfe102f 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -13,5 +13,7 @@ #import #import +#import + #import #import diff --git a/AsyncDisplayKit/Private/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h similarity index 100% rename from AsyncDisplayKit/Private/ASImageProtocols.h rename to AsyncDisplayKit/Details/ASImageProtocols.h diff --git a/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m new file mode 100644 index 0000000000..9f11f92d6f --- /dev/null +++ b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m @@ -0,0 +1,350 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import + +#import + +@interface ASMultiplexImageNodeTests : XCTestCase +{ +@private + id _mockCache; + id _mockDownloader; +} + +@end + + +@implementation ASMultiplexImageNodeTests + +#pragma mark - +#pragma mark Helpers. + +- (NSURL *)_testImageURL +{ + return [[NSBundle bundleForClass:[self class]] URLForResource:@"logo-square" + withExtension:@"png" + subdirectory:@"TestResources"]; +} + +- (UIImage *)_testImage +{ + return [[[UIImage alloc] initWithContentsOfFile:[self _testImageURL].path] autorelease]; +} + +static BOOL ASInvokeConditionBlockWithBarriers(BOOL (^block)()) { + // In case the block does multiple comparisons, ensure it has a consistent view of memory by issuing read-write + // barriers on either side of the block. + OSMemoryBarrier(); + BOOL result = block(); + OSMemoryBarrier(); + return result; +} + +static BOOL ASRunRunLoopUntilBlockIsTrue(BOOL (^block)()) +{ + // Time out after 30 seconds. + CFTimeInterval timeoutDate = CACurrentMediaTime() + 30.0f; + BOOL passed = NO; + + while (true) { + passed = ASInvokeConditionBlockWithBarriers(block); + + if (passed) { + break; + } + + CFTimeInterval now = CACurrentMediaTime(); + if (now > timeoutDate) { + break; + } + + // Run 1000 times a second until the poll timeout or until timeoutDate, whichever is first. + CFTimeInterval runLoopTimeout = MIN(0.001, timeoutDate - now); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, runLoopTimeout, true); + } + + return passed; +} + + +#pragma mark - +#pragma mark Unit tests. + +// TODO: add tests for delegate display notifications + +- (void)setUp +{ + [super setUp]; + + _mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)]; + _mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)]; +} + +- (void)testDataSourceImageMethod +{ + ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; + + // Mock the data source. + // Note that we're not using a niceMock because we want to assert if the URL data-source method gets hit, as the image + // method should be hit first and exclusively if it successfully returns an image. + id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; + imageNode.dataSource = mockDataSource; + + NSNumber *imageIdentifier = @1; + + // Expect the image method to be hit, and have it return our test image. + UIImage *testImage = [self _testImage]; + [[[mockDataSource expect] andReturn:testImage] multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]; + + imageNode.imageIdentifiers = @[imageIdentifier]; + + [mockDataSource verify]; + + // Also expect it to be loaded immediately. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded"); + // And for the image to be equivalent to the image we provided. + XCTAssertEqualObjects(UIImagePNGRepresentation(imageNode.image), + UIImagePNGRepresentation(testImage), + @"Loaded image isn't the one we provided"); + + imageNode.delegate = nil; + imageNode.dataSource = nil; + [imageNode release]; +} + +- (void)testDataSourceURLMethod +{ + ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; + + NSNumber *imageIdentifier = @1; + + // Mock the data source such that we... + id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; + imageNode.dataSource = mockDataSource; + // (a) first expect to be hit for the image directly, and fail to return it. + [mockDataSource setExpectationOrderMatters:YES]; + [[[mockDataSource expect] andReturn:nil] multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]; + // (b) and then expect to be hit for the URL, which we'll return. + [[[mockDataSource expect] andReturn:[self _testImageURL]] multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]; + + // Mock the cache to do a cache-hit for the test image URL. + [[[_mockCache stub] andDo:^(NSInvocation *inv) { + // Params are URL, callbackQueue, completion + NSArray *URL; + [inv getArgument:&URL atIndex:2]; + + void (^completionBlock)(CGImageRef); + [inv getArgument:&completionBlock atIndex:4]; + + // Call the completion block with our test image and URL. + NSURL *testImageURL = [self _testImageURL]; + XCTAssertEqualObjects(URL, testImageURL, @"Fetching URL other than test image"); + completionBlock([self _testImage].CGImage); + }] fetchCachedImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] completion:[OCMArg any]]; + + // Kick off loading. + imageNode.imageIdentifiers = @[imageIdentifier]; + + // Verify the data source. + [mockDataSource verify]; + // Also expect it to be loaded immediately. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded"); + // And for the image to be equivalent to the image we provided. + XCTAssertEqualObjects(UIImagePNGRepresentation(imageNode.image), + UIImagePNGRepresentation([self _testImage]), + @"Loaded image isn't the one we provided"); + + imageNode.delegate = nil; + imageNode.dataSource = nil; + [imageNode release]; +} + +- (void)testAddLowerQualityImageIdentifier +{ + // Adding a lower quality image identifier should not cause any loading. + ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; + + NSNumber *highResIdentifier = @2; + + // Mock the data source such that we: (a) return the test image, and log whether we get hit for the lower-quality image. + id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; + imageNode.dataSource = mockDataSource; + __block int dataSourceHits = 0; + [[[mockDataSource stub] andDo:^(NSInvocation *inv) { + dataSourceHits++; + + // Return the test image. + [inv setReturnValue:(void *)[self _testImage]]; + }] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]]; + + imageNode.imageIdentifiers = @[highResIdentifier]; + + // At this point, we should have the high-res identifier loaded and the DS should have been hit once. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); + XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count"); + + // Add the low res identifier. + NSNumber *lowResIdentifier = @1; + imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier]; + + // At this point the high-res should still be loaded, and the data source should not have been hit. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); + XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count"); + + imageNode.delegate = nil; + imageNode.dataSource = nil; + [imageNode release]; +} + +- (void)testAddHigherQualityImageIdentifier +{ + // Adding a higher quality image identifier should cause loading. + ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; + + NSNumber *lowResIdentifier = @1; + + // Mock the data source such that we: (a) return the test image, and log how many times the DS gets hit. + id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; + imageNode.dataSource = mockDataSource; + __block int dataSourceHits = 0; + [[[mockDataSource stub] andDo:^(NSInvocation *inv) { + dataSourceHits++; + + // Return the test image. + [inv setReturnValue:(void *)[self _testImage]]; + }] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]]; + + imageNode.imageIdentifiers = @[lowResIdentifier]; + + // At this point, we should have the low-res identifier loaded and the DS should have been hit once. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, lowResIdentifier, @"Low res identifier should be loaded."); + XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count"); + + // Add the low res identifier. + NSNumber *highResIdentifier = @2; + imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier]; + + // At this point the high-res should be loaded, and the data source should been hit twice. + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); + XCTAssertTrue(dataSourceHits == 2, @"Unexpected DS hit count"); + + imageNode.delegate = nil; + imageNode.dataSource = nil; + [imageNode release]; +} + +- (void)testProgressiveDownloading +{ + ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; + imageNode.downloadsIntermediateImages = YES; + + // Set up a few identifiers to load. + NSInteger identifierCount = 5; + NSMutableArray *imageIdentifiers = [NSMutableArray array]; + for (NSInteger identifierIndex = 0; identifierIndex < identifierCount; identifierIndex++) + [imageIdentifiers insertObject:@(identifierIndex + 1) atIndex:0]; + + // Mock the data source to only make the images available progressively. + // This is necessary because ASMultiplexImageNode will try to grab the best image immediately, regardless of + // `downloadsIntermediateImages`. + id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; + imageNode.dataSource = mockDataSource; + __block NSUInteger loadedImageCount = 0; + [[[mockDataSource stub] andDo:^(NSInvocation *inv) { + id requestedIdentifier; + [inv getArgument:&requestedIdentifier atIndex:3]; + + NSInteger requestedIdentifierValue = [requestedIdentifier intValue]; + + // If no images are loaded, bail on trying to load anything but the worst image. + if (!imageNode.loadedImageIdentifier && requestedIdentifierValue != [[imageIdentifiers lastObject] integerValue]) + return; + + // Bail if it's trying to load an identifier that's more than one step than what's loaded. + NSInteger nextImageIdentifier = [imageNode.loadedImageIdentifier integerValue] + 1; + if (requestedIdentifierValue != nextImageIdentifier) + return; + + // Return the test image. + loadedImageCount++; + [inv setReturnValue:(void *)[self _testImage]]; + }] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]]; + + imageNode.imageIdentifiers = imageIdentifiers; + + XCTAssertTrue(loadedImageCount == identifierCount, @"Expected to load the same number of identifiers we supplied"); + + imageNode.delegate = nil; + imageNode.dataSource = nil; + [imageNode release]; +} + +- (void)testUncachedDownload +{ + // Mock a cache miss. + id mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)]; + [[[mockCache stub] andDo:^(NSInvocation *inv) { + void (^completion)(CGImageRef imageFromCache); + [inv getArgument:&completion atIndex:4]; + completion(nil); + }] fetchCachedImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] completion:[OCMArg any]]; + + // Mock a 50%-progress URL download. + id mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)]; + const CGFloat mockedProgress = 0.5; + [[[mockDownloader stub] andDo:^(NSInvocation *inv) { + // Simulate progress. + void (^progressBlock)(CGFloat progress); + [inv getArgument:&progressBlock atIndex:4]; + progressBlock(mockedProgress); + + // Simulate completion. + void (^completionBlock)(CGImageRef image, NSError *error); + [inv getArgument:&completionBlock atIndex:5]; + completionBlock([self _testImage].CGImage, nil); + }] downloadImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] downloadProgressBlock:[OCMArg any] completion:[OCMArg any]]; + + ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:mockCache downloader:mockDownloader]; + NSNumber *imageIdentifier = @1; + + // Mock the data source to return our test URL. + id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; + [[[mockDataSource stub] andReturn:[self _testImageURL]] multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]; + imageNode.dataSource = mockDataSource; + + // Mock the delegate to expect start, 50% progress, and completion invocations. + id mockDelegate = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDelegate)]; + [[mockDelegate expect] multiplexImageNode:imageNode didStartDownloadOfImageWithIdentifier:imageIdentifier]; + [[mockDelegate expect] multiplexImageNode:imageNode didUpdateDownloadProgress:mockedProgress forImageWithIdentifier:imageIdentifier]; + [[mockDelegate expect] multiplexImageNode:imageNode didFinishDownloadingImageWithIdentifier:imageIdentifier error:nil]; + [[mockDelegate expect] multiplexImageNode:imageNode didUpdateImage:[OCMArg any] withIdentifier:imageIdentifier fromImage:nil withIdentifier:nil]; + imageNode.delegate = mockDelegate; + + // Kick off loading. + imageNode.imageIdentifiers = @[imageIdentifier]; + + // Wait until the image is loaded. + ASRunRunLoopUntilBlockIsTrue(^BOOL{ + return [imageNode.loadedImageIdentifier isEqual:imageIdentifier]; + }); + + // Verify the delegation. + [mockDelegate verify]; + // Also verify that it's acutally loaded (could be false if we timed out above). + XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"Failed to load image"); + + [imageNode release]; +} + +@end \ No newline at end of file diff --git a/AsyncDisplayKitTests/TestResources/logo-square.png b/AsyncDisplayKitTests/TestResources/logo-square.png new file mode 100755 index 0000000000000000000000000000000000000000..82ad66c69ecdd8626cdd380e5c536dbcaeb0a0fc GIT binary patch literal 66422 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

Axt>VUn+{%`lVxh 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 literal 0 HcmV?d00001 diff --git a/Base/ASAvailability.h b/Base/ASAvailability.h new file mode 100644 index 0000000000..78293c3d63 --- /dev/null +++ b/Base/ASAvailability.h @@ -0,0 +1,40 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import + +#ifndef kCFCoreFoundationVersionNumber_IOS_7_0 +#define kCFCoreFoundationVersionNumber_IOS_7_0 838.00 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iOS_7_1 +#define kCFCoreFoundationVersionNumber_iOS_7_1 847.24 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iOS_8_0 +#define kCFCoreFoundationVersionNumber_iOS_8_0 1140.1 +#endif + +#ifndef __IPHONE_7_0 +#define __IPHONE_7_0 70000 +#endif + +#ifndef __IPHONE_8_0 +#define __IPHONE_8_0 80000 +#endif + +#ifndef AS_IOS8_SDK_OR_LATER +#define AS_IOS8_SDK_OR_LATER __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_8_0 +#endif + +#define AS_AT_LEAST_IOS7 (kCFCoreFoundationVersionNumber > kCFCoreFoundationVersionNumber_iOS_6_1) +#define AS_AT_LEAST_IOS7_1 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_1) +#define AS_AT_LEAST_IOS8 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_8_0) diff --git a/Base/ASLog.h b/Base/ASLog.h new file mode 100644 index 0000000000..9ced28dee1 --- /dev/null +++ b/Base/ASLog.h @@ -0,0 +1,15 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#define ASMultiplexImageNodeLogDebug NSLog +#define ASMultiplexImageNodeCLogDebug NSLog + +#define ASMultiplexImageNodeLogError NSLog +#define ASMultiplexImageNodeCLogError NSLog