Forked from foundry/Progressive image loading for iOS
Created
March 10, 2016 22:37
-
-
Save dstd/7cafb6c2484a0ad93cd9 to your computer and use it in GitHub Desktop.
Progressive image loading for iOS. This is a GIST - detailed implementation is left as an exercise for the reader.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
typical usage (FYImageStore is a singleton) | |
[[FYImageStore instance] retrieveFullsizeImageFromPath:path | |
completion:^(UIImage* image){ | |
self.imageView.image = image | |
}]; | |
*/ | |
#import "FYImageURL.h" //URL-building utilities, omitted from this Gist. | |
/* | |
path is the full path to the image resource, either in the local cache or on the server. | |
paths have varying suffixes for resource size | |
eg small_thumb.jpg / large_thumb.jpg | |
error checking omitted from this Gist. | |
*/ | |
typedef void (^RetrieveImage)(UIImage *image); | |
typedef enum { | |
kSmallThumbImage, | |
kLargeThumbImage, | |
kFullsizeImage, | |
kNormalSize | |
} ImageSize; | |
@interface FYImageStore | |
@property (nonatomic, strong) NSOperationQueue* accessQueue; | |
@property (nonatomic, strong) NSCache* imageCache; | |
@property (nonatomic, strong) NSMutableDictionary* operations; | |
//progressive loading of lower-res images | |
- (void) retrieveFullsizeImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock; | |
- (void)retrieveLargeThumbImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock; | |
- (void)retrieveSmallThumbImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock; | |
//specified res only, no progressive loading | |
- (void)retrieveImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock; | |
//respond to low memory warnings | |
- (void)lowMemoryCleanup; | |
@end | |
@implementation FYImageStore //(initialisation and other details omitted from the gist) | |
#pragma mark - async requests from path | |
- (void) retrieveFullsizeImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock | |
{ | |
[self retrieveImageFromPath:path | |
ofSize:kFullsizeImage | |
completion:completionBlock]; | |
} | |
- (void)retrieveLargeThumbImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock | |
{ | |
[self retrieveImageFromPath:path | |
ofSize:kLargeThumbImage | |
completion:completionBlock]; | |
} | |
- (void)retrieveSmallThumbImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock | |
{ | |
[self retrieveImageFromPath:path | |
ofSize:kSmallThumbImage | |
completion:completionBlock]; | |
} | |
- (void)retrieveImageFromPath:(NSString*)path | |
completion:(RetrieveImage)completionBlock { | |
[self retrieveImageFromPath:path | |
ofSize:kNormalSize | |
completion:completionBlock]; | |
} | |
- (void)retrieveImageFromPath:(NSString*)path | |
ofSize:(ImageSize)size | |
completion:(RetrieveImage)completionBlock | |
{ | |
//recursively retrieve lower-res images | |
if (path && (size>0 && size<3)) { | |
[self retrieveImageFromPath:path | |
ofSize:(size-1) | |
completion:completionBlock]; | |
} | |
//set up block operation | |
NSBlockOperation* operation = [[NSBlockOperation alloc] init]; | |
__weak typeof (operation) operation_ = operation; | |
[operation addExecutionBlock:^{ | |
__strong typeof (operation) operation__ = operation_; | |
NSString* imagePath = [FYImageURL imagePath:path forSize:size]; | |
if (operation__.isCancelled) return; | |
UIImage* image = [self imageFromPath:imagePath ofSize:size]; | |
if (!image) return; | |
[self cancelOperationsForPath:path size:size]; | |
if (operation__.isCancelled) { | |
image = nil; | |
return; | |
}; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
if (!operation__.isCancelled) { | |
completionBlock(image); | |
} | |
}); | |
}]; | |
//add operation to operations dictionary (so that we can cancel) | |
if ([[self.operations allKeys] containsObject:path]) { | |
NSMutableDictionary* dict = [self.operations objectForKey:path]; | |
[dict setObject:operation forKey:@(size)]; | |
} else { | |
NSMutableDictionary* dict = [[NSMutableDictionary alloc] init]; | |
[dict setObject:operation forKey:@(size)]; | |
[self.operations setObject:dict forKey:path]; | |
} | |
//add operation to operation queue for execution | |
[self.accessQueue addOperation:operation]; | |
} | |
//operation cancelling | |
//when we retrieve an image of higher res, | |
//cancel operations for same image of lower res | |
- (void)cancelOperationsForPath:(NSString*)path size:(ImageSize)size { | |
if (![self.operations objectForKey:path]) return; | |
for (int idx=0; idx<size; idx++) { | |
NSOperation* operation = [[self.operations objectForKey:path] objectForKey:@(idx)]; | |
[operation cancel]; | |
[[self.operations objectForKey:path] removeObjectForKey:@(idx)]; | |
if ([[self.operations objectForKey:path] allKeys].count == 0) { | |
[self.operations removeObjectForKey:path]; | |
} | |
} | |
} | |
- (UIImage*) imageFromPath:(NSString*)path | |
ofSize:(ImageSize)size | |
{ | |
NSString* imagePath = [FYImageURL imagePath:path forSize:size]; | |
return [self imageFromPath:imagePath]; | |
} | |
- (UIImage*) imageFromPath:(NSString*)imagePath | |
{ | |
//get from NSCache | |
UIImage* image = [self imageFromNSCache:imagePath]; | |
if (!image) { | |
//if it's not in NSCache, get from the file cache | |
image = [self imageFromFileCache:imagePath]; | |
} | |
if (!image) { | |
//if it's not in the file cache, fetch from the server | |
image = [self imageFromServer:imagePath]; | |
//(this operation will also write to the file cache) | |
} | |
return image; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment