diff --git a/README.md b/README.md index 16b07c2..1015481 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,12 @@ This library provides a category for UIImageVIew with support for remote images It provides: -- Drop-in replacement to UIImageView -- Asynchronous image downloader -- Asynchronous memory + disk image caching with automatic cache expiration handling +- An UIImageView category adding web image and cache management to the Cocoa Touch framework +- An asynchronous image downloader using threads (NSOperation) +- An asynchronous memory + disk image caching with automatic cache expiration handling +- A garantie that the same URL won't be downloaded several times +- A garantie that bogus URLs won't be retried again and again +- Performances! Motivation ---------- @@ -59,11 +62,9 @@ How To Use It ### Using UIImageView+WebCache category with UITableView -Just #import the UIImageView+WebCache.h header, and call the setImageWithURL: method from the -tableView:cellForRowAtIndexPath: UITableViewDataSource method. Everything will be handled for you, -from parallel downloads to caching management. If you assigned an image to the view (via the -`images` property), this image will be used as a placeholder, waiting for the web image to be -loaded. +Just #import the UIImageView+WebCache.h header, and call the setImageWithURL:placeholderImage: +method from the tableView:cellForRowAtIndexPath: UITableViewDataSource method. Everything will be +handled for you, from parallel downloads to caching management. #import "UIImageView+WebCache.h" @@ -79,31 +80,58 @@ loaded. { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease]; - - // Here we set the placeholder image - cell.imageView.image = [UIImage imageNamed:@"placeholder.png"]; } // Here we use the new provided setImageWithURL: method to load the web image - [cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]]; + [cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] + placeholderImage:[UIImage imageNamed:@"placeholder.png"]]; cell.textLabel.text = @"My Text"; return cell; } -### Asynchronous Image Downloader +### Using SDWebImageManager + +The SDWebImageManager is the class behind the UIImageView+WebCache category. It ties the +asynchronous downloader with the image cache store. You can use this classe directly to benefits +from web image downloading with caching in another context than a UIView (ie: with Cocos). + +Here is a simple example of how to use SDWebImageManager: + + SDWebImageManager *manager = [SDWebImageManager sharedManager]; + + UIImage *cachedImage = [manager imageWithURL:url]; + + if (cachedImage) + { + // Use the cached image immediatly + } + else + { + // Start an async download + [manager downloadWithURL:url delegate:self]; + } + +Your class will have to implement the SDWebImageManagerDelegate protocol, and to implement the +imageHelper:didFinishWithImage: method from this protocol: + + - (void)imageHelper:(SDWebImageManager *)imageHelper didFinishWithImage:(UIImage *)image + { + // Do something with the downloaded image + } + +### Using Asynchronous Image Downloader Independently It is possible to use the NSOperation based image downloader independently. Just create an instance of SDWebImageDownloader using its convenience constructor downloaderWithURL:target:action:. - downloader = [SDWebImageDownloader downloaderWithURL:url - target:self - action:@selector(downloadFinishedWithImage:)]; + downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self]; -The download will by queued immediately and the downloadFinishedWithImage: method will be called as -soon as the download of image will be completed (prepare not to be called from the main thread). +The download will by queued immediately and the imageDownloader:didFinishWithImage: method from the +SDWebImageDownloaderDelegate protocol will be called as soon as the download of image is completed +(prepare not to be called from the main thread). -### Asynchronous Image Caching +### Using Asynchronous Image Caching Independently It is also possible to use the NSOperation based image cache store independently. SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed @@ -134,10 +162,8 @@ third argument. Future Enhancements ------------------- -- Easy way to use it with default UITableView styles without requiring to create a custom UITableViewCell - LRU memory cache cleanup instead of reset on memory warning - [Dailymotion]: http://www.dailymotion.com [Fraggle]: http://fraggle.squarespace.com [Urban Rivals]: http://fraggle.squarespace.com/blog/2009/9/15/almost-done-here-is-urban-rivals-iphone-trailer.html diff --git a/SDWebImageDownloader.h b/SDWebImageDownloader.h index 5d20d28..2608796 100644 --- a/SDWebImageDownloader.h +++ b/SDWebImageDownloader.h @@ -7,19 +7,18 @@ */ #import +#import "SDWebImageDownloaderDelegate.h" @interface SDWebImageDownloader : NSOperation { NSURL *url; - id target; - SEL action; + id delegate; } @property (retain) NSURL *url; -@property (assign) id target; -@property (assign) SEL action; +@property (assign) id delegate; -+ (id)downloaderWithURL:(NSURL *)url target:(id)target action:(SEL)action; ++ (id)downloaderWithURL:(NSURL *)url delegate:(id)delegate; + (void)setMaxConcurrentDownloads:(NSUInteger)max; @end diff --git a/SDWebImageDownloader.m b/SDWebImageDownloader.m index 384764b..e7d9e3f 100644 --- a/SDWebImageDownloader.m +++ b/SDWebImageDownloader.m @@ -12,7 +12,7 @@ static NSOperationQueue *downloadQueue; @implementation SDWebImageDownloader -@synthesize url, target, action; +@synthesize url, delegate; - (void)dealloc { @@ -20,12 +20,11 @@ static NSOperationQueue *downloadQueue; [super dealloc]; } -+ (id)downloaderWithURL:(NSURL *)url target:(id)target action:(SEL)action ++ (id)downloaderWithURL:(NSURL *)url delegate:(id)delegate { SDWebImageDownloader *downloader = [[[SDWebImageDownloader alloc] init] autorelease]; downloader.url = url; - downloader.target = target; - downloader.action = action; + downloader.delegate = delegate; if (downloadQueue == nil) { @@ -54,9 +53,9 @@ static NSOperationQueue *downloadQueue; UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]]; - if (!self.isCancelled) + if (!self.isCancelled && [delegate respondsToSelector:@selector(imageDownloader:didFinishWithImage:)]) { - [target performSelector:action withObject:image]; + [delegate performSelector:@selector(imageDownloader:didFinishWithImage:) withObject:self withObject:image]; } [pool release]; diff --git a/SDWebImageDownloaderDelegate.h b/SDWebImageDownloaderDelegate.h new file mode 100644 index 0000000..f5281ac --- /dev/null +++ b/SDWebImageDownloaderDelegate.h @@ -0,0 +1,17 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@class SDWebImageDownloader; + +@protocol SDWebImageDownloaderDelegate + +@optional + +- (void)imageDownloader:(SDWebImageDownloader *)downloader didFinishWithImage:(UIImage *)image; + +@end \ No newline at end of file diff --git a/SDWebImageHelperDelegate.h b/SDWebImageHelperDelegate.h new file mode 100644 index 0000000..2ec2c12 --- /dev/null +++ b/SDWebImageHelperDelegate.h @@ -0,0 +1,17 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@class SDWebImageManager; + +@protocol SDWebImageManagerDelegate + +@optional + +- (void)imageHelper:(SDWebImageManager *)imageHelper didFinishWithImage:(UIImage *)image; + +@end diff --git a/SDWebImageManager.h b/SDWebImageManager.h new file mode 100644 index 0000000..065711f --- /dev/null +++ b/SDWebImageManager.h @@ -0,0 +1,26 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import +#import "SDWebImageDownloaderDelegate.h" +#import "SDWebImageHelperDelegate.h" + +@interface SDWebImageManager : NSObject +{ + NSMutableArray *delegates; + NSMutableArray *downloaders; + NSMutableDictionary *downloaderForURL; + NSMutableArray *failedURLs; +} + ++ (id)sharedManager; +- (UIImage *)imageWithURL:(NSURL *)url; +- (void)downloadWithURL:(NSURL *)url delegate:(id)delegate; +- (void)cancelForDelegate:(id)delegate; + +@end diff --git a/SDWebImageManager.m b/SDWebImageManager.m new file mode 100644 index 0000000..6a08bdf --- /dev/null +++ b/SDWebImageManager.m @@ -0,0 +1,148 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "SDWebImageManager.h" +#import "SDImageCache.h" +#import "SDWebImageDownloader.h" + +static SDWebImageManager *instance; + +@implementation SDWebImageManager + +- (id)init +{ + if (self = [super init]) + { + delegates = [[NSMutableArray alloc] init]; + downloaders = [[NSMutableArray alloc] init]; + downloaderForURL = [[NSMutableDictionary alloc] init]; + failedURLs = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void) dealloc +{ + [delegates release]; + [downloaders release]; + [downloaderForURL release]; + [failedURLs release]; + [super dealloc]; +} + + ++ (id)sharedManager +{ + if (instance == nil) + { + instance = [[SDWebImageManager alloc] init]; + } + + return instance; +} + +- (UIImage *)imageWithURL:(NSURL *)url +{ + return [[SDImageCache sharedImageCache] imageFromKey:[url absoluteString]]; +} + +- (void)downloadWithURL:(NSURL *)url delegate:(id)delegate +{ + if ([failedURLs containsObject:url]) + { + return; + } + + // Share the same downloader for identical URLs so we don't download the same URL several times + SDWebImageDownloader *downloader = [downloaderForURL objectForKey:url]; + + if (!downloader) + { + downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self]; + [downloaderForURL setObject:downloader forKey:url]; + } + + @synchronized(self) + { + [delegates addObject:delegate]; + [downloaders addObject:downloader]; + } +} + +- (void)cancelForDelegate:(id)delegate +{ + @synchronized(self) + { + NSUInteger index = [delegates indexOfObjectIdenticalTo:delegate]; + + if (index == NSNotFound) + { + return; + } + + SDWebImageDownloader *downloader = [[downloaders objectAtIndex:index] retain]; + + [delegates removeObjectAtIndex:index]; + [downloaders removeObjectAtIndex:index]; + + if (![downloaders containsObject:downloader]) + { + NSLog(@"cancel download"); + // No more delegate are waiting for this download, cancel it + [downloader cancel]; + [downloaderForURL removeObjectForKey:downloader.url]; + } + + [downloader release]; + } +} + +- (void)imageDownloader:(SDWebImageDownloader *)downloader didFinishWithImage:(UIImage *)image +{ + [downloader retain]; + + @synchronized(self) + { + // Notify all the delegates with this downloader + for (NSInteger index = [downloaders count] - 1; index >= 0; index--) + { + SDWebImageDownloader *aDownloader = [downloaders objectAtIndex:index]; + if (aDownloader == downloader) + { + id delegate = [delegates objectAtIndex:index]; + + if (image && [delegate respondsToSelector:@selector(imageHelper:didFinishWithImage:)]) + { + [delegate performSelector:@selector(imageHelper:didFinishWithImage:) withObject:self withObject:image]; + } + + [downloaders removeObjectAtIndex:index]; + [delegates removeObjectAtIndex:index]; + } + } + } + + if (image) + { + // Store the image in the cache + [[SDImageCache sharedImageCache] storeImage:image forKey:[downloader.url absoluteString]]; + } + else + { + // The image can't be downloaded from this URL, mark the URL as failed so we won't try and fail again and again + [failedURLs addObject:downloader.url]; + } + + + // Release the downloader + [downloaderForURL removeObjectForKey:downloader.url]; + [downloader release]; +} + + +@end diff --git a/UIImageView+WebCache.h b/UIImageView+WebCache.h index a93a446..b604327 100644 --- a/UIImageView+WebCache.h +++ b/UIImageView+WebCache.h @@ -7,9 +7,11 @@ */ #import +#import "SDWebImageHelperDelegate.h" -@interface UIImageView (WebCache) +@interface UIImageView (WebCache) - (void)setImageWithURL:(NSURL *)url; +- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; @end diff --git a/UIImageView+WebCache.m b/UIImageView+WebCache.m index ede454f..731d663 100644 --- a/UIImageView+WebCache.m +++ b/UIImageView+WebCache.m @@ -7,48 +7,42 @@ */ #import "UIImageView+WebCache.h" -#import "UIImageViewHelper.h" +#import "SDWebImageManager.h" @implementation UIImageView (WebCache) - (void)setImageWithURL:(NSURL *)url { - UIImageViewHelper *helper = nil; + [self setImageWithURL:url placeholderImage:nil]; +} - if ([self.subviews count] > 0) - { - helper = [self.subviews objectAtIndex:0]; - } - - if (helper == nil) - { - helper = [[[UIImageViewHelper alloc] initWithDelegate:self] autorelease]; - [self addSubview:helper]; - } +- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder +{ + SDWebImageManager *manager = [SDWebImageManager sharedManager]; // Remove in progress downloader from queue - [helper cancel]; - - // Save the placeholder image in order to re-apply it when view is reused - if (helper.placeHolderImage == nil) - { - helper.placeHolderImage = self.image; - } - else - { - self.image = helper.placeHolderImage; - } - - UIImage *cachedImage = [helper imageWithURL:url]; - + [manager cancelForDelegate:self]; + + UIImage *cachedImage = [manager imageWithURL:url]; + if (cachedImage) { self.image = cachedImage; } else { - [helper downloadWithURL:url]; + if (placeholder) + { + self.image = placeholder; + } + + [manager downloadWithURL:url delegate:self]; } } +- (void)imageHelper:(SDWebImageManager *)imageHelper didFinishWithImage:(UIImage *)image +{ + self.image = image; +} + @end \ No newline at end of file diff --git a/UIImageViewHelper.h b/UIImageViewHelper.h deleted file mode 100644 index 6a3a7bf..0000000 --- a/UIImageViewHelper.h +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This file is part of the SDWebImage package. - * (c) Olivier Poitrey - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -#import -#import "SDWebImageDownloader.h" - -@interface UIImageViewHelper : UIView -{ - UIImageView *delegate; - SDWebImageDownloader *downloader; - UIImage *placeHolderImage; -} - -@property (nonatomic, retain) UIImage *placeHolderImage; - -- (id)initWithDelegate:(UIImageView *)aDelegate; -- (UIImage *)imageWithURL:(NSURL *)url; -- (void)downloadWithURL:(NSURL *)url; -- (void)cancel; - -@end diff --git a/UIImageViewHelper.m b/UIImageViewHelper.m deleted file mode 100644 index 56fa9aa..0000000 --- a/UIImageViewHelper.m +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is part of the SDWebImage package. - * (c) Olivier Poitrey - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -#import "UIImageViewHelper.h" -#import "SDImageCache.h" - -@implementation UIImageViewHelper - -@synthesize placeHolderImage; - -- (id)initWithDelegate:(UIImageView *)aDelegate -{ - if (self = [super init]) - { - delegate = aDelegate; - self.hidden = YES; - } - return self; -} - -- (UIImage *)imageWithURL:(NSURL *)url -{ - return [[SDImageCache sharedImageCache] imageFromKey:[url absoluteString]]; -} - -- (void)downloadWithURL:(NSURL *)url -{ - downloader = [[SDWebImageDownloader downloaderWithURL:url target:self action:@selector(downloadFinishedWithImage:)] retain]; -} - -- (void)cancel -{ - [downloader cancel]; - [downloader release]; - downloader = nil; -} - -- (void)downloadFinishedWithImage:(UIImage *)anImage -{ - // Apply image to the underlaying UIImageView - delegate.image = anImage; - - // Store the image in the cache - [[SDImageCache sharedImageCache] storeImage:anImage forKey:[downloader.url absoluteString]]; - - // Free the downloader - [downloader release]; - downloader = nil; -} - -@end \ No newline at end of file