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 0000000000..82ad66c69e Binary files /dev/null and b/AsyncDisplayKitTests/TestResources/logo-square.png differ 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