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.
This commit is contained in:
Nadine Salter
2014-11-13 18:59:18 -08:00
parent 40cee777a4
commit 3c8d4e9517
11 changed files with 1304 additions and 10 deletions

View File

@@ -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

View File

@@ -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']

View File

@@ -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 = "<group>"; };
0516FA3B1A15563400B4EBED /* ASLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLog.h; sourceTree = "<group>"; };
0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMultiplexImageNode.h; sourceTree = "<group>"; };
0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMultiplexImageNode.mm; sourceTree = "<group>"; };
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 = "<group>"; };
052EE06A1A15A0D8002C6279 /* TestResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TestResources; sourceTree = "<group>"; };
053011A719B9882B00A9F2D0 /* ASRangeControllerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRangeControllerInternal.h; sourceTree = "<group>"; };
055F1A3219ABD3E3004DAFF1 /* ASTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableView.h; sourceTree = "<group>"; };
055F1A3319ABD3E3004DAFF1 /* ASTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableView.m; sourceTree = "<group>"; };
@@ -219,7 +237,6 @@
058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeInternal.h; sourceTree = "<group>"; };
058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+CGExtras.h"; sourceTree = "<group>"; };
058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ASImageNode+CGExtras.m"; sourceTree = "<group>"; };
058D0A0F195D050800B7D73C /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = "<group>"; };
058D0A10195D050800B7D73C /* ASSentinel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSentinel.h; sourceTree = "<group>"; };
058D0A11195D050800B7D73C /* ASSentinel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASSentinel.m; sourceTree = "<group>"; };
058D0A12195D050800B7D73C /* ASThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASThread.h; sourceTree = "<group>"; };
@@ -239,6 +256,7 @@
058D0A45195D058D00B7D73C /* ASDisplayNodeExtraIvars.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeExtraIvars.h; sourceTree = "<group>"; };
05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASDealloc2MainObject.h; path = ../Details/ASDealloc2MainObject.h; sourceTree = "<group>"; };
05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASDealloc2MainObject.m; path = ../Details/ASDealloc2MainObject.m; sourceTree = "<group>"; };
05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = "<group>"; };
3C9C128419E616EF00E942A0 /* ASTableViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewTests.m; sourceTree = "<group>"; };
6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = "<group>"; };
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 = "<group>"; };
@@ -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 */,

View File

@@ -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 <AsyncDisplayKit/ASImageNode.h>
#import <AsyncDisplayKit/ASImageProtocols.h>
@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 <imageIdentifiers> 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<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)downloader;
- (instancetype)init NS_UNAVAILABLE;
/**
* @abstract The delegate, which must conform to the <ASMultiplexImageNodeDelegate> protocol.
*/
@property (nonatomic, readwrite, weak) id <ASMultiplexImageNodeDelegate> delegate;
/**
* @abstract The data source, which must conform to the <ASMultiplexImageNodeDataSource> protocol.
* @discussion This value is required for ASMultiplexImageNode to load images.
*/
@property (nonatomic, readwrite, weak) id <ASMultiplexImageNodeDataSource> 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 <imageIdentifiers> (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 <downloadsIntermediateImages> 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 <imageIdentifiers>.
*
* @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 <displayedImageIdentifier> 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 <NSObject>
@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 <NSObject>
@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

View File

@@ -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 <AssetsLibrary/AssetsLibrary.h>
#import <Photos/Photos.h>
#import <libkern/OSAtomic.h>
#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<ASImageCacheProtocol> _cache;
id<ASImageDownloaderProtocol> _downloader;
__weak id<ASMultiplexImageNodeDelegate> _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<ASMultiplexImageNodeDataSource> _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<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)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 <ASMultiplexImageNodeDelegate>)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 <ASMultiplexImageNodeDataSource>)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

View File

@@ -13,5 +13,7 @@
#import <AsyncDisplayKit/ASImageNode.h>
#import <AsyncDisplayKit/ASTextNode.h>
#import <AsyncDisplayKit/ASMultiplexImageNode.h>
#import <AsyncDisplayKit/ASTableView.h>
#import <AsyncDisplayKit/ASCellNode.h>

View File

@@ -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 <OCMock/OCMock.h>
#import <AsyncDisplayKit/ASImageProtocols.h>
#import <AsyncDisplayKit/ASMultiplexImageNode.h>
#import <libkern/OSAtomic.h>
#import <XCTest/XCTest.h>
@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

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

40
Base/ASAvailability.h Normal file
View File

@@ -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 <Availability.h>
#import <AvailabilityInternal.h>
#import <CoreFoundation/CFBase.h>
#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)

15
Base/ASLog.h Normal file
View File

@@ -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