リモート画像を取得してUIImageViewを作成する

概要

MWPhotoBrowserというとてもシャープに動作するPhotoBrowserがあるのですが、こいつの仕組みを調査してみるために簡単な箇所だけ抜き出して実装してみたというお話です。

https://github.com/mwaterfall/MWPhotoBrowser

実装内容

  • ImageViewを内包したMyPhotoクラスを準備
  • [MyPhoto image]を実行するとisAvailableの場合は画像を返す。
  • そうでない場合はリモート画像を非同期通信にて取得開始。
  • ひとまずスピナーを表示する
  • BackGroundで画像取得処理が終了する。
  • MyPhotoはdelegateオブジェクトに対してphotoDidFinishLoadingかphotoDidFailToLoadへの通知を行う。
  • 画像が取得できている場合は画像を再描画

コード

model
// MyPhoto.h
#import <Foundation/Foundation.h>
@class MyPhoto;

// Delegate
@protocol MyPhotoDelegate <NSObject>
- (void)photoDidFinishLoading:(MyPhoto *)photo;
- (void)photoDidFailToLoad:(MyPhoto *)photo;
@end

@interface MyPhoto : NSObject {
	NSURL    *_photoURL;
	UIImage  *_photoImage;	
	BOOL      _workingInBackground;
}

+ (MyPhoto *)photoWithURL:(NSURL *)url;
- (id)initWithURL:(NSURL *)url;

// Public methods
- (BOOL)isImageAvailable;
- (UIImage *)image;
- (UIImage *)obtainImage;
- (void)obtainImageInBackgroundAndNotify:(id <MyPhotoDelegate>)notifyDelegate;
- (void)releasePhoto;


@end
#import "Global.h"
#import "MyPhoto.h"

// Private
@interface MyPhoto ()

// Properties
@property (retain) UIImage *photoImage;
@property ()       BOOL     workingInBackground;

// Private Methods
- (void)doBackgroundWork:(id <MyPhotoDelegate>)delegate;

@end

@implementation MyPhoto

@synthesize photoImage = _photoImage;
@synthesize workingInBackground = _workingInBackground;

#pragma mark Class Methods

+ (MyPhoto *)photoWithURL:(NSURL *)url {
	LOG_METHOD;
	
	return [[[MyPhoto alloc] initWithURL:url] autorelease];
}

#pragma mark NSObject

- (id)initWithURL:(NSURL *)url {
	LOG_METHOD;
	
	if ((self = [super init])) {
		_photoURL = [url copy];
	}
	return self;
}

- (void)dealloc {
	LOG_METHOD;
	
	[_photoURL release];
	[_photoImage release];
	[super dealloc];
}

#pragma mark Photo

- (BOOL)isImageAvailable {
	LOG_METHOD;
	
	return (self.photoImage != nil);
}

- (UIImage *)image {
	LOG_METHOD;
	
	return self.photoImage;
}

- (UIImage *)obtainImage {
	LOG_METHOD;
	
	// 画像が表示できる状態でない場合は、表示できるようになんとかする
	if (!self.photoImage) {
		
		// Load
		UIImage *img = nil;
			
		if (_photoURL) { 
			
			LOG(@"%@", _photoURL);
			
			// Read image from URL and return
			NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_photoURL];
			[request setValue:@"http://refer.example.com" forHTTPHeaderField:@"Referer"];
			
			NSError *error = nil;
			NSURLResponse *response = nil;
			NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
			
			//[request release];
			
			if (data) {
				img = [[UIImage alloc] initWithData:data];
			} else {
				LOG(@"Photo from URL error: %@", error);
			}
			
		}
		
		// Force the loading and caching of raw image data for speed
		//		[img decompress];		
		
		// Store
		self.photoImage = img;
		[img release];
		
	}
	
	return [[self.photoImage retain] autorelease];
}

// Release if we can get it again from path or url
- (void)releasePhoto {
	LOG_METHOD;
	
	if (self.photoImage && ( _photoURL)) {
		self.photoImage = nil;
	}
}

