Add support for animated GIFs

Summary:
Use NSData as key

Add chunking of files

Add support for Animated Images

This adds support for GIF playback in ASDK. It's not currently ready
for merging to public ASDK – for one the GIF decompression may make
more sense in PINRemoteImage.

Removed duration as it wasn't used

Make cover image lazily loaded

Differential Revision: https://phabricator.pinadmin.com/D82033
This commit is contained in:
Garrett Moon
2016-03-29 21:00:24 -07:00
parent 5fc307f0bc
commit 3bcbfb149d
26 changed files with 2109 additions and 34 deletions

View File

@@ -0,0 +1,59 @@
//
// ASAnimatedImage.h
// Pods
//
// Created by Garrett Moon on 3/18/16.
//
//
#import <Foundation/Foundation.h>
#define ASAnimatedImageDebug 0
typedef NS_ENUM(NSUInteger, ASAnimatedImageError) {
ASAnimatedImageErrorNoError = 0,
ASAnimatedImageErrorFileCreationError,
ASAnimatedImageErrorFileHandleError,
ASAnimatedImageErrorImageFrameError,
ASAnimatedImageErrorMappingError,
};
typedef NS_ENUM(NSUInteger, ASAnimatedImageStatus) {
ASAnimatedImageStatusUnprocessed = 0,
ASAnimatedImageStatusInfoProcessed,
ASAnimatedImageStatusFirstFileProcessed,
ASAnimatedImageStatusProcessed,
ASAnimatedImageStatusCanceled,
ASAnimatedImageStatusError,
};
extern const Float32 kASAnimatedImageDefaultDuration;
//http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser
extern const Float32 kASAnimatedImageMinimumDuration;
extern const NSTimeInterval kASAnimatedImageDisplayRefreshRate;
typedef void(^ASAnimatedImageInfoReady)(UIImage *coverImage);
@interface ASAnimatedImage : NSObject
- (instancetype)initWithAnimatedImageData:(NSData *)animatedImageData NS_DESIGNATED_INITIALIZER;
@property (nonatomic, strong, readwrite) ASAnimatedImageInfoReady infoCompletion;
@property (nonatomic, strong, readwrite) dispatch_block_t fileReady;
@property (nonatomic, strong, readwrite) dispatch_block_t animatedImageReady;
@property (nonatomic, assign, readwrite) ASAnimatedImageStatus status;
//Access to any properties or methods below this line before status == ASAnimatedImageStatusInfoProcessed is undefined.
@property (nonatomic, readonly) UIImage *coverImage;
@property (nonatomic, readonly) Float32 *durations;
@property (nonatomic, readonly) CFTimeInterval totalDuration;
@property (nonatomic, readonly) size_t loopCount;
@property (nonatomic, readonly) size_t frameCount;
@property (nonatomic, readonly) size_t width;
@property (nonatomic, readonly) size_t height;
- (CGImageRef)imageAtIndex:(NSUInteger)index;
- (void)clearMemoryCache;
@end

View File

