Up until recently I haven’t had the need to load images into a table view from the web. I will be showing two very useful items to add to your arsenal. That is if you don’t already know this.
One very useful concept in Objective-C is Categories. In short Categories can allow you to extend existing classes. For instance I created a Category that Loads an Image from the web, I extended the NSString class. Instead of a couple lines of code to get the image I now have it in one.
Example:
UIImage *img = [_urlString getImageFromWeb];
Instead of:
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:_str]];
UIImage *image = [UIImage imageWithData:data];
And that is not including any error checking so, it feels much cleaner and quicker to simply create a Category.
Creating a Category is Simple.
“NSString+Categories.h”
@interface NSString (CustomName)
- (UIImage*) getImageFromWeb;
@end
“NSString+Categories.m”
@implementation NSString (CustomName)
- (UIImage*) getImageFromWeb
{
NSString *_str = self;
UIImage *image = nil;
@try {
if ([_str rangeOfString:@"http"].location != NSNotFound)
{
NSData *data = [NSData dataWithContentsOfURL: [NSURL URLWithString:_str]];
image = [UIImage imageWithData:data];
}
}
@catch (NSException * e) {
image = nil;
}
return image;
}
@end
Now you can load an Image in one line!
Now for the fun control! Creating a views loads images from the web without slowing down user interaction is pretty easy. I will be showing you code that I’m currently using in my project so I’m sure you will want to change the naming of the control ect.
Lets start out with the header.
@protocol IFUIImageLoadViewDelegate;
@interface IFUIImageLoadView : UIView
{
UIActivityIndicatorView *aIndicator;
UIImageView *imageView;
id object;
id <IFUIImageLoadViewDelegate> delegate;
}
@property (nonatomic, retain) UIActivityIndicatorView *aIndicator;
@property (nonatomic, retain) UIImageView *imageView;
@property (nonatomic, assign) id <IFUIImageLoadViewDelegate> delegate;
@property (nonatomic, assign) id object;
- (id)initWithFrame:(CGRect)frame imgURLString:(NSString*) _url object:(id) _obj image:(UIImage*) _image;
- (void) downloadImageOnThread:(NSString*) _urlString;
- (void) updateImageView:(UIImage*) _img;
@end
@protocol IFUIImageLoadViewDelegate <NSObject>
@optional
- (void) imageLoadViewFinishedWithImage:(UIImage*) _image forObject:(id) _obj;
@end
This class we are writing is basically just a standard view. I think it looks nice to have the activity indicator spinning while the images are loading so we added that, although this is not necessary. I also have an “object” property which allows me to pass one of the objects that I will be assigning the image to.
Passing in the Object allows me to call the delegate method with the object and the image and store the image. This is very useful in my case, because the user will be downloading the images with the Sets from this web service. Doing this allows the images to only be downloaded once. Also I’m sure you have noticed that the UITableViewCells when scrolled are not just kept in memory they will be reloaded, so we do not want to load these images multiple times if it is not necessary.
The Init for this view we are passing the frame, image string, object we will be assigning the image to and an UIImage Object. The reason we would like to pass an Image to this, is if the image has already been cached in memory we don’t want to download the image again.
.m File:
@implementation IFUIImageLoadView
@synthesize aIndicator, imageView, object, delegate;
- (id)initWithFrame:(CGRect)frame imgURLString:(NSString*) _url object:(id) _obj image:(UIImage*) _image {
if ((self = [super initWithFrame:frame])) {
self.backgroundColor = [UIColor blackColor];
self.layer.cornerRadius = 5.0;
self.layer.borderColor = [[UIColor whiteColor] CGColor];
self.layer.borderWidth = 2.0;
self.object = _obj;
UIImageView *imgV = [[UIImageView alloc] initWithFrame:CGRectMake(2, 2, self.frame.size.width - 4, self.frame.size.width - 4)];
imgV.backgroundColor = [UIColor clearColor];
self.imageView = imgV;
[self addSubview:imgV];
[imgV release];
if (_image == nil)
{
UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
activity.frame = CGRectMake(self.center.x - (activity.frame.size.width/2), self.center.y - (activity.frame.size.height/2), activity.frame.size.width, activity.frame.size.height);
activity.hidesWhenStopped = YES;
self.aIndicator = activity;
[self addSubview:activity];
[activity release];
[self.aIndicator startAnimating];
[NSThread detachNewThreadSelector:@selector(downloadImageOnThread:) toTarget:self withObject:_url];
}
else
{
self.imageView.image = _image;
}
}
return self;
}
In the init method, I am setting up the view’s styling, UIImageView Property, and the activity Indicator. This is where the UIImage being passed comes in handy, basically if the UIImage you are passing is not nil then use the image vs loading the image from the web.
The next method we want to implement is loading the Image on a Separate thread. Doing so Will not lock up the UI. Look how quick, clean, and easy the NSString category we implemented was!
- (void) downloadImageOnThread:(NSString*) _urlString
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *img = [_urlString getImageFromWeb];
[self performSelectorOnMainThread:@selector(updateImageView:) withObject:img waitUntilDone:YES];
[pool release];
}
- (void) updateImageView:(UIImage*) _img
{
self.imageView.image = _img;
[self.aIndicator stopAnimating];
if ([self.delegate respondsToSelector:@selector(imageLoadViewFinishedWithImage:forObject:)])
[self.delegate imageLoadViewFinishedWithImage:_img forObject:self.object];
}
- (void)dealloc {
delegate = nil;
object = nil;
[imageView release], imageView = nil;
[aIndicator release], aIndicator = nil;
[super dealloc];
}