// Obtain image in background and notify the browser when it has loaded
- (void)obtainImageInBackgroundAndNotify:(id <MyPhotoDelegate>)delegate {
	LOG_METHOD;
	
	if (self.workingInBackground == YES) return; // Already fetching
	self.workingInBackground = YES;
	[self performSelectorInBackground:@selector(doBackgroundWork:) withObject:delegate];
}

// Run on background thread
// Download image and notify delegate
- (void)doBackgroundWork:(id <MyPhotoDelegate>)delegate {
	LOG_METHOD;
	
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	
	// Load image
	UIImage *img = [self obtainImage];
	
	// Notify delegate of success or fail
	if (img) {
		[(NSObject *)delegate performSelectorOnMainThread:@selector(photoDidFinishLoading:) withObject:self waitUntilDone:NO];
	} else {
		[(NSObject *)delegate performSelectorOnMainThread:@selector(photoDidFailToLoad:) withObject:self waitUntilDone:NO];		
	}
	
	// Finish
	self.workingInBackground = NO;
	
	[pool release];
}

@end
ViewController
// ImageViewerViewController.h
#import <UIKit/UIKit.h>
#import "MyPhoto.h"

@interface ImageViewerViewController : UIViewController <MyPhotoDelegate> {
	MyPhoto *_photo;
	UIImageView *_photoImageView;
	UIActivityIndicatorView *_spinner;
}

-(void)displayImage;

@end
// ImageViewerViewController.m
#import "ImageViewerViewController.h"
#import "MyPhoto.h"

@implementation ImageViewerViewController

// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
	LOG_METHOD;
	[super loadView];

	// Setup photo frame
	_photo = [MyPhoto photoWithURL:[NSURL URLWithString:@"http://farm6.static.flickr.com/5296/5425314834_535d56f8aa_b.jpg"]];
	[_photo retain];
	
	_photoImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
	_photoImageView.backgroundColor = [UIColor whiteColor];
	[self.view addSubview:_photoImageView];
	[_photoImageView retain];

	// Spinner
	_spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
	_spinner.hidesWhenStopped = YES;
	_spinner.center = CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0);
	_spinner.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin |
	UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin;
	[self.view addSubview:_spinner];	
}


- (void)viewDidLoad {
	LOG_METHOD;
    [super viewDidLoad];

	[self displayImage];	
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
	LOG_METHOD;
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}


- (void)didReceiveMemoryWarning {
	LOG_METHOD;
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc. that aren't in use.
}

- (void)viewDidUnload {
	LOG_METHOD;
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}


- (void)dealloc {
	LOG_METHOD;
	[_photo release];
	[_photoImageView release];
	[_spinner release];
    [super dealloc];
}

#pragma mark -
#pragma mark Photos

- (void)displayImage {
	LOG_METHOD;
	
	// Get image or obtain in background
	if ([_photo isImageAvailable]) {
		[_spinner stopAnimating];
		
		// Setup photo frame
		CGRect photoImageViewFrame;
		photoImageViewFrame.origin = CGPointZero;
		photoImageViewFrame.size = [_photo image].size;
		_photoImageView.frame = photoImageViewFrame;
		_photoImageView.image = [_photo image];
	} else {
		// MyPhotoに画像取得処理を委譲。取得完了通知は
		// photoDidFinishLoadingあるいはphotoDidFailToLoadへ行う。
		[_spinner startAnimating];
		[_photo obtainImageInBackgroundAndNotify:self];
	}
}

#pragma mark -
#pragma mark PhotoDelegate

- (void)photoDidFinishLoading:(MyPhoto *)photo {
	LOG_METHOD;
	[_spinner stopAnimating];
	[self displayImage];	
}

- (void)photoDidFailToLoad:(MyPhoto *)photo {
	LOG_METHOD;
	[_spinner stopAnimating];

	// 本来はここに失敗した場合の処理を記述する
}

@end

感想

とりあえず途中で大変な作業量になりそうなのでこの辺で切り上げました。

MWPhotoBrowserは内部で相互参照していたりして読むのが大変でした。
でもよくこんなにややこしい仕組みを実装して公開して下さったものです。感謝。


でももうちょっと再利用しやすいといいな…