From ee0c027ba68e0299c2ccb803f84de2ed5bef0d9c Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 25 Sep 2015 15:25:37 -0700 Subject: [PATCH] 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