@@ -0,0 +1,864 @@
//
// ASAnimatedImage.m
// Pods
//
// Created by Garrett Moon on 3/18/16.
//
//
#import "ASAnimatedImage.h"
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import "ASThread.h"
#if ASAnimatedImageDebug
#define ASAnimatedLog(...) NSLog(__VA_ARGS__)
#else
#define ASAnimatedLog(...)
#endif
static NSString *kASAnimatedImageErrorDomain = @"kASAnimatedImageErrorDomain";
const Float32 kASAnimatedImageDefaultDuration = 0.1;
static const size_t bitsPerComponent = 8;
static const size_t componentsPerPixel = 4;
static const NSUInteger maxFileSize = 50000000; //max file size in bytes
static const Float32 maxFileDuration = 1; //max duration of a file in seconds
const NSTimeInterval kASAnimatedImageDisplayRefreshRate = 60.0;
const Float32 kASAnimatedImageMinimumDuration = 1 / kASAnimatedImageDisplayRefreshRate;
//TODO separate out classes
@class ASSharedAnimatedImage;
typedef void(^ASAnimatedImageDecodedPath)(BOOL finished, NSString *path, NSError *error);
typedef void(^ASAnimatedImageInfoProcessed)(UIImage *coverImage, Float32 *durations, CFTimeInterval totalDuration, size_t loopCount, size_t frameCount, size_t width, size_t height, UInt32 bitmapInfo);
typedef void(^ASAnimatedImageSharedReady)(UIImage *coverImage, ASSharedAnimatedImage *shared);
BOOL ASStatusCoverImageCompleted(ASAnimatedImageStatus status);
BOOL ASStatusCoverImageCompleted(ASAnimatedImageStatus status) {
return status == ASAnimatedImageStatusInfoProcessed || status == ASAnimatedImageStatusFirstFileProcessed || status == ASAnimatedImageStatusProcessed;
}
@interface ASSharedAnimatedImageFile : NSObject
{
ASDN::Mutex _lock;
}
@property (nonatomic, strong, readonly) NSString *path;
@property (nonatomic, assign, readonly) UInt32 frameCount;
@property (nonatomic, weak, readonly) NSData *memoryMappedData;
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
@end
@interface ASSharedAnimatedImage : NSObject
{
ASDN::Mutex _coverImageLock;
}
//This is intentionally atomic. ASAnimatedImageManager must be able to add entries
//and clients must be able to read them concurrently.
@property (atomic, strong, readwrite) NSArray <ASSharedAnimatedImageFile *> *maps;
@property (nonatomic, strong, readwrite) NSArray <ASAnimatedImageDecodedPath> *completions;
@property (nonatomic, strong, readwrite) NSArray <ASAnimatedImageSharedReady> *infoCompletions;
@property (nonatomic, weak, readwrite) UIImage *coverImage;
@property (nonatomic, strong, readwrite) NSError *error;
//TODO is status thread safe?
@property (nonatomic, assign, readwrite) ASAnimatedImageStatus status;
- (void)setInfoProcessedWithCoverImage:(UIImage *)coverImage durations:(Float32 *)durations totalDuration:(CFTimeInterval)totalDuration loopCount:(size_t)loopCount frameCount:(size_t)frameCount width:(size_t)width height:(size_t)height bitmapInfo:(CGBitmapInfo)bitmapInfo;
@property (nonatomic, readonly) Float32 *durations;
@property (nonatomic, readonly) CFTimeInterval totalDuration;
@property (nonatomic, readonly) size_t loopCount;
@property (nonatomic, readonly) size_t frameCount;
@property (nonatomic, readonly) size_t width;
@property (nonatomic, readonly) size_t height;
@property (nonatomic, readonly) CGBitmapInfo bitmapInfo;
@end
@interface ASAnimatedImageManager : NSObject
{
ASDN::Mutex _lock;
}
+ (instancetype)sharedManager;
@property (nonatomic, strong, readonly) NSString *temporaryDirectory;
@property (nonatomic, strong, readonly) NSMutableDictionary <NSData *, ASSharedAnimatedImage *> *animatedImages;
@property (nonatomic, strong, readonly) dispatch_queue_t serialProcessingQueue;
@end
@interface ASAnimatedImage ()
{
ASDN::Mutex _statusLock;
ASDN::Mutex _completionLock;
ASDN::Mutex _dataLock;
NSData *_currentData;
NSData *_nextData;
}
@property (nonatomic, strong, readonly) ASSharedAnimatedImage *sharedAnimatedImage;
+ (UIImage *)coverImageWithMemoryMap:(NSData *)memoryMap width:(UInt32)width height:(UInt32)height;
@end
@implementation ASAnimatedImageManager
+ (instancetype)sharedManager
{
static dispatch_once_t onceToken;
static ASAnimatedImageManager *sharedManager;
dispatch_once(&onceToken, ^{
sharedManager = [[ASAnimatedImageManager alloc] init];
});
return sharedManager;
}
- (instancetype)init
{
if (self = [super init]) {
//On iOS temp directories are not shared between apps. This may not be safe on OS X or other systems
_temporaryDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ASAnimatedImageCache"];
[self cleanupFiles];
if ([[NSFileManager defaultManager] fileExistsAtPath:_temporaryDirectory] == NO) {
[[NSFileManager defaultManager] createDirectoryAtPath:_temporaryDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
_animatedImages = [[NSMutableDictionary alloc] init];
_serialProcessingQueue = dispatch_queue_create("Serial animated image processing queue.", DISPATCH_QUEUE_SERIAL);
__weak ASAnimatedImageManager *weakSelf = self;
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification
object:nil
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
[weakSelf cleanupFiles];
}];
}
return self;
}
- (void)cleanupFiles
{
[[NSFileManager defaultManager] removeItemAtPath:self.temporaryDirectory error:nil];
}
- (void)animatedPathForImageData:(NSData *)animatedImageData infoCompletion:(ASAnimatedImageSharedReady)infoCompletion completion:(ASAnimatedImageDecodedPath)completion
{
BOOL startProcessing = NO;
{
ASDN::MutexLocker l(_lock);
ASSharedAnimatedImage *shared = self.animatedImages[animatedImageData];
if (shared == nil) {
shared = [[ASSharedAnimatedImage alloc] init];
self.animatedImages[animatedImageData] = shared;
startProcessing = YES;
}
if (shared.status == ASAnimatedImageStatusProcessed) {
if (completion) {
completion(YES, nil, nil);
}
} else if (shared.error) {
if (completion) {
completion(NO, nil, shared.error);
}
} else {
if (completion) {
shared.completions = [shared.completions arrayByAddingObject:completion];
}
}
if (ASStatusCoverImageCompleted(shared.status)) {
if (infoCompletion) {
infoCompletion(shared.coverImage, shared);
}
} else {
if (infoCompletion) {
shared.infoCompletions = [shared.infoCompletions arrayByAddingObject:infoCompletion];
}
}
}
if (startProcessing) {
dispatch_async(self.serialProcessingQueue, ^{
[[self class] processAnimatedImage:animatedImageData temporaryDirectory:self.temporaryDirectory infoCompletion:^(UIImage *coverImage, Float32 *durations, CFTimeInterval totalDuration, size_t loopCount, size_t frameCount, size_t width, size_t height, UInt32 bitmapInfo) {
NSArray *infoCompletions = nil;
ASSharedAnimatedImage *shared = nil;
{
ASDN::MutexLocker l(_lock);
shared = self.animatedImages[animatedImageData];
[shared setInfoProcessedWithCoverImage:coverImage durations:durations totalDuration:totalDuration loopCount:loopCount frameCount:frameCount width:width height:height bitmapInfo:bitmapInfo];
infoCompletions = shared.infoCompletions;
shared.infoCompletions = @[];
}
for (ASAnimatedImageSharedReady infoCompletion in infoCompletions) {
infoCompletion(coverImage, shared);
}
} decodedPath:^(BOOL finished, NSString *path, NSError *error) {
NSArray *completions = nil;
NSData *memoryMappedData = nil;
{
ASDN::MutexLocker l(_lock);
ASSharedAnimatedImage *shared = self.animatedImages[animatedImageData];
if (path && error == nil) {
shared.maps = [shared.maps arrayByAddingObject:[[ASSharedAnimatedImageFile alloc] initWithPath:path]];
}
shared.error = error;
completions = shared.completions;
if (finished || error) {
shared.completions = @[];
}
if (finished) {
shared.status = ASAnimatedImageStatusProcessed;
} else {
shared.status = ASAnimatedImageStatusFirstFileProcessed;
}
}
for (ASAnimatedImageDecodedPath completion in completions) {
completion(finished, path, error);
}
}];
});
}
}
+ (void)processAnimatedImage:(NSData *)animatedImageData
temporaryDirectory:(NSString *)temporaryDirectory
infoCompletion:(ASAnimatedImageInfoProcessed)infoCompletion
decodedPath:(ASAnimatedImageDecodedPath)completion
{
NSUUID *UUID = [NSUUID UUID];
NSError *error = nil;
NSString *filePath = nil;
//TODO Must handle file handle errors! Documentation says it throws exceptions on any errors :(
NSFileHandle *fileHandle = [self fileHandle:&error filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:0];
UInt32 width;
UInt32 height;
UInt32 bitmapInfo;
NSUInteger fileCount = 0;
UInt32 frameCountForFile = 0;
#if ASAnimatedImageDebug
CFTimeInterval start = CACurrentMediaTime();
#endif
if (fileHandle && error == nil) {
dispatch_queue_t diskWriteQueue = dispatch_queue_create("ASAnimatedImage disk write queue", DISPATCH_QUEUE_SERIAL);
dispatch_group_t diskGroup = dispatch_group_create();
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)animatedImageData,
(CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : (__bridge NSString *)kUTTypeGIF,
(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
if (imageSource) {
UInt32 frameCount = (UInt32)CGImageSourceGetCount(imageSource);
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, nil);
UInt32 loopCount = (UInt32)[[[imageProperties objectForKey:(__bridge NSString *)kCGImagePropertyGIFDictionary]
objectForKey:(__bridge NSString *)kCGImagePropertyGIFLoopCount] unsignedLongValue];
Float32 fileDuration = 0;
NSUInteger fileSize = 0;
Float32 durations[frameCount];
CFTimeInterval totalDuration = 0;
UIImage *coverImage = nil;
//Gather header file info
for (NSUInteger frameIdx = 0; frameIdx < frameCount; frameIdx++) {
if (frameIdx == 0) {
CGImageRef frameImage = CGImageSourceCreateImageAtIndex(imageSource, frameIdx, (CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
if (frameImage == nil) {
error = [NSError errorWithDomain:kASAnimatedImageErrorDomain code:ASAnimatedImageErrorImageFrameError userInfo:nil];
break;
}
bitmapInfo = CGImageGetBitmapInfo(frameImage);
width = (UInt32)CGImageGetWidth(frameImage);
height = (UInt32)CGImageGetHeight(frameImage);
coverImage = [UIImage imageWithCGImage:frameImage];
CGImageRelease(frameImage);
}
Float32 duration = [[self class] frameDurationAtIndex:frameIdx source:imageSource];
durations[frameIdx] = duration;
totalDuration += duration;
}
if (error == nil) {
//Get size, write file header get coverImage
//blockDurations will be freed below after calling infoCompletion
Float32 *blockDurations = (Float32 *)malloc(sizeof(Float32) * frameCount);
memcpy(blockDurations, durations, sizeof(Float32) * frameCount);
dispatch_group_async(diskGroup, diskWriteQueue, ^{
[self writeFileHeader:fileHandle width:width height:height loopCount:loopCount frameCount:frameCount bitmapInfo:bitmapInfo durations:blockDurations];
[fileHandle closeFile];
});
fileCount = 1;
fileHandle = [self fileHandle:&error filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:fileCount];
dispatch_group_async(diskGroup, diskWriteQueue, ^{
ASAnimatedLog(@"notifying info");
infoCompletion(coverImage, blockDurations, totalDuration, loopCount, frameCount, width, height, bitmapInfo);
free(blockDurations);
//write empty frame count
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
});
//Process frames
for (NSUInteger frameIdx = 0; frameIdx < frameCount; frameIdx++) {
@autoreleasepool {
if (fileDuration > maxFileDuration || fileSize > maxFileSize) {
//create a new file
dispatch_group_async(diskGroup, diskWriteQueue, ^{
//prepend file with frameCount
[fileHandle seekToFileOffset:0];
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
[fileHandle closeFile];
});
dispatch_group_async(diskGroup, diskWriteQueue, ^{
ASAnimatedLog(@"notifying file: %@", filePath);
completion(NO, filePath, error);
});
diskGroup = dispatch_group_create();
fileCount++;
fileHandle = [self fileHandle:&error filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:fileCount];
frameCountForFile = 0;
fileDuration = 0;
fileSize = 0;
//write empty frame count
dispatch_group_async(diskGroup, diskWriteQueue, ^{
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
});
}
CGImageRef frameImage = CGImageSourceCreateImageAtIndex(imageSource, frameIdx, (CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
if (frameImage == nil) {
error = [NSError errorWithDomain:kASAnimatedImageErrorDomain code:ASAnimatedImageErrorImageFrameError userInfo:nil];
break;
}
Float32 duration = durations[frameIdx];
fileDuration += duration;
NSData *frameData = (__bridge_transfer NSData *)CGDataProviderCopyData(CGImageGetDataProvider(frameImage));
NSAssert(frameData.length == width * height * componentsPerPixel, @"data should be width * height * 4 bytes");
dispatch_group_async(diskGroup, diskWriteQueue, ^{
[self writeFrameToFile:fileHandle duration:duration frameData:frameData];
});
CGImageRelease(frameImage);
frameCountForFile++;
}
}
}
CFRelease(imageSource);
}
dispatch_group_wait(diskGroup, DISPATCH_TIME_FOREVER);
//close the file handle
ASAnimatedLog(@"closing last file: %@", fileHandle);
[fileHandle seekToFileOffset:0];
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
[fileHandle closeFile];
}
#if ASAnimatedImageDebug
CFTimeInterval interval = CACurrentMediaTime() - start;
NSLog(@"Encoding and write time: %f", interval);
#endif
completion(YES, filePath, error);
}
//http://stackoverflow.com/questions/16964366/delaytime-or-unclampeddelaytime-for-gifs
+ (Float32)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source
{
Float32 frameDuration = kASAnimatedImageDefaultDuration;
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, nil);
// use unclamped delay time before delay time before default
NSNumber *unclamedDelayTime = frameProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFUnclampedDelayTime];
if (unclamedDelayTime) {
frameDuration = [unclamedDelayTime floatValue];
} else {
NSNumber *delayTime = frameProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFDelayTime];
if (delayTime) {
frameDuration = [delayTime floatValue];
}
}
if (frameDuration < kASAnimatedImageMinimumDuration) {
frameDuration = kASAnimatedImageDefaultDuration;
}
return frameDuration;
}
+ (NSString *)filePathWithTemporaryDirectory:(NSString *)temporaryDirectory UUID:(NSUUID *)UUID count:(NSUInteger)count
{
NSString *filePath = [temporaryDirectory stringByAppendingPathComponent:[UUID UUIDString]];
if (count > 0) {
filePath = [filePath stringByAppendingString:[@(count) stringValue]];
}
return filePath;
}
+ (NSFileHandle *)fileHandle:(NSError **)error filePath:(NSString **)filePath temporaryDirectory:(NSString *)temporaryDirectory UUID:(NSUUID *)UUID count:(NSUInteger)count;
{
NSString *dirPath = temporaryDirectory;
NSString *outFilePath = [self filePathWithTemporaryDirectory:temporaryDirectory UUID:UUID count:count];
NSError *outError = nil;
NSFileHandle *fileHandle = nil;
if (outError == nil) {
BOOL success = [[NSFileManager defaultManager] createFileAtPath:outFilePath contents:nil attributes:nil];
if (success == NO) {
outError = [NSError errorWithDomain:kASAnimatedImageErrorDomain code:ASAnimatedImageErrorFileCreationError userInfo:nil];
}
}
if (outError == nil) {
fileHandle = [NSFileHandle fileHandleForWritingAtPath:outFilePath];
if (fileHandle == nil) {
outError = [NSError errorWithDomain:kASAnimatedImageErrorDomain code:ASAnimatedImageErrorFileHandleError userInfo:nil];
}
}
if (error) {
*error = outError;
}
if (filePath) {
*filePath = outFilePath;
}
return fileHandle;
}
/**
ASAnimatedImage file header
Header:
[version] 2 bytes
[width] 4 bytes
[height] 4 bytes
[loop count] 4 bytes
[frame count] 4 bytes
[bitmap info] 4 bytes
[durations] 4 bytes * frame count
*/
+ (void)writeFileHeader:(NSFileHandle *)fileHandle width:(UInt32)width height:(UInt32)height loopCount:(UInt32)loopCount frameCount:(UInt32)frameCount bitmapInfo:(UInt32)bitmapInfo durations:(Float32*)durations
{
UInt16 version = 1;
[fileHandle writeData:[NSData dataWithBytes:&version length:sizeof(version)]];
[fileHandle writeData:[NSData dataWithBytes:&width length:sizeof(width)]];
[fileHandle writeData:[NSData dataWithBytes:&height length:sizeof(height)]];
[fileHandle writeData:[NSData dataWithBytes:&loopCount length:sizeof(loopCount)]];
[fileHandle writeData:[NSData dataWithBytes:&frameCount length:sizeof(frameCount)]];
[fileHandle writeData:[NSData dataWithBytes:&bitmapInfo length:sizeof(bitmapInfo)]];
[fileHandle writeData:[NSData dataWithBytes:durations length:sizeof(Float32) * frameCount]];
}
/**
ASAnimatedImage frame file
[frame count(in file)] 4 bytes
[frame(s)]
Each frame:
[duration] 4 bytes
[frame data] width * height * 4 bytes
*/
+ (void)writeFrameToFile:(NSFileHandle *)fileHandle duration:(Float32)duration frameData:(NSData *)frameData
{
[fileHandle writeData:[NSData dataWithBytes:&duration length:sizeof(duration)]];
[fileHandle writeData:frameData];
}
@end
@implementation ASAnimatedImage
- (instancetype)init
{
return [self initWithAnimatedImageData:nil];
}
- (instancetype)initWithAnimatedImageData:(NSData *)animatedImageData
{
if (self = [super init]) {
ASDisplayNodeAssertNotNil(animatedImageData, @"animatedImageData must not be nil.");
_status = ASAnimatedImageStatusUnprocessed;
[[ASAnimatedImageManager sharedManager] animatedPathForImageData:animatedImageData infoCompletion:^(UIImage *coverImage, ASSharedAnimatedImage *shared) {
{
ASDN::MutexLocker l(_statusLock);
_sharedAnimatedImage = shared;
if (_status == ASAnimatedImageStatusUnprocessed) {
_status = ASAnimatedImageStatusInfoProcessed;
}
}
{
ASDN::MutexLocker l(_completionLock);
if (_infoCompletion) {
_infoCompletion(coverImage);
}
}
} completion:^(BOOL completed, NSString *path, NSError *error) {
BOOL success = NO;
{
ASDN::MutexLocker l(_statusLock);
if (_status == ASAnimatedImageStatusInfoProcessed) {
_status = ASAnimatedImageStatusFirstFileProcessed;
}
if (completed && error == nil) {
_status = ASAnimatedImageStatusProcessed;
success = YES;
} else if (error) {
_status = ASAnimatedImageStatusError;
#if ASAnimatedImageDebug
NSLog(@"animated image error: %@", error);
#endif
}
}
{
ASDN::MutexLocker l(_completionLock);
if (_fileReady) {
_fileReady();
}
}
if (success) {
ASDN::MutexLocker l(_completionLock);
if (_animatedImageReady) {
_animatedImageReady();
}
}
}];
}
return self;
}
- (void)setInfoCompletion:(ASAnimatedImageInfoReady)infoCompletion
{
ASDN::MutexLocker l(_completionLock);
_infoCompletion = infoCompletion;
}
- (void)setAnimatedImageReady:(dispatch_block_t)animatedImageReady
{
ASDN::MutexLocker l(_completionLock);
_animatedImageReady = animatedImageReady;
}
- (void)setFileReady:(dispatch_block_t)fileReady
{
ASDN::MutexLocker l(_completionLock);
_fileReady = fileReady;
}
- (UIImage *)coverImageWithMemoryMap:(NSData *)memoryMap width:(UInt32)width height:(UInt32)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
return [UIImage imageWithCGImage:[[self class] imageAtIndex:0 inMemoryMap:memoryMap width:width height:height bitmapInfo:bitmapInfo]];
}
void releaseData(void *data, const void *imageData, size_t size);
void releaseData(void *data, const void *imageData, size_t size)
{
CFRelease(data);
}
- (CGImageRef)imageAtIndex:(NSUInteger)index inSharedImageFiles:(NSArray <ASSharedAnimatedImageFile *>*)imageFiles width:(UInt32)width height:(UInt32)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
for (NSUInteger fileIdx = 0; fileIdx < imageFiles.count; fileIdx++) {
ASSharedAnimatedImageFile *imageFile = imageFiles[fileIdx];
if (index < imageFile.frameCount) {
NSData *memoryMappedData = nil;
{
ASDN::MutexLocker l(_dataLock);
memoryMappedData = imageFile.memoryMappedData;
_currentData = memoryMappedData;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ASDN::MutexLocker l(_dataLock);
_nextData = (fileIdx + 1 < imageFiles.count) ? imageFiles[fileIdx + 1].memoryMappedData : imageFiles[0].memoryMappedData;
});
}
return [[self class] imageAtIndex:index inMemoryMap:memoryMappedData width:width height:height bitmapInfo:bitmapInfo];
} else {
index -= imageFile.frameCount;
}
}
//image file not done yet :(
return nil;
}
+ (CGImageRef)imageAtIndex:(NSUInteger)index inMemoryMap:(NSData *)memoryMap width:(UInt32)width height:(UInt32)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
Float32 outDuration;
size_t imageLength = width * height * componentsPerPixel;
//frame duration + previous images
NSUInteger offset = sizeof(UInt32) + (index * (imageLength + sizeof(outDuration)));
[memoryMap getBytes:&outDuration range:NSMakeRange(offset, sizeof(outDuration))];
BytePtr imageData = (BytePtr)[memoryMap bytes];
imageData += offset + sizeof(outDuration);
ASDisplayNodeAssert(offset + sizeof(outDuration) + imageLength <= memoryMap.length, @"Requesting frame beyond data bounds");
//retain the memory map, it will be released when releaseData is called
CFRetain((CFDataRef)memoryMap);
CGDataProviderRef dataProvider = CGDataProviderCreateWithData((void *)memoryMap, imageData, width * height * componentsPerPixel, releaseData);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGImageRef imageRef = CGImageCreate(width,
height,
bitsPerComponent,
bitsPerComponent * componentsPerPixel,
componentsPerPixel * width,
colorSpace,
bitmapInfo,
dataProvider,
NULL,
NO,
kCGRenderingIntentDefault);
CFAutorelease(imageRef);
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(dataProvider);
return imageRef;
}
+ (UInt32)widthFromMemoryMap:(NSData *)memoryMap
{
UInt32 width;
[memoryMap getBytes:&width range:NSMakeRange(2, sizeof(width))];
return width;
}
+ (UInt32)heightFromMemoryMap:(NSData *)memoryMap
{
UInt32 height;
[memoryMap getBytes:&height range:NSMakeRange(6, sizeof(height))];
return height;
}
+ (UInt32)loopCountFromMemoryMap:(NSData *)memoryMap
{
UInt32 loopCount;
[memoryMap getBytes:&loopCount range:NSMakeRange(10, sizeof(loopCount))];
return loopCount;
}
+ (UInt32)frameCountFromMemoryMap:(NSData *)memoryMap
{
UInt32 frameCount;
[memoryMap getBytes:&frameCount range:NSMakeRange(14, sizeof(frameCount))];
return frameCount;
}
+ (Float32 *)durationsFromMemoryMap:(NSData *)memoryMap frameCount:(UInt32)frameCount frameSize:(NSUInteger)frameSize totalDuration:(CFTimeInterval *)totalDuration
{
*totalDuration = 0;
Float32 durations[frameCount];
[memoryMap getBytes:&durations range:NSMakeRange(18, sizeof(Float32) * frameCount)];
for (NSUInteger idx = 0; idx < frameCount; idx++) {
*totalDuration += durations[idx];
}
return durations;
}
- (Float32 *)durations
{
return self.sharedAnimatedImage.durations;
}
- (CFTimeInterval)totalDuration
{
return self.sharedAnimatedImage.totalDuration;
}
- (size_t)loopCount
{
return self.sharedAnimatedImage.loopCount;
}
- (size_t)frameCount
{
return self.sharedAnimatedImage.frameCount;
}
- (size_t)width
{
return self.sharedAnimatedImage.width;
}
- (size_t)height
{
return self.sharedAnimatedImage.height;
}
- (ASAnimatedImageStatus)status
{
ASDN::MutexLocker l(_statusLock);
return _status;
}
- (CGImageRef)imageAtIndex:(NSUInteger)index
{
return [self imageAtIndex:index
inSharedImageFiles:self.sharedAnimatedImage.maps
width:self.sharedAnimatedImage.width
height:self.sharedAnimatedImage.height
bitmapInfo:self.sharedAnimatedImage.bitmapInfo];
}
- (UIImage *)coverImage
{
return self.sharedAnimatedImage.coverImage;
}
- (void)clearMemoryCache
{
ASDN::MutexLocker l(_dataLock);
_currentData = nil;
_nextData = nil;
}
@end
@implementation ASSharedAnimatedImage
- (instancetype)init
{
if (self = [super init]) {
_completions = @[];
_infoCompletions = @[];
_maps = @[];
}
return self;
}
- (void)setInfoProcessedWithCoverImage:(UIImage *)coverImage durations:(Float32 *)durations totalDuration:(CFTimeInterval)totalDuration loopCount:(size_t)loopCount frameCount:(size_t)frameCount width:(size_t)width height:(size_t)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
ASDisplayNodeAssert(_status == ASAnimatedImageStatusUnprocessed, @"Status should be unprocessed.");
{
ASDN::MutexLocker l(_coverImageLock);
_coverImage = coverImage;
}
_durations = (Float32 *)malloc(sizeof(Float32) * frameCount);
memcpy(_durations, durations, sizeof(Float32) * frameCount);
_totalDuration = totalDuration;
_loopCount = loopCount;
_frameCount = frameCount;
_width = width;
_height = height;
_bitmapInfo = bitmapInfo;
_status = ASAnimatedImageStatusInfoProcessed;
}
- (void)dealloc
{
free(_durations);
}
- (UIImage *)coverImage
{
ASDN::MutexLocker l(_coverImageLock);
UIImage *coverImage = nil;
if (_coverImage == nil) {
coverImage = [UIImage imageWithCGImage:[ASAnimatedImage imageAtIndex:0 inMemoryMap:self.maps[0].memoryMappedData width:self.width height:self.height bitmapInfo:self.bitmapInfo]];
_coverImage = coverImage;
} else {
coverImage = _coverImage;
}
return coverImage;
}
@end
@implementation ASSharedAnimatedImageFile
@synthesize memoryMappedData = _memoryMappedData;
@synthesize frameCount = _frameCount;
- (instancetype)initWithPath:(NSString *)path
{
if (self = [super init]) {
_path = path;
}
return self;
}
- (UInt32)frameCount
{
ASDN::MutexLocker l(_lock);
if (_frameCount == 0) {
NSData *memoryMappedData = _memoryMappedData;
if (memoryMappedData == nil) {
memoryMappedData = [self loadMemoryMappedData];
}
[memoryMappedData getBytes:&_frameCount range:NSMakeRange(0, sizeof(_frameCount))];
}
return _frameCount;
}
- (NSData *)memoryMappedData
{
ASDN::MutexLocker l(_lock);
if (_memoryMappedData == nil) {
return [self loadMemoryMappedData];
}
return _memoryMappedData;
}
//must be called within lock
- (NSData *)loadMemoryMappedData
{
NSError *error = nil;
_memoryMappedData = [NSData dataWithContentsOfFile:self.path options:NSDataReadingMappedAlways error:&error];
if (error) {
#if ASAnimatedImageDebug
NSLog(@"Could not memory map data: %@", error);
#endif
}
return _memoryMappedData;
}
@end

