diff --git a/AsyncImageView-blocks.podspec b/AsyncImageView-blocks.podspec new file mode 100644 index 0000000..ba14177 --- /dev/null +++ b/AsyncImageView-blocks.podspec @@ -0,0 +1,20 @@ +Pod::Spec.new do |s| +s.name = "AsyncImageView-blocks" +s.version = "1.5.1.2" +s.summary = "AsyncImageView is an extension of UIImageView for loading and displaying images asynchronously on iOS so that they do not lock up the UI." + +s.description = "AsyncImageView includes both a simple category on UIImageView for loading and displaying images asynchronously on iOS so that they do not lock up the UI, and a UIImageView subclass for more advanced features. AsyncImageView works with URLs so it can be used with either local or remote files. + +Loaded/downloaded images are cached in memory and are automatically cleaned up in the event of a memory warning. The AsyncImageView operates independently of the UIImage cache, but by default any images located in the root of the application bundle will be stored in the UIImage cache instead, avoiding any duplication of cached images. + +The library can also be used to load and cache images independently of a UIImageView as it provides direct access to the underlying loading and caching classes." + +s.homepage = "http://charcoaldesign.co.uk/source/cocoa#asyncimageview" +s.license = { :type => 'zlib', :file => 'LICENCE.md' } +s.author = { 'Fraser Scott-Morrison' => 'fraserscottmorrison@me.com' } +s.source = { :git => "https://github.com/IdleHandsApps/AsyncImageView.git", :tag => s.version.to_s } + +s.platform = :ios, '4.3' +s.source_files = 'AsyncImageView' +s.requires_arc = true +end diff --git a/AsyncImageView.podspec b/AsyncImageView.podspec deleted file mode 100644 index 56517ae..0000000 --- a/AsyncImageView.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - s.name = "AsyncImageView" - s.version = "1.5.1" - s.summary = "AsyncImageView is an extension of UIImageView for loading and displaying images asynchronously on iOS so that they do not lock up the UI." - - s.description = "AsyncImageView includes both a simple category on UIImageView for loading and displaying images asynchronously on iOS so that they do not lock up the UI, and a UIImageView subclass for more advanced features. AsyncImageView works with URLs so it can be used with either local or remote files. - - Loaded/downloaded images are cached in memory and are automatically cleaned up in the event of a memory warning. The AsyncImageView operates independently of the UIImage cache, but by default any images located in the root of the application bundle will be stored in the UIImage cache instead, avoiding any duplication of cached images. - - The library can also be used to load and cache images independently of a UIImageView as it provides direct access to the underlying loading and caching classes." - - s.homepage = "http://charcoaldesign.co.uk/source/cocoa#asyncimageview" - s.license = { :type => 'zlib', :file => 'LICENCE.md' } - s.author = { "Nick Lockwood" => "support@charcoaldesign.co.uk" } - s.source = { :git => "https://github.com/nicklockwood/AsyncImageView.git", :tag => "1.5.1" } - - s.platform = :ios, '4.3' - s.source_files = 'AsyncImageView' - s.requires_arc = true -end diff --git a/AsyncImageView/AsyncImageView.h b/AsyncImageView/AsyncImageView.h index 7b1b64d..9d330ed 100755 --- a/AsyncImageView/AsyncImageView.h +++ b/AsyncImageView/AsyncImageView.h @@ -54,12 +54,15 @@ extern NSString *const AsyncImageErrorKey; @property (nonatomic, assign) NSUInteger concurrentLoads; @property (nonatomic, assign) NSTimeInterval loadingTimeout; +- (void)loadImageWithURL:(NSURL *)URL successBlock:(void (^)(UIImage *image))successBlock failureBlock:(void(^)(NSError *error))failureBlock; // New method for blocks by Fraser Scott-Morrison - (void)loadImageWithURL:(NSURL *)URL target:(id)target success:(SEL)success failure:(SEL)failure; - (void)loadImageWithURL:(NSURL *)URL target:(id)target action:(SEL)action; - (void)loadImageWithURL:(NSURL *)URL; +- (void)cancelLoadingURL:(NSURL *)URL actionBlock:(void (^)(UIImage *image))successBlock; // New method for blocks by Fraser Scott-Morrison - (void)cancelLoadingURL:(NSURL *)URL target:(id)target action:(SEL)action; - (void)cancelLoadingURL:(NSURL *)URL target:(id)target; - (void)cancelLoadingURL:(NSURL *)URL; +- (void)cancelLoadingImagesForTarget:(id)target actionBlock:(void (^)(UIImage *image))successBlock; // New method for blocks by Fraser Scott-Morrison - (void)cancelLoadingImagesForTarget:(id)target action:(SEL)action; - (void)cancelLoadingImagesForTarget:(id)target; - (NSURL *)URLForTarget:(id)target action:(SEL)action; @@ -72,6 +75,7 @@ extern NSString *const AsyncImageErrorKey; @property (nonatomic, strong) NSURL *imageURL; +- (void)setImageURL:(NSURL *)imageURL successBlock:(void (^)(UIImage *image))successBlock failureBlock:(void(^)(NSError *error))failureBlock; // New method for blocks by Fraser Scott-Morrison @end @@ -81,6 +85,8 @@ extern NSString *const AsyncImageErrorKey; @property (nonatomic, assign) UIActivityIndicatorViewStyle activityIndicatorStyle; @property (nonatomic, assign) NSTimeInterval crossfadeDuration; +- (void)setImageURL:(NSURL *)imageURL successBlock:(void (^)(UIImage *image))successBlock failureBlock:(void(^)(NSError *error))failureBlock; // New method for blocks by Fraser Scott-Morrison + @end diff --git a/AsyncImageView/AsyncImageView.m b/AsyncImageView/AsyncImageView.m index a366de9..d8e9ffa 100755 --- a/AsyncImageView/AsyncImageView.m +++ b/AsyncImageView/AsyncImageView.m @@ -58,16 +58,25 @@ @interface AsyncImageConnection : NSObject @property (nonatomic, strong) NSURL *URL; @property (nonatomic, strong) NSCache *cache; @property (nonatomic, strong) id target; +@property (nonatomic) BOOL doRetry; @property (nonatomic, assign) SEL success; @property (nonatomic, assign) SEL failure; +@property (nonatomic, copy) void (^successBlock)(UIImage *image); +@property (nonatomic, copy) void (^failureBlock)(NSError *error); @property (nonatomic, getter = isLoading) BOOL loading; @property (nonatomic, getter = isCancelled) BOOL cancelled; +// New method for blocks by Fraser Scott-Morrison - (AsyncImageConnection *)initWithURL:(NSURL *)URL cache:(NSCache *)cache - target:(id)target - success:(SEL)success - failure:(SEL)failure; + successBlock:(void (^)(UIImage *image))successBlock + failureBlock:(void(^)(NSError *error))failureBlock; + +- (AsyncImageConnection *)initWithURL:(NSURL *)URL + cache:(NSCache *)cache + target:(id)target + success:(SEL)success + failure:(SEL)failure; - (void)start; - (void)cancel; @@ -80,9 +89,9 @@ @implementation AsyncImageConnection - (AsyncImageConnection *)initWithURL:(NSURL *)URL cache:(NSCache *)cache - target:(id)target - success:(SEL)success - failure:(SEL)failure + target:(id)target + success:(SEL)success + failure:(SEL)failure { if ((self = [self init])) { @@ -95,17 +104,33 @@ - (AsyncImageConnection *)initWithURL:(NSURL *)URL return self; } +// New method for blocks by Fraser Scott-Morrison +- (AsyncImageConnection *)initWithURL:(NSURL *)URL + cache:(NSCache *)cache + successBlock:(void (^)(UIImage *image))successBlock + failureBlock:(void(^)(NSError *error))failureBlock +{ + if ((self = [self init])) + { + self.URL = URL; + self.cache = cache; + self.successBlock = successBlock; + self.failureBlock = failureBlock; + } + return self; +} + - (UIImage *)cachedImage { if ([self.URL isFileURL]) - { - NSString *path = [[self.URL absoluteURL] path]; + { + NSString *path = [[self.URL absoluteURL] path]; NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; - if ([path hasPrefix:resourcePath]) - { - return [UIImage imageNamed:[path substringFromIndex:[resourcePath length]]]; - } - } + if ([path hasPrefix:resourcePath]) + { + return [UIImage imageNamed:[path substringFromIndex:[resourcePath length]]]; + } + } return [self.cache objectForKey:self.URL]; } @@ -116,18 +141,18 @@ - (BOOL)isInCache - (void)loadFailedWithError:(NSError *)error { - self.loading = NO; - self.cancelled = NO; + self.loading = NO; + self.cancelled = NO; [[NSNotificationCenter defaultCenter] postNotificationName:AsyncImageLoadDidFail object:self.target userInfo:@{AsyncImageURLKey: self.URL, - AsyncImageErrorKey: error}]; + AsyncImageErrorKey: error}]; } - (void)cacheImage:(UIImage *)image { - if (!self.cancelled) - { + if (!self.cancelled) + { if (image && self.URL) { BOOL storeInCache = YES; @@ -145,64 +170,64 @@ - (void)cacheImage:(UIImage *)image } } - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: - image, AsyncImageImageKey, - self.URL, AsyncImageURLKey, - nil]; - if (self.cache) - { - userInfo[AsyncImageCacheKey] = self.cache; - } - - self.loading = NO; - [[NSNotificationCenter defaultCenter] postNotificationName:AsyncImageLoadDidFinish - object:self.target - userInfo:[userInfo copy]]; - } - else - { - self.loading = NO; - self.cancelled = NO; - } + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + image, AsyncImageImageKey, + self.URL, AsyncImageURLKey, + nil]; + if (self.cache) + { + userInfo[AsyncImageCacheKey] = self.cache; + } + + self.loading = NO; + [[NSNotificationCenter defaultCenter] postNotificationName:AsyncImageLoadDidFinish + object:self.target + userInfo:[userInfo copy]]; + } + else + { + self.loading = NO; + self.cancelled = NO; + } } - (void)processDataInBackground:(NSData *)data { - @synchronized ([self class]) - { - if (!self.cancelled) - { - UIImage *image = [[UIImage alloc] initWithData:data]; - if (image) - { + @synchronized ([self class]) + { + if (!self.cancelled) + { + UIImage *image = [[UIImage alloc] initWithData:data scale:2.0f]; + if (image) + { //redraw to prevent deferred decompression UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); [image drawAtPoint:CGPointZero]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - //add to cache (may be cached already but it doesn't matter) + //add to cache (may be cached already but it doesn't matter) [self performSelectorOnMainThread:@selector(cacheImage:) withObject:image waitUntilDone:YES]; - } - else - { + } + else + { @autoreleasepool { NSError *error = [NSError errorWithDomain:@"AsyncImageLoader" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Invalid image data"}]; [self performSelectorOnMainThread:@selector(loadFailedWithError:) withObject:error waitUntilDone:YES]; - } - } - } - else - { - //clean up - [self performSelectorOnMainThread:@selector(cacheImage:) - withObject:nil - waitUntilDone:YES]; - } - } + } + } + } + else + { + //clean up + [self performSelectorOnMainThread:@selector(cacheImage:) + withObject:nil + waitUntilDone:YES]; + } + } } - (void)connection:(__unused NSURLConnection *)connection didReceiveResponse:(__unused NSURLResponse *)response @@ -236,10 +261,10 @@ - (void)start { return; } - - //begin loading - self.loading = YES; - self.cancelled = NO; + + //begin loading + self.loading = YES; + self.cancelled = NO; //check for nil URL if (self.URL == nil) @@ -249,7 +274,7 @@ - (void)start } //check for cached image - UIImage *image = [self cachedImage]; + UIImage *image = [self cachedImage]; if (image) { //add to cache (cached already but it doesn't matter) @@ -271,7 +296,7 @@ - (void)start - (void)cancel { - self.cancelled = YES; + self.cancelled = YES; [self.connection cancel]; self.connection = nil; self.data = nil; @@ -291,46 +316,46 @@ @implementation AsyncImageLoader + (AsyncImageLoader *)sharedLoader { - static AsyncImageLoader *sharedInstance = nil; - if (sharedInstance == nil) - { - sharedInstance = [(AsyncImageLoader *)[self alloc] init]; - } - return sharedInstance; + static AsyncImageLoader *sharedInstance = nil; + if (sharedInstance == nil) + { + sharedInstance = [(AsyncImageLoader *)[self alloc] init]; + } + return sharedInstance; } + (NSCache *)defaultCache { static NSCache *sharedCache = nil; - if (sharedCache == nil) - { - sharedCache = [[NSCache alloc] init]; + if (sharedCache == nil) + { + sharedCache = [[NSCache alloc] init]; [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification *note) { [sharedCache removeAllObjects]; }]; - } - return sharedCache; + } + return sharedCache; } - (AsyncImageLoader *)init { - if ((self = [super init])) - { + if ((self = [super init])) + { self.cache = [[self class] defaultCache]; _concurrentLoads = 2; _loadingTimeout = 60.0; - _connections = [[NSMutableArray alloc] init]; + _connections = [[NSMutableArray alloc] init]; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(imageLoaded:) - name:AsyncImageLoadDidFinish - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(imageFailed:) - name:AsyncImageLoadDidFail - object:nil]; - } - return self; + selector:@selector(imageLoaded:) + name:AsyncImageLoadDidFinish + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(imageFailed:) + name:AsyncImageLoadDidFail + object:nil]; + } + return self; } - (void)updateQueue @@ -355,7 +380,7 @@ - (void)updateQueue } - (void)imageLoaded:(NSNotification *)notification -{ +{ //complete connections for URL NSURL *URL = (notification.userInfo)[AsyncImageURLKey]; for (NSInteger i = (NSInteger)[self.connections count] - 1; i >= 0; i--) @@ -367,8 +392,7 @@ - (void)imageLoaded:(NSNotification *)notification for (NSInteger j = i - 1; j >= 0; j--) { AsyncImageConnection *earlier = self.connections[(NSUInteger)j]; - if (earlier.target == connection.target && - earlier.success == connection.success) + if ((earlier.target == connection.target && earlier.success == connection.success) || (connection.successBlock && earlier.successBlock == connection.successBlock)) { [earlier cancel]; [self.connections removeObjectAtIndex:(NSUInteger)j]; @@ -380,8 +404,13 @@ - (void)imageLoaded:(NSNotification *)notification [connection cancel]; //perform action - UIImage *image = (notification.userInfo)[AsyncImageImageKey]; - ((void (*)(id, SEL, id, id))objc_msgSend)(connection.target, connection.success, image, connection.URL); + UIImage *image = (notification.userInfo)[AsyncImageImageKey]; + if (connection.target && connection.success) { + ((void (*)(id, SEL, id, id))objc_msgSend)(connection.target, connection.success, image, connection.URL); + } + else if (connection.successBlock) { + connection.successBlock(image); + } //remove from queue [self.connections removeObjectAtIndex:(NSUInteger)i]; @@ -404,15 +433,24 @@ - (void)imageFailed:(NSNotification *)notification //cancel connection (in case it's a duplicate) [connection cancel]; - //perform failure action - if (connection.failure) - { - NSError *error = (notification.userInfo)[AsyncImageErrorKey]; - ((void (*)(id, SEL, id, id))objc_msgSend)(connection.target, connection.failure, error, URL); - } - //remove from queue [self.connections removeObjectAtIndex:(NSUInteger)i]; + + if (connection.doRetry) { + // retry once + [self loadImageWithURL:connection.URL doRetry:NO successBlock:connection.successBlock failureBlock:connection.failureBlock]; + } + else { + //perform failure action + if (connection.failure) + { + NSError *error = (notification.userInfo)[AsyncImageErrorKey]; + ((void (*)(id, SEL, id, id))objc_msgSend)(connection.target, connection.failure, error, URL); + } + else if (connection.failureBlock) { + connection.failureBlock([NSError errorWithDomain:@"An image failed to load" code:0 userInfo:nil]); + } + } } } @@ -420,6 +458,52 @@ - (void)imageFailed:(NSNotification *)notification [self updateQueue]; } +// New method for blocks by Fraser Scott-Morrison. Will automatically try to reload image (once) if it fails +- (void)loadImageWithURL:(NSURL *)URL successBlock:(void (^)(UIImage *image))successBlock failureBlock:(void(^)(NSError *error))failureBlock { + [self loadImageWithURL:URL doRetry:YES successBlock:successBlock failureBlock:failureBlock]; +} + +// New method for blocks by Fraser Scott-Morrison +- (void)loadImageWithURL:(NSURL *)URL doRetry:(BOOL)doRetry successBlock:(void (^)(UIImage *image))successBlock failureBlock:(void(^)(NSError *error))failureBlock { + //check cache + UIImage *image = [self.cache objectForKey:URL]; + if (image) + { + [self cancelLoadingImagesForTarget:self actionBlock:successBlock]; + if (successBlock) + { + dispatch_async(dispatch_get_main_queue(), ^(void) { + successBlock(image); + }); + } + return; + } + + //create new connection + AsyncImageConnection *connection = [[AsyncImageConnection alloc] initWithURL:URL + cache:self.cache + successBlock:successBlock + failureBlock:failureBlock]; + connection.doRetry = doRetry; + BOOL added = NO; + for (NSUInteger i = 0; i < [self.connections count]; i++) + { + AsyncImageConnection *existingConnection = self.connections[i]; + if (!existingConnection.loading) + { + [self.connections insertObject:connection atIndex:i]; + added = YES; + break; + } + } + if (!added) + { + [self.connections addObject:connection]; + } + + [self updateQueue]; +} + - (void)loadImageWithURL:(NSURL *)URL target:(id)target success:(SEL)success failure:(SEL)failure { //check cache @@ -472,6 +556,20 @@ - (void)loadImageWithURL:(NSURL *)URL [self loadImageWithURL:URL target:nil success:NULL failure:NULL]; } +// New method for blocks by Fraser Scott-Morrison +- (void)cancelLoadingURL:(NSURL *)URL actionBlock:(void (^)(UIImage *image))successBlock +{ + for (NSInteger i = (NSInteger)[self.connections count] - 1; i >= 0; i--) + { + AsyncImageConnection *connection = self.connections[(NSUInteger)i]; + if ([connection.URL isEqual:URL] && connection.successBlock == successBlock) + { + [connection cancel]; + [self.connections removeObjectAtIndex:(NSUInteger)i]; + } + } +} + - (void)cancelLoadingURL:(NSURL *)URL target:(id)target action:(SEL)action { for (NSInteger i = (NSInteger)[self.connections count] - 1; i >= 0; i--) @@ -511,6 +609,19 @@ - (void)cancelLoadingURL:(NSURL *)URL } } +// New method for blocks by Fraser Scott-Morrison +- (void)cancelLoadingImagesForTarget:(id)target actionBlock:(void (^)(UIImage *image))successBlock +{ + for (NSInteger i = (NSInteger)[self.connections count] - 1; i >= 0; i--) + { + AsyncImageConnection *connection = self.connections[(NSUInteger)i]; + if (connection.target == target && connection.successBlock == successBlock) + { + [connection cancel]; + } + } +} + - (void)cancelLoadingImagesForTarget:(id)target action:(SEL)action { for (NSInteger i = (NSInteger)[self.connections count] - 1; i >= 0; i--) @@ -567,7 +678,7 @@ - (NSURL *)URLForTarget:(id)target - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end @@ -575,14 +686,20 @@ - (void)dealloc @implementation UIImageView(AsyncImageView) +// New method for blocks by Fraser Scott-Morrison +- (void)setImageURL:(NSURL *)imageURL successBlock:(void (^)(UIImage *image))successBlock failureBlock:(void(^)(NSError *error))failureBlock +{ + [[AsyncImageLoader sharedLoader] loadImageWithURL:imageURL successBlock:successBlock failureBlock:failureBlock]; +} + - (void)setImageURL:(NSURL *)imageURL { - [[AsyncImageLoader sharedLoader] loadImageWithURL:imageURL target:self action:@selector(setImage:)]; + [[AsyncImageLoader sharedLoader] loadImageWithURL:imageURL target:self action:@selector(setImage:)]; } - (NSURL *)imageURL { - return [[AsyncImageLoader sharedLoader] URLForTarget:self action:@selector(setImage:)]; + return [[AsyncImageLoader sharedLoader] URLForTarget:self action:@selector(setImage:)]; } @end @@ -599,9 +716,9 @@ @implementation AsyncImageView - (void)setUp { - self.showActivityIndicator = (self.image == nil); - self.activityIndicatorStyle = UIActivityIndicatorViewStyleGray; - self.crossfadeDuration = 0.4; + self.showActivityIndicator = (self.image == nil); + self.activityIndicatorStyle = UIActivityIndicatorViewStyleGray; + self.crossfadeDuration = 0.4; } - (id)initWithFrame:(CGRect)frame @@ -622,6 +739,31 @@ - (id)initWithCoder:(NSCoder *)aDecoder return self; } +// New method for blocks by Fraser Scott-Morrison +- (void)setImageURL:(NSURL *)imageURL successBlock:(void (^)(UIImage *image))successBlock failureBlock:(void(^)(NSError *error))failureBlock +{ + UIImage *image = [[AsyncImageLoader sharedLoader].cache objectForKey:imageURL]; + if (image) + { + self.image = image; + successBlock(image); + return; + } + [super setImageURL:imageURL successBlock:successBlock failureBlock:failureBlock]; + if (self.showActivityIndicator && !self.image && imageURL) + { + if (self.activityView == nil) + { + self.activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:self.activityIndicatorStyle]; + self.activityView.hidesWhenStopped = YES; + self.activityView.center = CGPointMake(self.bounds.size.width / 2.0f, self.bounds.size.height / 2.0f); + self.activityView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + [self addSubview:self.activityView]; + } + [self.activityView startAnimating]; + } +} + - (void)setImageURL:(NSURL *)imageURL { UIImage *image = [[AsyncImageLoader sharedLoader].cache objectForKey:imageURL]; @@ -647,9 +789,9 @@ - (void)setImageURL:(NSURL *)imageURL - (void)setActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style { - _activityIndicatorStyle = style; - [self.activityView removeFromSuperview]; - self.activityView = nil; + _activityIndicatorStyle = style; + [self.activityView removeFromSuperview]; + self.activityView = nil; } - (void)setImage:(UIImage *)image