/SPHChatBubble/AsyncImageView.m
Objective C | 709 lines | 556 code | 90 blank | 63 comment | 84 complexity | c94c474ca6e41663f838fb2f19f7fce6 MD5 | raw file
- //
- // AsyncImageView.m
- //
- // Version 1.4
- //
- // Created by Nick Lockwood on 03/04/2011.
- // Copyright (c) 2011 Charcoal Design
- //
- // Distributed under the permissive zlib License
- // Get the latest version from either of these locations:
- //
- // http://charcoaldesign.co.uk/source/cocoa#asyncimageview
- // https://github.com/nicklockwood/AsyncImageView
- //
- // This software is provided 'as-is', without any express or implied
- // warranty. In no event will the authors be held liable for any damages
- // arising from the use of this software.
- //
- // Permission is granted to anyone to use this software for any purpose,
- // including commercial applications, and to alter it and redistribute it
- // freely, subject to the following restrictions:
- //
- // 1. The origin of this software must not be misrepresented; you must not
- // claim that you wrote the original software. If you use this software
- // in a product, an acknowledgment in the product documentation would be
- // appreciated but is not required.
- //
- // 2. Altered source versions must be plainly marked as such, and must not be
- // misrepresented as being the original software.
- //
- // 3. This notice may not be removed or altered from any source distribution.
- //
- #import "AsyncImageView.h"
- #import <objc/message.h>
- NSString *const AsyncImageLoadDidFinish = @"AsyncImageLoadDidFinish";
- NSString *const AsyncImageLoadDidFail = @"AsyncImageLoadDidFail";
- NSString *const AsyncImageTargetReleased = @"AsyncImageTargetReleased";
- NSString *const AsyncImageImageKey = @"image";
- NSString *const AsyncImageURLKey = @"URL";
- NSString *const AsyncImageCacheKey = @"cache";
- NSString *const AsyncImageErrorKey = @"error";
- @interface AsyncImageConnection : NSObject
- @property (nonatomic, strong) NSURLConnection *connection;
- @property (nonatomic, strong) NSMutableData *data;
- @property (nonatomic, strong) NSURL *URL;
- @property (nonatomic, strong) NSCache *cache;
- @property (nonatomic, strong) id target;
- @property (nonatomic, assign) SEL success;
- @property (nonatomic, assign) SEL failure;
- @property (nonatomic, readonly, getter = isLoading) BOOL loading;
- @property (nonatomic, readonly) BOOL cancelled;
- - (AsyncImageConnection *)initWithURL:(NSURL *)URL
- cache:(NSCache *)cache
- target:(id)target
- success:(SEL)success
- failure:(SEL)failure;
- - (void)start;
- - (void)cancel;
- - (BOOL)isInCache;
- @end
- @implementation AsyncImageConnection
- @synthesize connection = _connection;
- @synthesize data = _data;
- @synthesize URL = _URL;
- @synthesize cache = _cache;
- @synthesize target = _target;
- @synthesize success = _success;
- @synthesize failure = _failure;
- @synthesize loading = _loading;
- @synthesize cancelled = _cancelled;
- - (AsyncImageConnection *)initWithURL:(NSURL *)URL
- cache:(NSCache *)cache
- target:(id)target
- success:(SEL)success
- failure:(SEL)failure
- {
- if ((self = [self init]))
- {
- self.URL = URL;
- self.cache = cache;
- self.target = target;
- self.success = success;
- self.failure = failure;
- }
- return self;
- }
- - (UIImage *)cachedImage
- {
- if ([_URL isFileURL])
- {
- NSString *path = [[_URL absoluteURL] path];
- NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
- if ([path hasPrefix:resourcePath])
- {
- return [UIImage imageNamed:[path substringFromIndex:[resourcePath length]]];
- }
- }
- return [_cache objectForKey:_URL];
- }
- - (BOOL)isInCache
- {
- return [self cachedImage] != nil;
- }
- - (void)loadFailedWithError:(NSError *)error
- {
- _loading = NO;
- _cancelled = NO;
- [[NSNotificationCenter defaultCenter] postNotificationName:AsyncImageLoadDidFail
- object:_target
- userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
- _URL, AsyncImageURLKey,
- error, AsyncImageErrorKey,
- nil]];
- }
- - (void)cacheImage:(UIImage *)image
- {
- if (!_cancelled)
- {
- if (image && _URL)
- {
- BOOL storeInCache = YES;
- if ([_URL isFileURL])
- {
- if ([[[_URL absoluteURL] path] hasPrefix:[[NSBundle mainBundle] resourcePath]])
- {
- //do not store in cache
- storeInCache = NO;
- }
- }
- if (storeInCache)
- {
- [_cache setObject:image forKey:_URL];
- }
- }
-
- NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
- image, AsyncImageImageKey,
- _URL, AsyncImageURLKey,
- nil];
- if (_cache)
- {
- [userInfo setObject:_cache forKey:AsyncImageCacheKey];
- }
-
- _loading = NO;
- [[NSNotificationCenter defaultCenter] postNotificationName:AsyncImageLoadDidFinish
- object:_target
- userInfo:[[userInfo copy] autorelease]];
- }
- else
- {
- _loading = NO;
- _cancelled = NO;
- }
- }
- - (void)processDataInBackground:(NSData *)data
- {
- @synchronized ([self class])
- {
- if (!_cancelled)
- {
- UIImage *image = [[UIImage alloc] initWithData:data];
- if (image)
- {
- //add to cache (may be cached already but it doesn't matter)
- [self performSelectorOnMainThread:@selector(cacheImage:)
- withObject:image
- waitUntilDone:YES];
- [image release];
- }
- else
- {
- @autoreleasepool
- {
- NSError *error = [NSError errorWithDomain:@"AsyncImageLoader" code:0 userInfo:[NSDictionary dictionaryWithObject:@"Invalid image data" forKey:NSLocalizedDescriptionKey]];
- [self performSelectorOnMainThread:@selector(loadFailedWithError:) withObject:error waitUntilDone:YES];
- }
- }
- }
- else
- {
- //clean up
- [self performSelectorOnMainThread:@selector(cacheImage:)
- withObject:nil
- waitUntilDone:YES];
- }
- }
- }
- - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
- {
- self.data = [NSMutableData data];
- }
- - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
- {
- //add data
- [_data appendData:data];
- }
- - (void)connectionDidFinishLoading:(NSURLConnection *)connection
- {
- [self performSelectorInBackground:@selector(processDataInBackground:) withObject:_data];
- self.connection = nil;
- self.data = nil;
- }
- - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
- {
- self.connection = nil;
- self.data = nil;
- [self loadFailedWithError:error];
- }
- - (void)start
- {
- if (_loading && !_cancelled)
- {
- return;
- }
-
- //begin loading
- _loading = YES;
- _cancelled = NO;
-
- //check for nil URL
- if (_URL == nil)
- {
- [self cacheImage:nil];
- return;
- }
-
- //check for cached image
- UIImage *image = [self cachedImage];
- if (image)
- {
- //add to cache (cached already but it doesn't matter)
- [self performSelectorOnMainThread:@selector(cacheImage:)
- withObject:image
- waitUntilDone:NO];
- return;
- }
-
- //begin load
- NSURLRequest *request = [NSURLRequest requestWithURL:_URL
- cachePolicy:NSURLCacheStorageNotAllowed
- timeoutInterval:[AsyncImageLoader sharedLoader].loadingTimeout];
-
- _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
- [_connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- [_connection start];
- }
- - (void)cancel
- {
- _cancelled = YES;
- [_connection cancel];
- self.connection = nil;
- self.data = nil;
- }
- - (void)dealloc
- {
- [_connection release];
- [_data release];
- [_URL release];
- [_target release];
- [super ah_dealloc];
- }
- @end
- @interface AsyncImageLoader ()
- @property (nonatomic, strong) NSMutableArray *connections;
- @end
- @implementation AsyncImageLoader
- @synthesize cache = _cache;
- @synthesize connections = _connections;
- @synthesize concurrentLoads = _concurrentLoads;
- @synthesize loadingTimeout = _loadingTimeout;
- + (AsyncImageLoader *)sharedLoader
- {
- static AsyncImageLoader *sharedInstance = nil;
- if (sharedInstance == nil)
- {
- sharedInstance = [[self alloc] init];
- }
- return sharedInstance;
- }
- + (NSCache *)defaultCache
- {
- static NSCache *sharedInstance = nil;
- if (sharedInstance == nil)
- {
- sharedInstance = [[NSCache alloc] init];
- }
- return sharedInstance;
- }
- - (AsyncImageLoader *)init
- {
- if ((self = [super init]))
- {
- self.cache = [[self class] defaultCache];
- _concurrentLoads = 2;
- _loadingTimeout = 60.0;
- _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];
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(targetReleased:)
- name:AsyncImageTargetReleased
- object:nil];
- }
- return self;
- }
- - (void)updateQueue
- {
- //start connections
- NSUInteger count = 0;
- for (AsyncImageConnection *connection in _connections)
- {
- if (![connection isLoading])
- {
- if ([connection isInCache])
- {
- [connection start];
- }
- else if (count < _concurrentLoads)
- {
- count ++;
- [connection start];
- }
- }
- }
- }
- - (void)imageLoaded:(NSNotification *)notification
- {
- //complete connections for URL
- NSURL *URL = [notification.userInfo objectForKey:AsyncImageURLKey];
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if (connection.URL == URL || [connection.URL isEqual:URL])
- {
- //cancel earlier connections for same target/action
- for (int j = i - 1; j >= 0; j--)
- {
- AsyncImageConnection *earlier = [_connections objectAtIndex:(NSUInteger)j];
- if (earlier.target == connection.target &&
- earlier.success == connection.success)
- {
- [earlier cancel];
- [_connections removeObjectAtIndex:(NSUInteger)j];
- i--;
- }
- }
-
- //cancel connection (in case it's a duplicate)
- [connection cancel];
-
- //perform action
- UIImage *image = [notification.userInfo objectForKey:AsyncImageImageKey];
- objc_msgSend(connection.target, connection.success, image, connection.URL);
- //remove from queue
- [_connections removeObjectAtIndex:(NSUInteger)i];
- }
- }
-
- //update the queue
- [self updateQueue];
- }
- - (void)imageFailed:(NSNotification *)notification
- {
- //remove connections for URL
- NSURL *URL = [notification.userInfo objectForKey:AsyncImageURLKey];
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if ([connection.URL isEqual:URL])
- {
- //cancel connection (in case it's a duplicate)
- [connection cancel];
-
- //perform failure action
- if (connection.failure)
- {
- NSError *error = [notification.userInfo objectForKey:AsyncImageErrorKey];
- objc_msgSend(connection.target, connection.failure, error, URL);
- }
-
- //remove from queue
- [_connections removeObjectAtIndex:(NSUInteger)i];
- }
- }
-
- //update the queue
- [self updateQueue];
- }
- - (void)targetReleased:(NSNotification *)notification
- {
- //remove connections for URL
- id target = [notification object];
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if (connection.target == target)
- {
- //cancel connection
- [connection cancel];
- [_connections removeObjectAtIndex:(NSUInteger)i];
- }
- }
-
- //update the queue
- [self updateQueue];
- }
- - (void)loadImageWithURL:(NSURL *)URL target:(id)target success:(SEL)success failure:(SEL)failure
- {
- //check cache
- UIImage *image = [_cache objectForKey:URL];
- if (image)
- {
- [self cancelLoadingImagesForTarget:self action:success];
- if (success) [target performSelectorOnMainThread:success withObject:image waitUntilDone:NO];
- return;
- }
-
- //create new connection
- AsyncImageConnection *connection = [[AsyncImageConnection alloc] initWithURL:URL
- cache:_cache
- target:target
- success:success
- failure:failure];
- BOOL added = NO;
- for (NSUInteger i = 0; i < [_connections count]; i++)
- {
- AsyncImageConnection *existingConnection = [_connections objectAtIndex:i];
- if (!existingConnection.loading)
- {
- [_connections insertObject:connection atIndex:i];
- added = YES;
- break;
- }
- }
- if (!added)
- {
- [_connections addObject:connection];
- }
-
- [connection release];
- [self updateQueue];
- }
- - (void)loadImageWithURL:(NSURL *)URL target:(id)target action:(SEL)action
- {
- [self loadImageWithURL:URL target:target success:action failure:NULL];
- }
- - (void)loadImageWithURL:(NSURL *)URL
- {
- [self loadImageWithURL:URL target:nil success:NULL failure:NULL];
- }
- - (void)cancelLoadingURL:(NSURL *)URL target:(id)target action:(SEL)action
- {
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if ([connection.URL isEqual:URL] && connection.target == target && connection.success == action)
- {
- [connection cancel];
- [_connections removeObjectAtIndex:(NSUInteger)i];
- }
- }
- }
- - (void)cancelLoadingURL:(NSURL *)URL target:(id)target
- {
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if ([connection.URL isEqual:URL] && connection.target == target)
- {
- [connection cancel];
- [_connections removeObjectAtIndex:(NSUInteger)i];
- }
- }
- }
- - (void)cancelLoadingURL:(NSURL *)URL
- {
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if ([connection.URL isEqual:URL])
- {
- [connection cancel];
- [_connections removeObjectAtIndex:(NSUInteger)i];
- }
- }
- }
- - (void)cancelLoadingImagesForTarget:(id)target action:(SEL)action
- {
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if (connection.target == target && connection.success == action)
- {
- [connection cancel];
- }
- }
- }
- - (void)cancelLoadingImagesForTarget:(id)target
- {
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if (connection.target == target)
- {
- [connection cancel];
- }
- }
- }
- - (NSURL *)URLForTarget:(id)target action:(SEL)action
- {
- //return the most recent image URL assigned to the target for the given action
- //this is not neccesarily the next image that will be assigned
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if (connection.target == target && connection.success == action)
- {
- return [[connection.URL ah_retain] autorelease];
- }
- }
- return nil;
- }
- - (NSURL *)URLForTarget:(id)target
- {
- //return the most recent image URL assigned to the target
- //this is not neccesarily the next image that will be assigned
- for (int i = (int)[_connections count] - 1; i >= 0; i--)
- {
- AsyncImageConnection *connection = [_connections objectAtIndex:(NSUInteger)i];
- if (connection.target == target)
- {
- return [[connection.URL ah_retain] autorelease];
- }
- }
- return nil;
- }
- - (void)dealloc
- {
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [_cache release];
- [_connections release];
- [super ah_dealloc];
- }
- @end
- @implementation UIImageView(AsyncImageView)
- - (void)setImageURL:(NSURL *)imageURL
- {
- [[AsyncImageLoader sharedLoader] loadImageWithURL:imageURL target:self action:@selector(setImage:)];
- }
- - (NSURL *)imageURL
- {
- return [[AsyncImageLoader sharedLoader] URLForTarget:self action:@selector(setImage:)];
- }
- @end
- @interface AsyncImageView ()
- @property (nonatomic, strong) UIActivityIndicatorView *activityView;
- @end
- @implementation AsyncImageView
- @synthesize showActivityIndicator = _showActivityIndicator;
- @synthesize activityIndicatorStyle = _activityIndicatorStyle;
- @synthesize crossfadeImages = _crossfadeImages;
- @synthesize crossfadeDuration = _crossfadeDuration;
- @synthesize activityView = _activityView;
- - (void)setUp
- {
- _showActivityIndicator = (self.image == nil);
- _activityIndicatorStyle = UIActivityIndicatorViewStyleGray;
- _crossfadeImages = YES;
- _crossfadeDuration = 0.4;
- }
- - (id)initWithFrame:(CGRect)frame
- {
- if ((self = [super initWithFrame:frame]))
- {
- [self setUp];
- }
- return self;
- }
- - (id)initWithCoder:(NSCoder *)aDecoder
- {
- if ((self = [super initWithCoder:aDecoder]))
- {
- [self setUp];
- }
- return self;
- }
- - (void)setImageURL:(NSURL *)imageURL
- {
- super.imageURL = imageURL;
- if (_showActivityIndicator && !self.image && imageURL)
- {
- if (_activityView == nil)
- {
- _activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:_activityIndicatorStyle];
- _activityView.hidesWhenStopped = YES;
- _activityView.center = CGPointMake(self.bounds.size.width / 2.0f, self.bounds.size.height / 2.0f);
- _activityView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
- [self addSubview:_activityView];
- }
- [_activityView startAnimating];
- }
- }
- - (void)setActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style
- {
- _activityIndicatorStyle = style;
- [_activityView removeFromSuperview];
- self.activityView = nil;
- }
- - (void)setImage:(UIImage *)image
- {
- if (_crossfadeImages)
- {
- //implement crossfade transition without needing to import QuartzCore
- id animation = objc_msgSend(NSClassFromString(@"CATransition"), @selector(animation));
- objc_msgSend(animation, @selector(setType:), @"kCATransitionFade");
- objc_msgSend(animation, @selector(setDuration:), _crossfadeDuration);
- objc_msgSend(self.layer, @selector(addAnimation:forKey:), animation, nil);
- }
- super.image = image;
- [_activityView stopAnimating];
- }
- - (void)dealloc
- {
- [[AsyncImageLoader sharedLoader] cancelLoadingURL:self.imageURL target:self];
- [_activityView release];
- [super ah_dealloc];
- }
- @end