From ee0c027ba68e0299c2ccb803f84de2ed5bef0d9c Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 25 Sep 2015 15:25:37 -0700 Subject: [PATCH 01/12] Build photos image request class and tests --- AsyncDisplayKit.xcodeproj/project.pbxproj | 12 ++ .../Details/ASPhotosImageRequest.h | 74 ++++++++ .../Details/ASPhotosImageRequest.m | 162 ++++++++++++++++++ .../ASPhotosImageRequestTests.m | 59 +++++++ 4 files changed, 307 insertions(+) create mode 100644 AsyncDisplayKit/Details/ASPhotosImageRequest.h create mode 100644 AsyncDisplayKit/Details/ASPhotosImageRequest.m create mode 100644 AsyncDisplayKitTests/ASPhotosImageRequestTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index c2fe465643..09d00b2816 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -374,6 +374,9 @@ B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; }; B350625F1B0111800018CF92 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AF195D04C000B7D73C /* Foundation.framework */; }; + CC7FD9DE1BB5E962005CCB2B /* ASPhotosImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */; settings = {ASSET_TAGS = (); }; }; + CC7FD9DF1BB5E962005CCB2B /* ASPhotosImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosImageRequest.m */; settings = {ASSET_TAGS = (); }; }; + CC7FD9E11BB5F750005CCB2B /* ASPhotosImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosImageRequestTests.m */; settings = {ASSET_TAGS = (); }; }; D785F6621A74327E00291744 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; D785F6631A74327E00291744 /* ASScrollNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.m */; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -619,6 +622,9 @@ ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackLayoutSpecSnapshotTests.mm; sourceTree = ""; }; B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B35061DD1B010EDF0018CF92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "../AsyncDisplayKit-iOS/Info.plist"; sourceTree = ""; }; + CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosImageRequest.h; sourceTree = ""; }; + CC7FD9DD1BB5E962005CCB2B /* ASPhotosImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosImageRequest.m; sourceTree = ""; }; + CC7FD9E01BB5F750005CCB2B /* ASPhotosImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosImageRequestTests.m; sourceTree = ""; }; D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = ""; }; @@ -801,6 +807,7 @@ ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m */, 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */, 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */, + CC7FD9E01BB5F750005CCB2B /* ASPhotosImageRequestTests.m */, 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */, 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */, 2911485B1A77147A005D0878 /* ASControlNodeTests.m */, @@ -836,6 +843,8 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */, + CC7FD9DD1BB5E962005CCB2B /* ASPhotosImageRequest.m */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -1106,6 +1115,7 @@ 9C8221951BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */, 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */, AC21EC101B3D0BF600C8B19A /* ASStackLayoutDefines.h in Headers */, + CC7FD9DE1BB5E962005CCB2B /* ASPhotosImageRequest.h in Headers */, ACF6ED2F1B17843500DA7C62 /* ASStackLayoutSpec.h in Headers */, ACF6ED4E1B17847A00DA7C62 /* ASStackLayoutSpecUtilities.h in Headers */, ACF6ED4F1B17847A00DA7C62 /* ASStackPositionedLayout.h in Headers */, @@ -1493,6 +1503,7 @@ 058D0A21195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.m in Sources */, 205F0E101B371875007741D0 /* UICollectionViewLayout+ASConvenience.m in Sources */, 058D0A25195D050800B7D73C /* UIView+ASConvenience.m in Sources */, + CC7FD9DF1BB5E962005CCB2B /* ASPhotosImageRequest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1514,6 +1525,7 @@ 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */, ACF6ED5E1B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm in Sources */, ACF6ED601B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m in Sources */, + CC7FD9E11BB5F750005CCB2B /* ASPhotosImageRequestTests.m in Sources */, 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m in Sources */, ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */, diff --git a/AsyncDisplayKit/Details/ASPhotosImageRequest.h b/AsyncDisplayKit/Details/ASPhotosImageRequest.h new file mode 100644 index 0000000000..92d9910924 --- /dev/null +++ b/AsyncDisplayKit/Details/ASPhotosImageRequest.h @@ -0,0 +1,74 @@ +// +// ASPhotosImageRequest.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/25/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import +#import + +// NS_ASSUME_NONNULL_BEGIN + +extern NSString *const ASPhotosURLScheme; + +/** + @abstract Use ASPhotosImageRequest to encapsulate all the information needed to request an image from + the Photos framework and store it in a URL. + */ +@interface ASPhotosImageRequest : NSObject + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier NS_DESIGNATED_INITIALIZER; + +/** + @return A new image request deserialized from `url`, or nil if `url` is not a valid photos URL. + */ ++ (/*nullable*/ ASPhotosImageRequest *)requestWithURL:(NSURL *)url; + +/** + @abstract The asset identifier for this image request provided during initialization. + */ +@property (nonatomic, readonly) NSString *assetIdentifier; + +/** + @abstract The target size for this image request. Defaults to `PHImageManagerMaximumSize`. + */ +@property (nonatomic) CGSize targetSize; + +/** + @abstract The content mode for this image request. Defaults to `PHImageContentModeDefault`. + + @see `PHImageManager` + */ +@property (nonatomic) PHImageContentMode contentMode; + +/** + @abstract The options specified for this request. Default value is the result of `[PHImageRequestOptions new]`. + + @discussion Some properties of this object are ignored when converting this request into a URL. + As of iOS SDK 9.0, these properties are `progressHandler`, `synchronous`, and `deliveryMode`. + */ +@property (nonatomic, strong) PHImageRequestOptions *options; + +/** + @return A new URL converted from this request. + */ +@property (nonatomic, readonly) NSURL *url; + +/** + @return `YES` if `object` is an equivalent image request, `NO` otherwise. + */ +- (BOOL)isEqual:(id)object; +@end + +@interface NSURL (ASPhotosRequestConverting) + +/** + @abstract A convenience function that calls `[ASPhotosImageRequest requestWithURL:self]`. + */ +- (/*nullable*/ ASPhotosImageRequest *)asyncdisplaykit_photosRequest; + +@end + +// NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Details/ASPhotosImageRequest.m b/AsyncDisplayKit/Details/ASPhotosImageRequest.m new file mode 100644 index 0000000000..bc506ec916 --- /dev/null +++ b/AsyncDisplayKit/Details/ASPhotosImageRequest.m @@ -0,0 +1,162 @@ +// +// ASPhotosImageRequest.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/25/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import "ASPhotosImageRequest.h" +#import "ASBaseDefines.h" + +NSString *const ASPhotosURLScheme = @"ph"; + +static NSString *const _ASPhotosURLQueryKeyWidth = @"width"; +static NSString *const _ASPhotosURLQueryKeyHeight = @"height"; + +// value is PHImageContentMode value +static NSString *const _ASPhotosURLQueryKeyContentMode = @"contentmode"; + +// value is PHImageRequestOptionsResizeMode value +static NSString *const _ASPhotosURLQueryKeyResizeMode = @"resizemode"; + +// value is PHImageRequestOptionsVersion value +static NSString *const _ASPhotosURLQueryKeyVersion = @"version"; + +// value is 0 or 1 +static NSString *const _ASPhotosURLQueryKeyAllowNetworkAccess = @"network"; + +static NSString *const _ASPhotosURLQueryKeyCropOriginX = @"crop_x"; +static NSString *const _ASPhotosURLQueryKeyCropOriginY = @"crop_y"; +static NSString *const _ASPhotosURLQueryKeyCropWidth = @"crop_w"; +static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; + +@implementation ASPhotosImageRequest + +- (instancetype)init +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + self = [self initWithAssetIdentifier:@""]; + return nil; +} + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier +{ + self = [super init]; + if (self) { + _assetIdentifier = assetIdentifier; + _options = [PHImageRequestOptions new]; + _contentMode = PHImageContentModeDefault; + _targetSize = PHImageManagerMaximumSize; + } + return self; +} + +#pragma mark NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + ASPhotosImageRequest *copy = [[ASPhotosImageRequest alloc] initWithAssetIdentifier:self.assetIdentifier]; + copy.options = [self.options copy]; + copy.targetSize = self.targetSize; + copy.contentMode = self.contentMode; + return copy; +} + +#pragma mark Converting to URL + +- (NSURL *)url +{ + NSURLComponents *comp = [NSURLComponents new]; + comp.scheme = ASPhotosURLScheme; + comp.host = _assetIdentifier; + NSMutableArray *queryItems = [NSMutableArray arrayWithObjects: + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyWidth value:@(_targetSize.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyHeight value:@(_targetSize.height).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyVersion value:@(_options.version).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyContentMode value:@(_contentMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyAllowNetworkAccess value:@(_options.networkAccessAllowed).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyResizeMode value:@(_options.resizeMode).stringValue] + , nil]; + + CGRect cropRect = _options.normalizedCropRect; + if (!CGRectIsEmpty(cropRect)) { + [queryItems addObjectsFromArray:@[ + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginX value:@(cropRect.origin.x).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginY value:@(cropRect.origin.y).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropWidth value:@(cropRect.size.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropHeight value:@(cropRect.size.height).stringValue] + ]]; + } + comp.queryItems = queryItems; + return comp.URL; +} + +#pragma mark Converting from URL + ++ (ASPhotosImageRequest *)requestWithURL:(NSURL *)url +{ + NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + // not a photos URL + if (![comp.scheme isEqualToString:ASPhotosURLScheme]) { + return nil; + } + + ASPhotosImageRequest *request = [[ASPhotosImageRequest alloc] initWithAssetIdentifier:url.host]; + + CGRect cropRect = CGRectZero; + CGSize targetSize = PHImageManagerMaximumSize; + for (NSURLQueryItem *item in comp.queryItems) { + if ([_ASPhotosURLQueryKeyAllowNetworkAccess isEqualToString:item.name]) { + request.options.networkAccessAllowed = item.value.boolValue; + } else if ([_ASPhotosURLQueryKeyWidth isEqualToString:item.name]) { + targetSize.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyHeight isEqualToString:item.name]) { + targetSize.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyContentMode isEqualToString:item.name]) { + request.contentMode = (PHImageContentMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyVersion isEqualToString:item.name]) { + request.options.version = (PHImageRequestOptionsVersion)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyCropOriginX isEqualToString:item.name]) { + cropRect.origin.x = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropOriginY isEqualToString:item.name]) { + cropRect.origin.y = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropWidth isEqualToString:item.name]) { + cropRect.size.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropHeight isEqualToString:item.name]) { + cropRect.size.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyResizeMode isEqualToString:item.name]) { + request.options.resizeMode = (PHImageRequestOptionsResizeMode)item.value.integerValue; + } + } + request.targetSize = targetSize; + request.options.normalizedCropRect = cropRect; + return request; +} + +#pragma mark NSObject + +- (BOOL)isEqual:(id)object +{ + if (![object isKindOfClass:ASPhotosImageRequest.class]) { + return NO; + } + ASPhotosImageRequest *other = object; + return [other.assetIdentifier isEqualToString:self.assetIdentifier] && + other.contentMode == self.contentMode && + CGSizeEqualToSize(other.targetSize, self.targetSize) && + CGRectEqualToRect(other.options.normalizedCropRect, self.options.normalizedCropRect) && + other.options.resizeMode == self.options.resizeMode && + other.options.version == self.options.version; +} + +@end + +@implementation NSURL (ASPhotosRequestConverting) + +- (ASPhotosImageRequest *)asyncdisplaykit_photosRequest +{ + return [ASPhotosImageRequest requestWithURL:self]; +} + +@end diff --git a/AsyncDisplayKitTests/ASPhotosImageRequestTests.m b/AsyncDisplayKitTests/ASPhotosImageRequestTests.m new file mode 100644 index 0000000000..38b8cc7170 --- /dev/null +++ b/AsyncDisplayKitTests/ASPhotosImageRequestTests.m @@ -0,0 +1,59 @@ +// +// ASPhotosImageRequestTests.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/25/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import +#import "ASPhotosImageRequest.h" + +static NSString *const kTestAssetID = @"testAssetID"; + +@interface ASPhotosImageRequestTests : XCTestCase + +@end + +@implementation ASPhotosImageRequestTests + +#pragma mark Example Data + ++ (ASPhotosImageRequest *)exampleImageRequest +{ + ASPhotosImageRequest *req = [[ASPhotosImageRequest alloc] initWithAssetIdentifier:kTestAssetID]; + req.options.networkAccessAllowed = YES; + req.options.normalizedCropRect = CGRectMake(0.2, 0.1, 0.6, 0.8); + req.targetSize = CGSizeMake(1024, 1536); + req.contentMode = PHImageContentModeAspectFill; + req.options.version = PHImageRequestOptionsVersionOriginal; + req.options.resizeMode = PHImageRequestOptionsResizeModeFast; + return req; +} + ++ (NSURL *)urlForExampleImageRequest +{ + NSString *str = [NSString stringWithFormat:@"ph://%@?width=1024&height=1536&version=2&contentmode=1&network=1&resizemode=1&crop_x=0.2&crop_y=0.1&crop_w=0.6&crop_h=0.8", kTestAssetID]; + return [NSURL URLWithString:str]; +} + +#pragma mark Test cases + +- (void)testThatConvertingToURLWorks +{ + XCTAssertEqualObjects([self.class exampleImageRequest].url, [self.class urlForExampleImageRequest]); +} + +- (void)testThatParsingFromURLWorks +{ + XCTAssertEqualObjects([self.class urlForExampleImageRequest].asyncdisplaykit_photosRequest, [self.class exampleImageRequest]); +} + +- (void)testThatCopyingWorks +{ + ASPhotosImageRequest *example = [self.class exampleImageRequest]; + ASPhotosImageRequest *copy = [[self.class exampleImageRequest] copy]; + XCTAssertEqualObjects(example, copy); +} + +@end From bbf9550e081bf3c4863e0de03622ce7de541284b Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 25 Sep 2015 15:41:35 -0700 Subject: [PATCH 02/12] Use ASPhotosImageRequest in ASMultiplexImageNode, in a backwards-compatible way --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++- AsyncDisplayKit/ASMultiplexImageNode.h | 2 +- AsyncDisplayKit/ASMultiplexImageNode.mm | 32 +++++++------------ AsyncDisplayKit/AsyncDisplayKit.h | 1 + .../Details/ASPhotosImageRequest.h | 1 + 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 09d00b2816..92d75212a8 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -374,9 +374,10 @@ B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; }; B350625F1B0111800018CF92 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AF195D04C000B7D73C /* Foundation.framework */; }; - CC7FD9DE1BB5E962005CCB2B /* ASPhotosImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */; settings = {ASSET_TAGS = (); }; }; + CC7FD9DE1BB5E962005CCB2B /* ASPhotosImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC7FD9DF1BB5E962005CCB2B /* ASPhotosImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosImageRequest.m */; settings = {ASSET_TAGS = (); }; }; CC7FD9E11BB5F750005CCB2B /* ASPhotosImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosImageRequestTests.m */; settings = {ASSET_TAGS = (); }; }; + CC7FD9E21BB603FF005CCB2B /* ASPhotosImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; D785F6621A74327E00291744 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; D785F6631A74327E00291744 /* ASScrollNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.m */; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -1218,6 +1219,7 @@ 9C8221961BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */, 9C49C3701B853961000B0DD5 /* ASStackLayoutable.h in Headers */, 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */, + CC7FD9E21BB603FF005CCB2B /* ASPhotosImageRequest.h in Headers */, 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */, 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */, 044284FE1BAA387800D16268 /* ASStackLayoutSpecUtilities.h in Headers */, diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index db10daa324..ce9f22f16e 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -195,7 +195,7 @@ didFinishDownloadingImageWithIdentifier:(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 + * @discussion Supported URLs include assets-library, URLs converted from ASPhotosImageRequest, 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. diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index d32d3be061..987575929f 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -18,6 +18,7 @@ #import "ASBaseDefines.h" #import "ASDisplayNode+Subclasses.h" #import "ASLog.h" +#import "ASPhotosImageRequest.h" #if !AS_IOS8_SDK_OR_LATER #error ASMultiplexImageNode can be used on iOS 7, but must be linked against the iOS 8 SDK. @@ -26,8 +27,6 @@ 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. @@ -120,14 +119,14 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent - (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. + @abstract Loads the image corresponding to the given image request 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 request The photos image request to load. 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; +- (void)_loadPHAssetWithRequest:(ASPhotosImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock; /** @abstract Downloads the image corresponding to the given imageIdentifier from the given URL. @@ -456,8 +455,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent }]; } // 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) { + else if (ASPhotosImageRequest *request = nextImageURL.asyncdisplaykit_photosRequest) { + [self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) { ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from Photos Framework", weakSelf, nextImageIdentifier); finishedLoadingBlock(image, nextImageIdentifier, error); }]; @@ -511,17 +510,15 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent }]; } -- (void)_loadPHAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock +- (void)_loadPHAssetWithRequest:(ASPhotosImageRequest *)request identifier:(id)imageIdentifier 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(request, @"request 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]; + PHFetchResult *assetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil]; if ([assetFetchResult count] == 0) { // Error. completionBlock(nil, nil); @@ -531,15 +528,10 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent // 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 + targetSize:request.targetSize + contentMode:request.contentMode + options:request.options resultHandler:^(UIImage *image, NSDictionary *info) { completionBlock(image, info[PHImageErrorKey]); }]; diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index 6b2aa9ea9d..57fe043902 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -18,6 +18,7 @@ #import #import #import +#import #import #import diff --git a/AsyncDisplayKit/Details/ASPhotosImageRequest.h b/AsyncDisplayKit/Details/ASPhotosImageRequest.h index 92d9910924..45e34c9042 100644 --- a/AsyncDisplayKit/Details/ASPhotosImageRequest.h +++ b/AsyncDisplayKit/Details/ASPhotosImageRequest.h @@ -60,6 +60,7 @@ extern NSString *const ASPhotosURLScheme; @return `YES` if `object` is an equivalent image request, `NO` otherwise. */ - (BOOL)isEqual:(id)object; + @end @interface NSURL (ASPhotosRequestConverting) From e787c4041116594314b5d16405043286ece0bf29 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 25 Sep 2015 15:45:14 -0700 Subject: [PATCH 03/12] Only parse photos URL after we check the scheme --- AsyncDisplayKit/Details/ASPhotosImageRequest.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/Details/ASPhotosImageRequest.m b/AsyncDisplayKit/Details/ASPhotosImageRequest.m index bc506ec916..4653f8a049 100644 --- a/AsyncDisplayKit/Details/ASPhotosImageRequest.m +++ b/AsyncDisplayKit/Details/ASPhotosImageRequest.m @@ -96,12 +96,13 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; + (ASPhotosImageRequest *)requestWithURL:(NSURL *)url { - NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; // not a photos URL - if (![comp.scheme isEqualToString:ASPhotosURLScheme]) { + if (![url.scheme isEqualToString:ASPhotosURLScheme]) { return nil; } + NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + ASPhotosImageRequest *request = [[ASPhotosImageRequest alloc] initWithAssetIdentifier:url.host]; CGRect cropRect = CGRectZero; From 06556d04a3d89398a48625fbbee6c6f7a9828b46 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 25 Sep 2015 15:54:17 -0700 Subject: [PATCH 04/12] ASMultiplexImageNode uses high quality format when requesting images --- AsyncDisplayKit/ASMultiplexImageNode.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 987575929f..841e2b64e9 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -527,11 +527,13 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent // Get the best image we can. PHAsset *imageAsset = [assetFetchResult firstObject]; + PHImageRequestOptions *options = [request.options copy]; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; [[PHImageManager defaultManager] requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode - options:request.options + options:options resultHandler:^(UIImage *image, NSDictionary *info) { completionBlock(image, info[PHImageErrorKey]); }]; From bf88bd8e197f9495f2a10a6815700fdc947c7d3d Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sat, 26 Sep 2015 13:40:00 -0700 Subject: [PATCH 05/12] Rename image request class, add deliveryMode into the request params --- AsyncDisplayKit.xcodeproj/project.pbxproj | 28 +++++++++---------- AsyncDisplayKit/ASMultiplexImageNode.h | 2 +- AsyncDisplayKit/ASMultiplexImageNode.mm | 8 +++--- AsyncDisplayKit/AsyncDisplayKit.h | 2 +- ...uest.h => ASPhotosFrameworkImageRequest.h} | 16 ++++++----- ...uest.m => ASPhotosFrameworkImageRequest.m} | 28 +++++++++++-------- ...m => ASPhotosFrameworkImageRequestTests.m} | 16 +++++------ 7 files changed, 54 insertions(+), 46 deletions(-) rename AsyncDisplayKit/Details/{ASPhotosImageRequest.h => ASPhotosFrameworkImageRequest.h} (68%) rename AsyncDisplayKit/Details/{ASPhotosImageRequest.m => ASPhotosFrameworkImageRequest.m} (82%) rename AsyncDisplayKitTests/{ASPhotosImageRequestTests.m => ASPhotosFrameworkImageRequestTests.m} (70%) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 92d75212a8..3cf1189c0b 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -374,10 +374,10 @@ B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; }; B350625F1B0111800018CF92 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AF195D04C000B7D73C /* Foundation.framework */; }; - CC7FD9DE1BB5E962005CCB2B /* ASPhotosImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CC7FD9DF1BB5E962005CCB2B /* ASPhotosImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosImageRequest.m */; settings = {ASSET_TAGS = (); }; }; - CC7FD9E11BB5F750005CCB2B /* ASPhotosImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosImageRequestTests.m */; settings = {ASSET_TAGS = (); }; }; - CC7FD9E21BB603FF005CCB2B /* ASPhotosImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; settings = {ASSET_TAGS = (); }; }; + CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; settings = {ASSET_TAGS = (); }; }; + CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; D785F6621A74327E00291744 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; D785F6631A74327E00291744 /* ASScrollNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.m */; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -623,9 +623,9 @@ ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackLayoutSpecSnapshotTests.mm; sourceTree = ""; }; B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B35061DD1B010EDF0018CF92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "../AsyncDisplayKit-iOS/Info.plist"; sourceTree = ""; }; - CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosImageRequest.h; sourceTree = ""; }; - CC7FD9DD1BB5E962005CCB2B /* ASPhotosImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosImageRequest.m; sourceTree = ""; }; - CC7FD9E01BB5F750005CCB2B /* ASPhotosImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosImageRequestTests.m; sourceTree = ""; }; + CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; + CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = ""; }; + CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = ""; }; D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = ""; }; @@ -808,7 +808,7 @@ ACF6ED581B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m */, 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */, 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */, - CC7FD9E01BB5F750005CCB2B /* ASPhotosImageRequestTests.m */, + CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */, 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */, 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */, 2911485B1A77147A005D0878 /* ASControlNodeTests.m */, @@ -844,8 +844,8 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( - CC7FD9DC1BB5E962005CCB2B /* ASPhotosImageRequest.h */, - CC7FD9DD1BB5E962005CCB2B /* ASPhotosImageRequest.m */, + CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */, + CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -1116,7 +1116,7 @@ 9C8221951BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */, 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */, AC21EC101B3D0BF600C8B19A /* ASStackLayoutDefines.h in Headers */, - CC7FD9DE1BB5E962005CCB2B /* ASPhotosImageRequest.h in Headers */, + CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, ACF6ED2F1B17843500DA7C62 /* ASStackLayoutSpec.h in Headers */, ACF6ED4E1B17847A00DA7C62 /* ASStackLayoutSpecUtilities.h in Headers */, ACF6ED4F1B17847A00DA7C62 /* ASStackPositionedLayout.h in Headers */, @@ -1219,7 +1219,7 @@ 9C8221961BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */, 9C49C3701B853961000B0DD5 /* ASStackLayoutable.h in Headers */, 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */, - CC7FD9E21BB603FF005CCB2B /* ASPhotosImageRequest.h in Headers */, + CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */, 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */, 044284FE1BAA387800D16268 /* ASStackLayoutSpecUtilities.h in Headers */, @@ -1505,7 +1505,7 @@ 058D0A21195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.m in Sources */, 205F0E101B371875007741D0 /* UICollectionViewLayout+ASConvenience.m in Sources */, 058D0A25195D050800B7D73C /* UIView+ASConvenience.m in Sources */, - CC7FD9DF1BB5E962005CCB2B /* ASPhotosImageRequest.m in Sources */, + CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1527,7 +1527,7 @@ 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */, ACF6ED5E1B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm in Sources */, ACF6ED601B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.m in Sources */, - CC7FD9E11BB5F750005CCB2B /* ASPhotosImageRequestTests.m in Sources */, + CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */, 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m in Sources */, ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */, diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index ce9f22f16e..5b811f6654 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -195,7 +195,7 @@ didFinishDownloadingImageWithIdentifier:(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, URLs converted from ASPhotosImageRequest, HTTP, HTTPS, and FTP URLs. If the + * @discussion Supported URLs include assets-library, URLs converted from ASPhotosFrameworkImageRequest, 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. diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 841e2b64e9..c3028b0123 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -18,7 +18,7 @@ #import "ASBaseDefines.h" #import "ASDisplayNode+Subclasses.h" #import "ASLog.h" -#import "ASPhotosImageRequest.h" +#import "ASPhotosFrameworkImageRequest.h" #if !AS_IOS8_SDK_OR_LATER #error ASMultiplexImageNode can be used on iOS 7, but must be linked against the iOS 8 SDK. @@ -126,7 +126,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent @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)_loadPHAssetWithRequest:(ASPhotosImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock; +- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock; /** @abstract Downloads the image corresponding to the given imageIdentifier from the given URL. @@ -455,7 +455,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent }]; } // Likewise, if it's a iOS 8 Photo asset, we need to fetch it accordingly. - else if (ASPhotosImageRequest *request = nextImageURL.asyncdisplaykit_photosRequest) { + else if (ASPhotosFrameworkImageRequest *request = nextImageURL.asyncdisplaykit_photosRequest) { [self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) { ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from Photos Framework", weakSelf, nextImageIdentifier); finishedLoadingBlock(image, nextImageIdentifier, error); @@ -510,7 +510,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent }]; } -- (void)_loadPHAssetWithRequest:(ASPhotosImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock +- (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock { ASDisplayNodeAssert(AS_AT_LEAST_IOS8, @"PhotosKit is unavailable on iOS 7."); ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index 57fe043902..2f4d77d15b 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -18,7 +18,7 @@ #import #import #import -#import +#import #import #import diff --git a/AsyncDisplayKit/Details/ASPhotosImageRequest.h b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h similarity index 68% rename from AsyncDisplayKit/Details/ASPhotosImageRequest.h rename to AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h index 45e34c9042..ebf04fdfbc 100644 --- a/AsyncDisplayKit/Details/ASPhotosImageRequest.h +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h @@ -1,5 +1,5 @@ // -// ASPhotosImageRequest.h +// ASPhotosFrameworkImageRequest.h // AsyncDisplayKit // // Created by Adlai Holler on 9/25/15. @@ -14,17 +14,17 @@ extern NSString *const ASPhotosURLScheme; /** - @abstract Use ASPhotosImageRequest to encapsulate all the information needed to request an image from + @abstract Use ASPhotosFrameworkImageRequest to encapsulate all the information needed to request an image from the Photos framework and store it in a URL. */ -@interface ASPhotosImageRequest : NSObject +@interface ASPhotosFrameworkImageRequest : NSObject - (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier NS_DESIGNATED_INITIALIZER; /** @return A new image request deserialized from `url`, or nil if `url` is not a valid photos URL. */ -+ (/*nullable*/ ASPhotosImageRequest *)requestWithURL:(NSURL *)url; ++ (/*nullable*/ ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url; /** @abstract The asset identifier for this image request provided during initialization. @@ -47,7 +47,9 @@ extern NSString *const ASPhotosURLScheme; @abstract The options specified for this request. Default value is the result of `[PHImageRequestOptions new]`. @discussion Some properties of this object are ignored when converting this request into a URL. - As of iOS SDK 9.0, these properties are `progressHandler`, `synchronous`, and `deliveryMode`. + As of iOS SDK 9.0, these properties are `progressHandler` and `synchronous`. + Note that PHImageRequestOptionsDeliveryModeOpportunistic is not recommended when using ASMultiplexImageNode, + because it sends multiple images and only the first will be accepted. */ @property (nonatomic, strong) PHImageRequestOptions *options; @@ -66,9 +68,9 @@ extern NSString *const ASPhotosURLScheme; @interface NSURL (ASPhotosRequestConverting) /** - @abstract A convenience function that calls `[ASPhotosImageRequest requestWithURL:self]`. + @abstract A convenience function that calls `[ASPhotosFrameworkImageRequest requestWithURL:self]`. */ -- (/*nullable*/ ASPhotosImageRequest *)asyncdisplaykit_photosRequest; +- (/*nullable*/ ASPhotosFrameworkImageRequest *)asyncdisplaykit_photosRequest; @end diff --git a/AsyncDisplayKit/Details/ASPhotosImageRequest.m b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m similarity index 82% rename from AsyncDisplayKit/Details/ASPhotosImageRequest.m rename to AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m index 4653f8a049..dcf47957c6 100644 --- a/AsyncDisplayKit/Details/ASPhotosImageRequest.m +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m @@ -1,12 +1,12 @@ // -// ASPhotosImageRequest.m +// ASPhotosFrameworkImageRequest.m // AsyncDisplayKit // // Created by Adlai Holler on 9/25/15. // Copyright © 2015 Facebook. All rights reserved. // -#import "ASPhotosImageRequest.h" +#import "ASPhotosFrameworkImageRequest.h" #import "ASBaseDefines.h" NSString *const ASPhotosURLScheme = @"ph"; @@ -20,6 +20,9 @@ static NSString *const _ASPhotosURLQueryKeyContentMode = @"contentmode"; // value is PHImageRequestOptionsResizeMode value static NSString *const _ASPhotosURLQueryKeyResizeMode = @"resizemode"; +// value is PHImageRequestOptionsDeliveryMode value +static NSString *const _ASPhotosURLQueryKeyDeliveryMode = @"deliverymode"; + // value is PHImageRequestOptionsVersion value static NSString *const _ASPhotosURLQueryKeyVersion = @"version"; @@ -31,7 +34,7 @@ static NSString *const _ASPhotosURLQueryKeyCropOriginY = @"crop_y"; static NSString *const _ASPhotosURLQueryKeyCropWidth = @"crop_w"; static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; -@implementation ASPhotosImageRequest +@implementation ASPhotosFrameworkImageRequest - (instancetype)init { @@ -56,7 +59,7 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; - (id)copyWithZone:(NSZone *)zone { - ASPhotosImageRequest *copy = [[ASPhotosImageRequest alloc] initWithAssetIdentifier:self.assetIdentifier]; + ASPhotosFrameworkImageRequest *copy = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:self.assetIdentifier]; copy.options = [self.options copy]; copy.targetSize = self.targetSize; copy.contentMode = self.contentMode; @@ -76,7 +79,8 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyVersion value:@(_options.version).stringValue], [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyContentMode value:@(_contentMode).stringValue], [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyAllowNetworkAccess value:@(_options.networkAccessAllowed).stringValue], - [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyResizeMode value:@(_options.resizeMode).stringValue] + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyResizeMode value:@(_options.resizeMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyDeliveryMode value:@(_options.deliveryMode).stringValue] , nil]; CGRect cropRect = _options.normalizedCropRect; @@ -94,7 +98,7 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; #pragma mark Converting from URL -+ (ASPhotosImageRequest *)requestWithURL:(NSURL *)url ++ (ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url { // not a photos URL if (![url.scheme isEqualToString:ASPhotosURLScheme]) { @@ -103,7 +107,7 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; - ASPhotosImageRequest *request = [[ASPhotosImageRequest alloc] initWithAssetIdentifier:url.host]; + ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:url.host]; CGRect cropRect = CGRectZero; CGSize targetSize = PHImageManagerMaximumSize; @@ -128,6 +132,8 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; cropRect.size.height = item.value.doubleValue; } else if ([_ASPhotosURLQueryKeyResizeMode isEqualToString:item.name]) { request.options.resizeMode = (PHImageRequestOptionsResizeMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyDeliveryMode isEqualToString:item.name]) { + request.options.deliveryMode = (PHImageRequestOptionsDeliveryMode)item.value.integerValue; } } request.targetSize = targetSize; @@ -139,10 +145,10 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; - (BOOL)isEqual:(id)object { - if (![object isKindOfClass:ASPhotosImageRequest.class]) { + if (![object isKindOfClass:ASPhotosFrameworkImageRequest.class]) { return NO; } - ASPhotosImageRequest *other = object; + ASPhotosFrameworkImageRequest *other = object; return [other.assetIdentifier isEqualToString:self.assetIdentifier] && other.contentMode == self.contentMode && CGSizeEqualToSize(other.targetSize, self.targetSize) && @@ -155,9 +161,9 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; @implementation NSURL (ASPhotosRequestConverting) -- (ASPhotosImageRequest *)asyncdisplaykit_photosRequest +- (ASPhotosFrameworkImageRequest *)asyncdisplaykit_photosRequest { - return [ASPhotosImageRequest requestWithURL:self]; + return [ASPhotosFrameworkImageRequest requestWithURL:self]; } @end diff --git a/AsyncDisplayKitTests/ASPhotosImageRequestTests.m b/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m similarity index 70% rename from AsyncDisplayKitTests/ASPhotosImageRequestTests.m rename to AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m index 38b8cc7170..5c276706b4 100644 --- a/AsyncDisplayKitTests/ASPhotosImageRequestTests.m +++ b/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m @@ -1,5 +1,5 @@ // -// ASPhotosImageRequestTests.m +// ASPhotosFrameworkImageRequestTests.m // AsyncDisplayKit // // Created by Adlai Holler on 9/25/15. @@ -7,21 +7,21 @@ // #import -#import "ASPhotosImageRequest.h" +#import "ASPhotosFrameworkImageRequest.h" static NSString *const kTestAssetID = @"testAssetID"; -@interface ASPhotosImageRequestTests : XCTestCase +@interface ASPhotosFrameworkImageRequestTests : XCTestCase @end -@implementation ASPhotosImageRequestTests +@implementation ASPhotosFrameworkImageRequestTests #pragma mark Example Data -+ (ASPhotosImageRequest *)exampleImageRequest ++ (ASPhotosFrameworkImageRequest *)exampleImageRequest { - ASPhotosImageRequest *req = [[ASPhotosImageRequest alloc] initWithAssetIdentifier:kTestAssetID]; + ASPhotosFrameworkImageRequest *req = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:kTestAssetID]; req.options.networkAccessAllowed = YES; req.options.normalizedCropRect = CGRectMake(0.2, 0.1, 0.6, 0.8); req.targetSize = CGSizeMake(1024, 1536); @@ -51,8 +51,8 @@ static NSString *const kTestAssetID = @"testAssetID"; - (void)testThatCopyingWorks { - ASPhotosImageRequest *example = [self.class exampleImageRequest]; - ASPhotosImageRequest *copy = [[self.class exampleImageRequest] copy]; + ASPhotosFrameworkImageRequest *example = [self.class exampleImageRequest]; + ASPhotosFrameworkImageRequest *copy = [[self.class exampleImageRequest] copy]; XCTAssertEqualObjects(example, copy); } From ffa0829fc932b98f4f4e3559c405d78cd9851595 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sat, 26 Sep 2015 18:41:38 -0700 Subject: [PATCH 06/12] Update & document changes to ASPhotosFrameworkImageRequest handling of PHImageRequestOptionsDeliveryMode --- AsyncDisplayKit/ASMultiplexImageNode.mm | 4 +++- AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index c3028b0123..a3cd99558d 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -528,7 +528,9 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent // Get the best image we can. PHAsset *imageAsset = [assetFetchResult firstObject]; PHImageRequestOptions *options = [request.options copy]; - options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + if (options.deliveryMode == PHImageRequestOptionsDeliveryModeOpportunistic) { + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + } [[PHImageManager defaultManager] requestImageForAsset:imageAsset targetSize:request.targetSize diff --git a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h index ebf04fdfbc..4f55b0c401 100644 --- a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h @@ -48,8 +48,8 @@ extern NSString *const ASPhotosURLScheme; @discussion Some properties of this object are ignored when converting this request into a URL. As of iOS SDK 9.0, these properties are `progressHandler` and `synchronous`. - Note that PHImageRequestOptionsDeliveryModeOpportunistic is not recommended when using ASMultiplexImageNode, - because it sends multiple images and only the first will be accepted. + Note that ASMultiplexImageNode does not support PHImageRequestOptionsDeliveryModeOpportunistic + because it sends multiple images, and that mode will be replaced by PHImageRequestOptionsDeliveryModeHighQualityFormat */ @property (nonatomic, strong) PHImageRequestOptions *options; From f6fe3b07d831ab8681f98ae29f2e44f83151199a Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sat, 26 Sep 2015 19:17:43 -0700 Subject: [PATCH 07/12] Request and receive images in background queue --- AsyncDisplayKit/ASMultiplexImageNode.mm | 50 +++++++++++++------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index a3cd99558d..beb21e3a65 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -516,29 +516,33 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required"); ASDisplayNodeAssertNotNil(request, @"request is required"); ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required"); - - // Get the PHAsset itself. - PHFetchResult *assetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil]; - if ([assetFetchResult count] == 0) { - // Error. - completionBlock(nil, nil); - return; - } - - // Get the best image we can. - PHAsset *imageAsset = [assetFetchResult firstObject]; - PHImageRequestOptions *options = [request.options copy]; - if (options.deliveryMode == PHImageRequestOptionsDeliveryModeOpportunistic) { - options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; - } - - [[PHImageManager defaultManager] requestImageForAsset:imageAsset - targetSize:request.targetSize - contentMode:request.contentMode - options:options - resultHandler:^(UIImage *image, NSDictionary *info) { - completionBlock(image, info[PHImageErrorKey]); - }]; + + // This is sometimes called on main but there's no reason to stay there + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + // Get the PHAsset itself. + PHFetchResult *assetFetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil]; + if ([assetFetchResult count] == 0) { + // Error. + completionBlock(nil, nil); + return; + } + + PHAsset *imageAsset = [assetFetchResult firstObject]; + PHImageRequestOptions *options = [request.options copy]; + if (options.deliveryMode == PHImageRequestOptionsDeliveryModeHighQualityFormat) { + // Without this flag the result will be delivered on the main queue, which is pointless + // But synchronous -> HighQualityFormat so we only use it if high quality format is specified + options.synchronous = YES; + } + + [[PHImageManager defaultManager] requestImageForAsset:imageAsset + targetSize:request.targetSize + contentMode:request.contentMode + options:options + resultHandler:^(UIImage *image, NSDictionary *info) { + completionBlock(image, info[PHImageErrorKey]); + }]; + }); } - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock From 5f49e0f67d4960d50b651bb4e6b01640ecbab277 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sat, 26 Sep 2015 19:50:27 -0700 Subject: [PATCH 08/12] If Photos framework image is delivered on main queue, dispatch async to background queue --- AsyncDisplayKit/ASMultiplexImageNode.mm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index beb21e3a65..026c1680a6 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -540,7 +540,13 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) { - completionBlock(image, info[PHImageErrorKey]); + if (NSThread.isMainThread) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + completionBlock(image, info[PHImageErrorKey]); + }); + } else { + completionBlock(image, info[PHImageErrorKey]); + } }]; }); } From de1f9788f35b067ca70ab83784c5fa8c59b4bf3d Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 2 Oct 2015 12:44:21 -0700 Subject: [PATCH 09/12] Simplify creation of Photos Framework NSURLs in ASMultiplexImageNode --- AsyncDisplayKit/ASMultiplexImageNode.h | 35 ++++++++++++-- AsyncDisplayKit/ASMultiplexImageNode.mm | 46 +++++++++++++------ .../Details/ASPhotosFrameworkImageRequest.h | 11 ----- .../Details/ASPhotosFrameworkImageRequest.m | 9 ---- .../ASPhotosFrameworkImageRequestTests.m | 3 +- 5 files changed, 65 insertions(+), 39 deletions(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index 5b811f6654..2a3ec7f821 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -9,6 +9,7 @@ #import #import +#import @protocol ASMultiplexImageNodeDelegate; @protocol ASMultiplexImageNodeDataSource; @@ -98,6 +99,13 @@ typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { */ @property (nonatomic, readonly) id displayedImageIdentifier; +/** + * @abstract The image manager that this image node should use when requesting images from the Photos framework. Defaults to `PHImageManager.defaultManager`. + + * @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below. + */ +@property (nonatomic, strong) PHImageManager *imageManager; + @end @@ -195,11 +203,32 @@ didFinishDownloadingImageWithIdentifier:(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, URLs converted from ASPhotosFrameworkImageRequest, HTTP, HTTPS, and FTP URLs. If the - * image is already available to the data source, it should be provided via <[ASMultiplexImageNodeDataSource + * @discussion Supported URLs include HTTP, HTTPS, AssetsLibrary, and FTP URLs as well as Photos framework URLs (see note). + * + * 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. + * @return An NSURL for the image identified by `imageIdentifier`, or nil if none is available. + * @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below. */ - (NSURL *)multiplexImageNode:(ASMultiplexImageNode *)imageNode URLForImageIdentifier:(id)imageIdentifier; @end + +#pragma mark - + +@interface NSURL (ASPhotosFrameworkURLs) + +/** + * @abstract Create an NSURL that specifies an image from the Photos framework. + * + * @discussion When implementing `-multiplexImageNode:URLForImageIdentifier:`, you can return a URL + * created by this method and the image node will attempt to load the image from the Photos framework. + * @note The `synchronous` flag in `options` is ignored. + * @note The `Opportunistic` delivery mode is not supported and will be treated as `HighQualityFormat`. + */ ++ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier + targetSize:(CGSize)targetSize + contentMode:(PHImageContentMode)contentMode + options:(PHImageRequestOptions *)options; + +@end \ No newline at end of file diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 026c1680a6..aa65649a7f 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -157,7 +157,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent _cache = cache; _downloader = downloader; - + _imageManager = PHImageManager.defaultManager; + return self; } @@ -455,7 +456,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent }]; } // Likewise, if it's a iOS 8 Photo asset, we need to fetch it accordingly. - else if (ASPhotosFrameworkImageRequest *request = nextImageURL.asyncdisplaykit_photosRequest) { + else if (ASPhotosFrameworkImageRequest *request = [ASPhotosFrameworkImageRequest requestWithURL:nextImageURL]) { [self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) { ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from Photos Framework", weakSelf, nextImageIdentifier); finishedLoadingBlock(image, nextImageIdentifier, error); @@ -529,25 +530,27 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent PHAsset *imageAsset = [assetFetchResult firstObject]; PHImageRequestOptions *options = [request.options copy]; + + // We don't support opportunistic delivery – one request, one image. + if (options.deliveryMode == PHImageRequestOptionsDeliveryModeOpportunistic) { + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + } + if (options.deliveryMode == PHImageRequestOptionsDeliveryModeHighQualityFormat) { // Without this flag the result will be delivered on the main queue, which is pointless // But synchronous -> HighQualityFormat so we only use it if high quality format is specified options.synchronous = YES; } - [[PHImageManager defaultManager] requestImageForAsset:imageAsset - targetSize:request.targetSize - contentMode:request.contentMode - options:options - resultHandler:^(UIImage *image, NSDictionary *info) { - if (NSThread.isMainThread) { - dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ - completionBlock(image, info[PHImageErrorKey]); - }); - } else { - completionBlock(image, info[PHImageErrorKey]); - } - }]; + [self.imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) { + if (NSThread.isMainThread) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + completionBlock(image, info[PHImageErrorKey]); + }); + } else { + completionBlock(image, info[PHImageErrorKey]); + } + }]; }); } @@ -648,3 +651,16 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent } @end + +@implementation NSURL (ASPhotosFrameworkURLs) + ++ (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options +{ + ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:assetLocalIdentifier]; + request.options = options; + request.contentMode = contentMode; + request.targetSize = targetSize; + return request.url; +} + +@end \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h index 4f55b0c401..7630fe2e84 100644 --- a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.h @@ -48,8 +48,6 @@ extern NSString *const ASPhotosURLScheme; @discussion Some properties of this object are ignored when converting this request into a URL. As of iOS SDK 9.0, these properties are `progressHandler` and `synchronous`. - Note that ASMultiplexImageNode does not support PHImageRequestOptionsDeliveryModeOpportunistic - because it sends multiple images, and that mode will be replaced by PHImageRequestOptionsDeliveryModeHighQualityFormat */ @property (nonatomic, strong) PHImageRequestOptions *options; @@ -65,13 +63,4 @@ extern NSString *const ASPhotosURLScheme; @end -@interface NSURL (ASPhotosRequestConverting) - -/** - @abstract A convenience function that calls `[ASPhotosFrameworkImageRequest requestWithURL:self]`. - */ -- (/*nullable*/ ASPhotosFrameworkImageRequest *)asyncdisplaykit_photosRequest; - -@end - // NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m index dcf47957c6..0e4b4e66ac 100644 --- a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m @@ -158,12 +158,3 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; } @end - -@implementation NSURL (ASPhotosRequestConverting) - -- (ASPhotosFrameworkImageRequest *)asyncdisplaykit_photosRequest -{ - return [ASPhotosFrameworkImageRequest requestWithURL:self]; -} - -@end diff --git a/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m b/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m index 5c276706b4..0d34e1944e 100644 --- a/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m +++ b/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m @@ -46,7 +46,8 @@ static NSString *const kTestAssetID = @"testAssetID"; - (void)testThatParsingFromURLWorks { - XCTAssertEqualObjects([self.class urlForExampleImageRequest].asyncdisplaykit_photosRequest, [self.class exampleImageRequest]); + NSURL *url = [self.class urlForExampleImageRequest]; + XCTAssertEqualObjects([ASPhotosFrameworkImageRequest requestWithURL:url], [self.class exampleImageRequest]); } - (void)testThatCopyingWorks From ac8b224d9bdde160e109f0782ad66005b2655689 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 2 Oct 2015 13:04:48 -0700 Subject: [PATCH 10/12] Fix broken ASPhotosFrameworkImageRequest test --- AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m b/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m index 0d34e1944e..d6c641bd8a 100644 --- a/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m +++ b/AsyncDisplayKitTests/ASPhotosFrameworkImageRequestTests.m @@ -33,7 +33,7 @@ static NSString *const kTestAssetID = @"testAssetID"; + (NSURL *)urlForExampleImageRequest { - NSString *str = [NSString stringWithFormat:@"ph://%@?width=1024&height=1536&version=2&contentmode=1&network=1&resizemode=1&crop_x=0.2&crop_y=0.1&crop_w=0.6&crop_h=0.8", kTestAssetID]; + NSString *str = [NSString stringWithFormat:@"ph://%@?width=1024&height=1536&version=2&contentmode=1&network=1&resizemode=1&deliverymode=0&crop_x=0.2&crop_y=0.1&crop_w=0.6&crop_h=0.8", kTestAssetID]; return [NSURL URLWithString:str]; } From d5a6ad21235483381d80bbdeb151af808dd25e60 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 2 Oct 2015 15:41:01 -0700 Subject: [PATCH 11/12] Avoid accessing PHImageManager in ASMultiplexImageNode init --- AsyncDisplayKit/ASMultiplexImageNode.h | 2 +- AsyncDisplayKit/ASMultiplexImageNode.mm | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index 2a3ec7f821..1956760899 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -100,7 +100,7 @@ typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { @property (nonatomic, readonly) id displayedImageIdentifier; /** - * @abstract The image manager that this image node should use when requesting images from the Photos framework. Defaults to `PHImageManager.defaultManager`. + * @abstract The image manager that this image node should use when requesting images from the Photos framework. If this is `nil` (the default), then `PHImageManager.defaultManager` is used. * @see `+[NSURL URLWithAssetLocalIdentifier:targetSize:contentMode:options:]` below. */ diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 057cf279af..4ffb8a1275 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -157,7 +157,6 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent _cache = cache; _downloader = downloader; - _imageManager = PHImageManager.defaultManager; self.shouldBypassEnsureDisplay = YES; return self; @@ -543,7 +542,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent options.synchronous = YES; } - [self.imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) { + PHImageManager *imageManager = self.imageManager ?: PHImageManager.defaultManager; + [imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) { if (NSThread.isMainThread) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ completionBlock(image, info[PHImageErrorKey]); From 304ec9b543deaec9e3dc0fe99bc4fdceb509e097 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 2 Oct 2015 15:51:32 -0700 Subject: [PATCH 12/12] Refuse to parse ph:// URLs if iOS < 8 for safety --- AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m index 0e4b4e66ac..d46b3791c1 100644 --- a/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m +++ b/AsyncDisplayKit/Details/ASPhotosFrameworkImageRequest.m @@ -8,6 +8,7 @@ #import "ASPhotosFrameworkImageRequest.h" #import "ASBaseDefines.h" +#import "ASAvailability.h" NSString *const ASPhotosURLScheme = @"ph"; @@ -100,8 +101,8 @@ static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; + (ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url { - // not a photos URL - if (![url.scheme isEqualToString:ASPhotosURLScheme]) { + // not a photos URL or iOS < 8 + if (![url.scheme isEqualToString:ASPhotosURLScheme] || !AS_AT_LEAST_IOS8) { return nil; }