View File

@@ -0,0 +1,36 @@
//
// ASImageNode+AnimatedImage.h
// Pods
//
// Created by Garrett Moon on 3/22/16.
//
//
#import "ASImageNode.h"
#import "ASThread.h"
@interface ASImageNode ()
{
ASDN::RecursiveMutex _animatedImageLock;
ASDN::Mutex _displayLinkLock;
ASAnimatedImage *_animatedImage;
CADisplayLink *_displayLink;
//accessed on main thread only
CFTimeInterval _playHead;
NSUInteger _playedLoops;
}
@property (atomic, assign) BOOL animatedImagePaused;
@property (atomic, assign) CFTimeInterval lastDisplayLinkFire;
@end
@interface ASImageNode (AnimatedImage)
@property (nullable, atomic, strong) ASAnimatedImage *animatedImage;
- (void)coverImageCompleted:(UIImage *)coverImage;
@end

View File

@@ -0,0 +1,270 @@
//
// ASImageNode+AnimatedImage.m
// Pods
//
// Created by Garrett Moon on 3/22/16.
//
//
#import "ASImageNode+AnimatedImage.h"
#import "ASAssert.h"
#import "ASAnimatedImage.h"
#import "ASDisplayNode+Subclasses.h"
#import "ASDisplayNodeExtras.h"
#import "ASEqualityHelpers.h"
#import "ASDisplayNode+FrameworkPrivate.h"
@interface ASWeakProxy : NSObject
@property (nonatomic, weak, readonly) id target;
+ (instancetype)weakProxyWithTarget:(id)target;
@end
@implementation ASImageNode (AnimatedImage)
#pragma mark - GIF support
- (void)setAnimatedImage:(ASAnimatedImage *)animatedImage
{
ASDN::MutexLocker l(_animatedImageLock);
if (!ASObjectIsEqual(_animatedImage, animatedImage)) {
_animatedImage = animatedImage;
}
if (animatedImage != nil) {
animatedImage.infoCompletion = ^(UIImage *coverImage) {
[self coverImageCompleted:coverImage];
};
animatedImage.fileReady = ^{
[self animatedImageFileReady];
};
}
}
- (ASAnimatedImage *)animatedImage
{
ASDN::MutexLocker l(_animatedImageLock);
return _animatedImage;
}
- (void)coverImageCompleted:(UIImage *)coverImage
{
BOOL setCoverImage = YES;
{
ASDN::MutexLocker l(_displayLinkLock);
if (_displayLink != nil && _displayLink.paused == NO) {
setCoverImage = NO;
}
}
if (setCoverImage) {
self.image = coverImage;
}
}
- (void)animatedImageFileReady
{
dispatch_async(dispatch_get_main_queue(), ^{
[self startAnimating];
});
}
- (void)startAnimating
{
ASDisplayNodeAssertMainThread();
if (ASInterfaceStateIncludesVisible(self.interfaceState) == NO) {
return;
}
if (self.animatedImagePaused == YES) {
return;
}
if (self.animatedImage.status != ASAnimatedImageStatusProcessed && self.animatedImage.status != ASAnimatedImageStatusFirstFileProcessed) {
return;
}
#if ASAnimatedImageDebug
NSLog(@"starting animation: %p", self);
#endif
ASDN::MutexLocker l(_displayLinkLock);
if (_displayLink == nil) {
_playHead = 0;
_displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)];
//Credit to FLAnimatedImage (https://github.com/Flipboard/FLAnimatedImage) for display link interval calculations
_displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kASAnimatedImageDisplayRefreshRate, 1);
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
} else {
_displayLink.paused = NO;
}
}
- (void)stopAnimating
{
#if ASAnimatedImageDebug
NSLog(@"stopping animation: %p", self);
#endif
ASDisplayNodeAssertMainThread();
ASDN::MutexLocker l(_displayLinkLock);
_displayLink.paused = YES;
self.lastDisplayLinkFire = 0;
[self.animatedImage clearMemoryCache];
}
- (void)visibilityDidChange:(BOOL)isVisible
{
[super visibilityDidChange:isVisible];
ASDisplayNodeAssertMainThread();
if (isVisible) {
if (self.animatedImage.status == ASAnimatedImageStatusInfoProcessed || self.animatedImage.status == ASAnimatedImageStatusFirstFileProcessed || self.animatedImage.status == ASAnimatedImageStatusProcessed) {
self.image = self.animatedImage.coverImage;
}
[self startAnimating];
} else {
[self stopAnimating];
}
}
- (void)__enterHierarchy
{
[super __enterHierarchy];
[self startAnimating];
}
- (void)__exitHierarchy
{
[super __exitHierarchy];
[self stopAnimating];
}
//Credit to FLAnimatedImage (https://github.com/Flipboard/FLAnimatedImage) for display link interval calculations
- (NSTimeInterval)frameDelayGreatestCommonDivisor
{
const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kASAnimatedImageMinimumDuration;
// Scales the frame delays by `kGreatestCommonDivisorPrecision`
// then converts it to an UInteger for in order to calculate the GCD.
NSUInteger scaledGCD = lrint(self.animatedImage.durations[0] * kGreatestCommonDivisorPrecision);
for (NSUInteger durationIdx = 0; durationIdx < self.animatedImage.frameCount; durationIdx++) {
Float32 duration = self.animatedImage.durations[durationIdx];
scaledGCD = gcd(lrint(duration * kGreatestCommonDivisorPrecision), scaledGCD);
}
// Reverse to scale to get the value back into seconds.
return scaledGCD / kGreatestCommonDivisorPrecision;
}
//Credit to FLAnimatedImage (https://github.com/Flipboard/FLAnimatedImage) for display link interval calculations
static NSUInteger gcd(NSUInteger a, NSUInteger b)
{
// http://en.wikipedia.org/wiki/Greatest_common_divisor
if (a < b) {
return gcd(b, a);
} else if (a == b) {
return b;
}
while (true) {
NSUInteger remainder = a % b;
if (remainder == 0) {
return b;
}
a = b;
b = remainder;
}
}
- (void)displayLinkFired:(CADisplayLink *)displayLink
{
ASDisplayNodeAssertMainThread();
CFTimeInterval timeBetweenLastFire;
if (self.lastDisplayLinkFire == 0) {
timeBetweenLastFire = 0;
} else {
timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire;
}
self.lastDisplayLinkFire = CACurrentMediaTime();
_playHead += timeBetweenLastFire;
while (_playHead > self.animatedImage.totalDuration) {
_playHead -= self.animatedImage.totalDuration;
_playedLoops++;
}
if (self.animatedImage.loopCount > 0 && _playedLoops >= self.animatedImage.loopCount) {
[self stopAnimating];
return;
}
NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead];
CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex];
if (frameImage == nil) {
_playHead -= timeBetweenLastFire;
//Pause the display link until we get a file ready notification
displayLink.paused = YES;
self.lastDisplayLinkFire = 0;
} else {
self.contents = (__bridge id)frameImage;
}
}
- (NSUInteger)frameIndexAtPlayHeadPosition:(CFTimeInterval)playHead
{
ASDisplayNodeAssertMainThread();
NSUInteger frameIndex = 0;
for (NSUInteger durationIndex = 0; durationIndex < self.animatedImage.frameCount; durationIndex++) {
playHead -= self.animatedImage.durations[durationIndex];
if (playHead < 0) {
return frameIndex;
}
frameIndex++;
}
return frameIndex;
}
- (void)dealloc
{
ASDN::MutexLocker l(_displayLinkLock);
#if ASAnimatedImageDebug
if (_displayLink) {
NSLog(@"invalidating display link");
}
#endif
[_displayLink invalidate];
_displayLink = nil;
}
@end
@implementation ASWeakProxy
- (instancetype)initWithTarget:(id)target
{
if (self = [super init]) {
_target = target;
}
return self;
}
+ (instancetype)weakProxyWithTarget:(id)target
{
return [[ASWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return _target;
}
@end

View File

@@ -8,6 +8,8 @@
#import <AsyncDisplayKit/ASControlNode.h>
@class ASAnimatedImage;
NS_ASSUME_NONNULL_BEGIN
/**

View File

@@ -10,11 +10,13 @@
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASAnimatedImage.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASImageNode+AnimatedImage.h>
#import <AsyncDisplayKit/ASTextNode.h>
#import "ASImageNode+CGExtras.h"

View File

@@ -738,8 +738,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
if (_cache) {
if (_cacheSupportsNewProtocol) {
[_cache cachedImageWithURL:imageURL callbackQueue:dispatch_get_main_queue() completion:^(UIImage *imageFromCache) {
completionBlock(imageFromCache);
[_cache cachedImageWithURL:imageURL callbackQueue:dispatch_get_main_queue() completion:^(id <ASImageContainerProtocol> imageContainer) {
completionBlock([imageContainer asdk_image]);
}];
} else {
#pragma clang diagnostic push
@@ -784,7 +784,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
[self _setDownloadIdentifier:[_downloader downloadImageWithURL:imageURL
callbackQueue:dispatch_get_main_queue()
downloadProgress:downloadProgressBlock
completion:^(UIImage *downloadedImage, NSError *error, id downloadIdentifier) {
completion:^(id <ASImageContainerProtocol> imageContainer, NSError *error, id downloadIdentifier) {
// We dereference iVars directly, so we can't have weakSelf going nil on us.
__typeof__(self) strongSelf = weakSelf;
if (!strongSelf)
@@ -796,7 +796,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
return;
}
completionBlock(downloadedImage, error);
completionBlock([imageContainer asdk_image], error);
// Delegateify.
if (strongSelf->_delegateFlags.downloadFinish)

View File

@@ -8,12 +8,15 @@
#import "ASNetworkImageNode.h"
#import "ASAnimatedImage.h"
#import "ASBasicImageDownloader.h"
#import "ASDisplayNode+Subclasses.h"
#import "ASDisplayNode+FrameworkPrivate.h"
#import "ASEqualityHelpers.h"
#import "ASThread.h"
#import "ASInternalHelpers.h"
#import "ASImageContainerProtocolCategories.h"
#import "ASImageNode+AnimatedImage.h"
#if PIN_REMOTE_IMAGE
#import "ASPINRemoteImageDownloader.h"
@@ -188,7 +191,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
if (_cacheSupportsSynchronousFetch) {
ASDN::MutexLocker l(_lock);
if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) {
UIImage *result = [_cache synchronouslyFetchedCachedImageWithURL:_URL];
UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image];
if (result) {
self.image = result;
_imageLoaded = YES;
@@ -309,7 +312,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
_cacheUUID = nil;
}
- (void)_downloadImageWithCompletion:(void (^)(UIImage *image, NSError*, id downloadIdentifier))finished
- (void)_downloadImageWithCompletion:(void (^)(id <ASImageContainerProtocol> imageContainer, NSError*, id downloadIdentifier))finished
{
ASPerformBlockOnBackgroundThread(^{
ASDN::MutexLocker l(_lock);
@@ -317,9 +320,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
_downloadIdentifier = [_downloader downloadImageWithURL:_URL
callbackQueue:dispatch_get_main_queue()
downloadProgress:NULL
completion:^(UIImage * _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier) {
completion:^(id <ASImageContainerProtocol> _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier) {
if (finished != NULL) {
finished(image, error, downloadIdentifier);
finished(imageContainer, error, downloadIdentifier);
}
}];
} else {
@@ -376,7 +379,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
}
} else {
__weak __typeof__(self) weakSelf = self;
void (^finished)(UIImage *, NSError *, id downloadIdentifier) = ^(UIImage *responseImage, NSError *error, id downloadIdentifier) {
void (^finished)(id <ASImageContainerProtocol>, NSError *, id downloadIdentifier) = ^(id <ASImageContainerProtocol>imageContainer, NSError *error, id downloadIdentifier) {
__typeof__(self) strongSelf = weakSelf;
if (strongSelf == nil) {
return;
@@ -390,9 +393,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
return;
}
if (responseImage != NULL) {
if (imageContainer != nil) {
strongSelf->_imageLoaded = YES;
strongSelf.image = responseImage;
if ([imageContainer asdk_animatedImageData]) {
strongSelf.animatedImage = [[ASAnimatedImage alloc] initWithAnimatedImageData:[imageContainer asdk_animatedImageData]];
} else {
strongSelf.image = [imageContainer asdk_image];
}
}
strongSelf->_downloadIdentifier = nil;
@@ -402,7 +409,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
{
ASDN::MutexLocker l(strongSelf->_lock);
if (responseImage != NULL) {
if (imageContainer != nil) {
[strongSelf->_delegate imageNode:strongSelf didLoadImage:strongSelf.image];
}
else if (error && _delegateSupportsDidFailWithError) {
@@ -415,16 +422,16 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
NSUUID *cacheUUID = [NSUUID UUID];
_cacheUUID = cacheUUID;
void (^cacheCompletion)(UIImage *) = ^(UIImage *image) {
void (^cacheCompletion)(id <ASImageContainerProtocol>) = ^(id <ASImageContainerProtocol> imageContainer) {
// If the cache UUID changed, that means this request was cancelled.
if (![_cacheUUID isEqual:cacheUUID]) {
return;
}
if (image == NULL && _downloader != nil) {
if ([imageContainer asdk_image] == NULL && _downloader != nil) {
[self _downloadImageWithCompletion:finished];
} else {
finished(image, NULL, nil);
finished(imageContainer, NULL, nil);
}
};

View File

@@ -0,0 +1,19 @@
//
// ASImageContainerProtocolCategories.h
// Pods
//
// Created by Garrett Moon on 3/18/16.
//
//
#import <Foundation/Foundation.h>
#import "ASImageProtocols.h"
@interface UIImage (ASImageContainerProtocol) <ASImageContainerProtocol>
@end
@interface NSData (ASImageContainerProtocol) <ASImageContainerProtocol>
@end

View File

@@ -0,0 +1,37 @@
//
// ASImageContainerProtocolCategories.m
// Pods
//
// Created by Garrett Moon on 3/18/16.
//
//
#import "ASImageContainerProtocolCategories.h"
@implementation UIImage (ASImageContainerProtocol)
- (UIImage *)asdk_image
{
return self;
}
- (NSData *)asdk_animatedImageData
{
return nil;
}
@end
@implementation NSData (ASImageContainerProtocol)
- (UIImage *)asdk_image
{
return nil;
}
- (NSData *)asdk_animatedImageData
{
return self;
}
@end

View File

@@ -11,7 +11,14 @@
NS_ASSUME_NONNULL_BEGIN
typedef void(^ASImageCacherCompletion)(UIImage * _Nullable imageFromCache);
@protocol ASImageContainerProtocol <NSObject>
- (UIImage *)asdk_image;
- (NSData *)asdk_animatedImageData;
@end
typedef void(^ASImageCacherCompletion)(id <ASImageContainerProtocol> _Nullable imageFromCache);
@protocol ASImageCacheProtocol <NSObject>
@@ -28,7 +35,7 @@ typedef void(^ASImageCacherCompletion)(UIImage * _Nullable imageFromCache);
the calling thread to fetch the image from a fast memory cache. It is OK to return nil from this method and instead
support only cachedImageWithURL:callbackQueue:completion: however, synchronous rendering will not be possible.
*/
- (nullable UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL;
- (nullable id <ASImageContainerProtocol>)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL;
/**
@abstract Attempts to fetch an image with the given URL from the cache.
@@ -52,7 +59,7 @@ typedef void(^ASImageCacherCompletion)(UIImage * _Nullable imageFromCache);
@end
typedef void(^ASImageDownloaderCompletion)(UIImage * _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier);
typedef void(^ASImageDownloaderCompletion)(id <ASImageContainerProtocol> _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier);
typedef void(^ASImageDownloaderProgress)(CGFloat progress);
typedef void(^ASImageDownloaderProgressImage)(UIImage *progressImage, id _Nullable downloadIdentifier);

View File

@@ -12,9 +12,15 @@
#import "ASAssert.h"
#import "ASThread.h"
#import <PINRemoteImage/PINAlternateRepresentationDelegate.h>
#import <PINRemoteImage/PINRemoteImageManager.h>
#import <PINRemoteImage/NSData+ImageDetectors.h>
#import <PINCache/PINCache.h>
@interface ASPINRemoteImageDownloader () <PINRemoteImageManagerAlternateRepresentationDelegate>
@end
@implementation ASPINRemoteImageDownloader
+ (instancetype)sharedDownloader
@@ -29,16 +35,30 @@
#pragma mark ASImageProtocols
- (UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL
+ (PINRemoteImageManager *)sharedPINRemoteImageManager
{
NSString *key = [[PINRemoteImageManager sharedImageManager] cacheKeyForURL:URL processorKey:nil];
PINRemoteImageManagerResult *result = [[PINRemoteImageManager sharedImageManager] synchronousImageFromCacheWithCacheKey:key options:PINRemoteImageManagerDownloadOptionsSkipDecode];
static PINRemoteImageManager *sharedPINRemoteImageManager = nil;
static dispatch_once_t once = 0;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPINRemoteImageManager = [[PINRemoteImageManager alloc] initWithSessionConfiguration:nil alternativeRepresentationDelegate:self];
});
return sharedPINRemoteImageManager;
}
- (id <ASImageContainerProtocol>)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL;
{
NSString *key = [[[self class] sharedPINRemoteImageManager] cacheKeyForURL:URL processorKey:nil];
PINRemoteImageManagerResult *result = [[[self class] sharedPINRemoteImageManager] synchronousImageFromCacheWithCacheKey:key options:PINRemoteImageManagerDownloadOptionsSkipDecode];
if (result.alternativeRepresentation) {
return result.alternativeRepresentation;
}
return result.image;
}
- (void)fetchCachedImageWithURL:(NSURL *)URL
callbackQueue:(dispatch_queue_t)callbackQueue
completion:(void (^)(CGImageRef imageFromCache))completion
- (void)cachedImageWithURL:(NSURL *)URL
callbackQueue:(dispatch_queue_t)callbackQueue
completion:(ASImageCacherCompletion)completion
{
// We do not check the cache here and instead check it in downloadImageWithURL to avoid checking the cache twice.
// If we're targeting the main queue and we're on the main thread, complete immediately.
@@ -53,23 +73,31 @@
- (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL
{
PINRemoteImageManager *manager = [PINRemoteImageManager sharedImageManager];
PINRemoteImageManager *manager = [[self class] sharedPINRemoteImageManager];
NSString *key = [manager cacheKeyForURL:URL processorKey:nil];
[[[manager cache] memoryCache] removeObjectForKey:key];
}
- (nullable id)downloadImageWithURL:(NSURL *)URL
callbackQueue:(dispatch_queue_t)callbackQueue
downloadProgress:(void (^)(CGFloat progress))downloadProgressBlock
completion:(void (^)(UIImage *image, NSError * error, id downloadIdentifier))completion
downloadProgress:(ASImageDownloaderProgress)downloadProgress
completion:(ASImageDownloaderCompletion)completion;
{
return [[PINRemoteImageManager sharedImageManager] downloadImageWithURL:URL options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult *result) {
return [[[self class] sharedPINRemoteImageManager] downloadImageWithURL:URL options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult *result) {
/// If we're targeting the main queue and we're on the main thread, complete immediately.
if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) {
completion(result.image, result.error, result.UUID);
if (result.alternativeRepresentation) {
completion(result.alternativeRepresentation, result.error, result.UUID);
} else {
completion(result.image, result.error, result.UUID);
}
} else {
dispatch_async(callbackQueue, ^{
completion(result.image, result.error, result.UUID);
if (result.alternativeRepresentation) {
completion(result.alternativeRepresentation, result.error, result.UUID);
} else {
completion(result.image, result.error, result.UUID);
}
});
}
}];
@@ -78,7 +106,7 @@
- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier
{
ASDisplayNodeAssert([downloadIdentifier isKindOfClass:[NSUUID class]], @"downloadIdentifier must be NSUUID");
[[PINRemoteImageManager sharedImageManager] cancelTaskWithUUID:downloadIdentifier];
[[[self class] sharedPINRemoteImageManager] cancelTaskWithUUID:downloadIdentifier];
}
- (void)setProgressImageBlock:(ASImageDownloaderProgressImage)progressBlock callbackQueue:(dispatch_queue_t)callbackQueue withDownloadIdentifier:(id)downloadIdentifier
@@ -86,13 +114,13 @@
ASDisplayNodeAssert([downloadIdentifier isKindOfClass:[NSUUID class]], @"downloadIdentifier must be NSUUID");
if (progressBlock) {
[[PINRemoteImageManager sharedImageManager] setProgressImageCallback:^(PINRemoteImageManagerResult * _Nonnull result) {
[[[self class] sharedPINRemoteImageManager] setProgressImageCallback:^(PINRemoteImageManagerResult * _Nonnull result) {
dispatch_async(callbackQueue, ^{
progressBlock(result.image, result.UUID);
});
} ofTaskWithUUID:downloadIdentifier];
} else {
[[PINRemoteImageManager sharedImageManager] setProgressImageCallback:nil ofTaskWithUUID:downloadIdentifier];
[[[self class] sharedPINRemoteImageManager] setProgressImageCallback:nil ofTaskWithUUID:downloadIdentifier];
}
}
@@ -114,7 +142,17 @@
pi_priority = PINRemoteImageManagerPriorityVeryHigh;
break;
}
[[PINRemoteImageManager sharedImageManager] setPriority:pi_priority ofTaskWithUUID:downloadIdentifier];
[[[self class] sharedPINRemoteImageManager] setPriority:pi_priority ofTaskWithUUID:downloadIdentifier];
}
#pragma mark - PINRemoteImageManagerAlternateRepresentationDelegate
+ (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageManagerDownloadOptions)options
{
if ([data pin_isGIF]) {
return data;
}
return nil;
}
@end

View File

@@ -0,0 +1,385 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
683ADBA31CA19883005863A4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 683ADBA21CA19883005863A4 /* main.m */; };
683ADBA61CA19883005863A4 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 683ADBA51CA19883005863A4 /* AppDelegate.m */; };
683ADBA91CA19883005863A4 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 683ADBA81CA19883005863A4 /* ViewController.m */; };
683ADBAC1CA19883005863A4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 683ADBAA1CA19883005863A4 /* Main.storyboard */; };
683ADBAE1CA19883005863A4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 683ADBAD1CA19883005863A4 /* Assets.xcassets */; };
683ADBB11CA19883005863A4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 683ADBAF1CA19883005863A4 /* LaunchScreen.storyboard */; };
EE964E5E7CD506D45C6DCC49 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A6F2399FA1A86586D9BDAE05 /* libPods.a */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
683ADB9E1CA19883005863A4 /* ASAnimatedImage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ASAnimatedImage.app; sourceTree = BUILT_PRODUCTS_DIR; };
683ADBA21CA19883005863A4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
683ADBA41CA19883005863A4 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
683ADBA51CA19883005863A4 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
683ADBA71CA19883005863A4 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
683ADBA81CA19883005863A4 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
683ADBAB1CA19883005863A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
683ADBAD1CA19883005863A4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
683ADBB01CA19883005863A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
683ADBB21CA19883005863A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A6F2399FA1A86586D9BDAE05 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; };
BBB395EF2813E7DB5CB49459 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = "<group>"; };
BBBE85D30A6D31AD7021A9AF /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
683ADB9B1CA19883005863A4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EE964E5E7CD506D45C6DCC49 /* libPods.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
683ADB951CA19883005863A4 = {
isa = PBXGroup;
children = (
683ADBA01CA19883005863A4 /* ASAnimatedImage */,
683ADB9F1CA19883005863A4 /* Products */,
71A772B0DB9B7760CE330DD9 /* Pods */,
8C6AC07DE55B51935C632F56 /* Frameworks */,
);
sourceTree = "<group>";
};
683ADB9F1CA19883005863A4 /* Products */ = {
isa = PBXGroup;
children = (
683ADB9E1CA19883005863A4 /* ASAnimatedImage.app */,
);
name = Products;
sourceTree = "<group>";
};
683ADBA01CA19883005863A4 /* ASAnimatedImage */ = {
isa = PBXGroup;
children = (
683ADBA41CA19883005863A4 /* AppDelegate.h */,
683ADBA51CA19883005863A4 /* AppDelegate.m */,
683ADBA71CA19883005863A4 /* ViewController.h */,
683ADBA81CA19883005863A4 /* ViewController.m */,
683ADBAA1CA19883005863A4 /* Main.storyboard */,
683ADBAD1CA19883005863A4 /* Assets.xcassets */,
683ADBAF1CA19883005863A4 /* LaunchScreen.storyboard */,
683ADBB21CA19883005863A4 /* Info.plist */,
683ADBA11CA19883005863A4 /* Supporting Files */,
);
path = ASAnimatedImage;
sourceTree = "<group>";
};
683ADBA11CA19883005863A4 /* Supporting Files */ = {
isa = PBXGroup;
children = (
683ADBA21CA19883005863A4 /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
71A772B0DB9B7760CE330DD9 /* Pods */ = {
isa = PBXGroup;
children = (
BBBE85D30A6D31AD7021A9AF /* Pods.debug.xcconfig */,
BBB395EF2813E7DB5CB49459 /* Pods.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
8C6AC07DE55B51935C632F56 /* Frameworks */ = {
isa = PBXGroup;
children = (
A6F2399FA1A86586D9BDAE05 /* libPods.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
683ADB9D1CA19883005863A4 /* ASAnimatedImage */ = {
isa = PBXNativeTarget;
buildConfigurationList = 683ADBB51CA19883005863A4 /* Build configuration list for PBXNativeTarget "ASAnimatedImage" */;
buildPhases = (
694B306B43ED1C3916B0D909 /* Check Pods Manifest.lock */,
683ADB9A1CA19883005863A4 /* Sources */,
683ADB9B1CA19883005863A4 /* Frameworks */,
683ADB9C1CA19883005863A4 /* Resources */,
26A96BEEF893B1FA39F144CF /* Embed Pods Frameworks */,
2ADE0E7B5309A9CD043DDB3E /* Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = ASAnimatedImage;
productName = ASAnimatedImage;
productReference = 683ADB9E1CA19883005863A4 /* ASAnimatedImage.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
683ADB961CA19883005863A4 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0720;
ORGANIZATIONNAME = "Facebook, Inc.";
TargetAttributes = {
683ADB9D1CA19883005863A4 = {
CreatedOnToolsVersion = 7.2;
};
};
};
buildConfigurationList = 683ADB991CA19883005863A4 /* Build configuration list for PBXProject "ASAnimatedImage" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 683ADB951CA19883005863A4;
productRefGroup = 683ADB9F1CA19883005863A4 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
683ADB9D1CA19883005863A4 /* ASAnimatedImage */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
683ADB9C1CA19883005863A4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
683ADBB11CA19883005863A4 /* LaunchScreen.storyboard in Resources */,
683ADBAE1CA19883005863A4 /* Assets.xcassets in Resources */,
683ADBAC1CA19883005863A4 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
26A96BEEF893B1FA39F144CF /* Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
2ADE0E7B5309A9CD043DDB3E /* Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n";
showEnvVarsInLog = 0;
};
694B306B43ED1C3916B0D909 /* Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Check Pods Manifest.lock";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
683ADB9A1CA19883005863A4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
683ADBA91CA19883005863A4 /* ViewController.m in Sources */,
683ADBA61CA19883005863A4 /* AppDelegate.m in Sources */,
683ADBA31CA19883005863A4 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
683ADBAA1CA19883005863A4 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
683ADBAB1CA19883005863A4 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
683ADBAF1CA19883005863A4 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
683ADBB01CA19883005863A4 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
683ADBB31CA19883005863A4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
683ADBB41CA19883005863A4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
683ADBB61CA19883005863A4 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BBBE85D30A6D31AD7021A9AF /* Pods.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = ASAnimatedImage/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = facebook.ASAnimatedImage;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
683ADBB71CA19883005863A4 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BBB395EF2813E7DB5CB49459 /* Pods.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = ASAnimatedImage/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = facebook.ASAnimatedImage;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
683ADB991CA19883005863A4 /* Build configuration list for PBXProject "ASAnimatedImage" */ = {
isa = XCConfigurationList;
buildConfigurations = (
683ADBB31CA19883005863A4 /* Debug */,
683ADBB41CA19883005863A4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
683ADBB51CA19883005863A4 /* Build configuration list for PBXNativeTarget "ASAnimatedImage" */ = {
isa = XCConfigurationList;
buildConfigurations = (
683ADBB61CA19883005863A4 /* Debug */,
683ADBB71CA19883005863A4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 683ADB961CA19883005863A4 /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:ASAnimatedImage.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:ASAnimatedImage.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,30 @@
{
"DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "F50970FDD9A73637F0802C23B728B6B5862D49A6",
"DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : {
},
"DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : {
"9B1DC435B89529078DE1FAE4D39ED8D2F4E74032" : 0,
"F50970FDD9A73637F0802C23B728B6B5862D49A6" : 0
},
"DVTSourceControlWorkspaceBlueprintIdentifierKey" : "E4DD3811-C87E-4EF5-A540-2040B2D011E3",
"DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : {
"9B1DC435B89529078DE1FAE4D39ED8D2F4E74032" : "PINRemoteImage\/",
"F50970FDD9A73637F0802C23B728B6B5862D49A6" : "AsyncDisplayKit\/"
},
"DVTSourceControlWorkspaceBlueprintNameKey" : "ASAnimatedImage",
"DVTSourceControlWorkspaceBlueprintVersion" : 204,
"DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "examples\/ASAnimatedImage\/ASAnimatedImage.xcworkspace",
"DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [
{
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:pinterest\/PINRemoteImage.git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "9B1DC435B89529078DE1FAE4D39ED8D2F4E74032"
},
{
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "ssh:\/\/phabricator.pinadmin.com\/diffusion\/ASYNC\/asycdisplaykit.git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "F50970FDD9A73637F0802C23B728B6B5862D49A6"
}
]
}

View File

@@ -0,0 +1,17 @@
//
// AppDelegate.h
// ASAnimatedImage
//
// Created by Garrett Moon on 3/22/16.
// Copyright © 2016 Facebook, Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

View File

@@ -0,0 +1,45 @@
//
// AppDelegate.m
// ASAnimatedImage
//
// Created by Garrett Moon on 3/22/16.
// Copyright © 2016 Facebook, Inc. All rights reserved.
//
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@end

View File

@@ -0,0 +1,68 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8150" systemVersion="15A204g" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8122"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6211" systemVersion="14A298i" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6204"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
//
// ViewController.h
// ASAnimatedImage
//
// Created by Garrett Moon on 3/22/16.
// Copyright © 2016 Facebook, Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end

View File

@@ -0,0 +1,38 @@
//
// ViewController.m
// ASAnimatedImage
//
// Created by Garrett Moon on 3/22/16.
// Copyright © 2016 Facebook, Inc. All rights reserved.
//
#import "ViewController.h"
#import <AsyncDisplayKit/AsyncDisplayKit.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init];
imageNode.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"];
// imageNode.URL = [NSURL fileURLWithPath:@"/Users/garrett/Downloads/new-transparent-gif-221.gif"];
imageNode.frame = self.view.bounds;
imageNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
imageNode.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubnode:imageNode];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end

View File

@@ -0,0 +1,16 @@
//
// main.m
// ASAnimatedImage
//
// Created by Garrett Moon on 3/22/16.
// Copyright © 2016 Facebook, Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@@ -0,0 +1,4 @@
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
pod 'AsyncDisplayKit', :path => '../..'
pod 'PINRemoteImage', :git => 'https://github.com/pinterest/PINRemoteImage.git', :branch => 'addSupportForCustomDecode'