iPhoneレシピ6:zipファイルをダウンロードし、Documentsディレクトリに解凍する


下記4つのレシピを組み合わせて、zipファイルをダウンロードし、Documentsディレクトリに解凍する方法。

ポイント

基本的には上記のレシピの中にあるとおり。
ただしiPhone SDKレシピ3:UIProgressViewの使い方 - Random Noteにある、UIProgressViewのprogressプロパティを別スレッドで設定する、という部分の別スレッド呼び出しについては、NSURLConnectionとHetimaUnZipItemの中に隠蔽されている。

ソース

Downloader.zip

UIActionSheetを用意しておいて、

- (void)viewDidLoad {
    [super viewDidLoad];
	actionSheet = [[UIActionSheet alloc] initWithTitle:@"Please wait...\n\n\n\n" 
											  delegate:self 
									 cancelButtonTitle:NSLocalizedString(@"Cancel",nil) 
								destructiveButtonTitle:nil 
									 otherButtonTitles:nil];
	progressBar = [[UIProgressView alloc] initWithFrame:CGRectMake(30.0f, 40.0f, 240.0f, 90.0f)];
	progressBar.progressViewStyle = UIProgressViewStyleDefault;
	progressBar.progress = 0.0f;
	[actionSheet addSubview:progressBar];
	
	progressLabel = [[UILabel alloc] initWithFrame:CGRectMake(30.0f, 50.0f, 240.0f, 20.0f)];
	progressLabel.backgroundColor = [UIColor clearColor];
	progressLabel.textColor = [UIColor whiteColor];
	progressLabel.font = [UIFont systemFontOfSize:[UIFont systemFontSize]];
	progressLabel.text = NSLocalizedString(@"Downloading...", nil);
	[actionSheet addSubview:progressLabel];
	
	downloaderLock = [[NSLock alloc] init];
}

ダウンロードを開始し、

- (IBAction) download:(id)sender {
	[urlField resignFirstResponder];
	downloadedContentLength = 0;
	progressBar.progress = 0;
	NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:[urlField text]]];
	
	[[NSFileManager defaultManager] clearTmpDirectory];
	progressLabel.text = NSLocalizedString(@"Downloading...", nil);
	[actionSheet showInView:self.view];
	downloader = [[URLDownload alloc] initWithRequest:req directory:APPLICATION_TMP_DIR delegate:self];
}

ダウンロードが終わったら解凍する。だけ。

- (void)downloadDidFinish:(URLDownload *)download {
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	LOG(download.filePath);
	progressLabel.text = NSLocalizedString(@"Extracting...", nil);
	
	HetimaUnZipContainer *unzipContainer = [[HetimaUnZipContainer alloc] initWithZipFile:download.filePath];
	[unzipContainer setListOnlyRealFile:YES];
	
	if ([[unzipContainer contents] count] == 0) {
		NSString *err = NSLocalizedString(@"No zip file is found.", nil);
		UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:err delegate:actionSheet cancelButtonTitle:nil otherButtonTitles:@"OK",nil];
		[alert show];
		[alert release];
	} else {
		HetimaUnZipItem *item;
		NSEnumerator *contentsEnum = [[unzipContainer contents] objectEnumerator];
		expectedUmcompressedContentSize = 0;
		for (item in contentsEnum) {
			expectedUmcompressedContentSize += [item uncompressedSize];
			LOG(@"zip\tpath:%@\t%d", [item path], [item uncompressedSize]);
		}
		contentsEnum = [[unzipContainer contents] objectEnumerator];
		for (item in contentsEnum) {
			NSString *path = [[NSFileManager defaultManager] suggestFilePath:[APPLICATION_DOC_DIR stringByAppendingPathComponent:[item path]]];
			BOOL result = [item extractTo:path delegate:self];
			if (!result) {
				NSString *err = [NSString stringWithFormat:NSLocalizedString(@"Failed to extract %@.", nil), [item path]];
				UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:err delegate:actionSheet cancelButtonTitle:nil otherButtonTitles:@"OK",nil];
				[alert show];
				[alert release];
			}
		}
	}
	
	
	[unzipContainer release];
	[pool release];
	[self dismissActionSheet];
}

ダウンロード中の進行状況はURLDownloadDelegateで取得。

- (void)download:(URLDownload *)download didReceiveResponse:(NSURLResponse *)response {
	expectedContentLength = [response expectedContentLength];
}
- (void)download:(URLDownload *)download didReceiveDataOfLength:(NSUInteger)length {
	progressBar.progress = progressBar.progress + ((long double)length / (long double)expectedContentLength) * 0.5f;
	LOG(@"Download: %f", progressBar.progress);
}

解凍中の進行状況はHetimaUnZipItemDeletegateで取得。

- (void)item:(HetimaUnZipItem *)item didExtractDataOfLength:(NSUInteger)length {
	progressBar.progress = progressBar.progress + ((long double)length / (long double)expectedUmcompressedContentSize) * 0.5f;
	LOG(@"Extracting %f", progressBar.progress);
}

終わったらdismissActionSheetを呼びだし、後始末。
UIActionSheetが非表示になった時にダウンロードに使ったURLDownloadをリリースしている。

- (void)dismissActionSheet {
	if (actionSheet) {
		[actionSheet dismissWithClickedButtonIndex:-1 animated:YES];
	}
}
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex {
	[self releaseDownloader];
}

- (void)releaseDownloader {
	[downloaderLock lock];
	if (downloader != nil) {
		[downloader release];
		downloader = nil;
	}
	[[NSFileManager defaultManager] clearTmpDirectory];
	[downloaderLock unlock];
}

途中キャンセルもできる。

// なぜかactionSheetCancelが呼ばれないので、こちらで代用
- (void)actionSheet:(UIActionSheet *)sheet clickedButtonAtIndex:(NSInteger)buttonIndex {
	if (buttonIndex == sheet.cancelButtonIndex) {
		[downloader cancel];
	}
}

iPhoneレシピ5:zipファイルを解凍する

ポイント

iPhoneにはzlibが入っているので、それを利用して解凍する。
zlibを使うためには、libz***.dylibへのリンクが必要。そのためには、xcodeで「グループとファイル」ビュー上で右クリック→「追加」→「既存のフレームワークを追加」でibz***.dylibを追加すればよい。

生のzlibをそのまま使うのはちょっと面倒だが、zlibと一緒に配布されているminizipをさらにObjective-Cでラップした「HetimaUnZip.framework」をHetimaさんが公開している。

HetimaUnZipはCocoaを使っているため、iPhoneで利用できるよう、Cocoaへの依存部分および不要なminizipファイルの削除と、メモリ上ではなく、直接ファイルに解凍するメソッドを追加したものを利用する。

オリジナルのFrameworkをiPhone上で使う方法が分からないため、とりあえず直接ソースのまま取り込んで使う。
改変したHetimaUnZipはここからダウンロードできる。

HetimaUnzip改変版

ソース

NSString *filePath = @"zipファイルへのパス";
HetimaUnZipContainer *unzipContainer = [[HetimaUnZipContainer alloc] initWithZipFile:filePath];
[unzipContainer setListOnlyRealFile:YES];
	
HetimaUnZipItem *item;
NSEnumerator *contentsEnum = [[unzipContainer contents] objectEnumerator];

// プログレスバー表示用に全ファイルのサイズを取得
expectedUmcompressedContentSize = 0;
for (item in contentsEnum) {
	expectedUmcompressedContentSize += [item uncompressedSize];
	LOG(@"zip\tpath:%@\t%d", [item path], [item uncompressedSize]);
}

// 解凍
contentsEnum = [[unzipContainer contents] objectEnumerator];
for (item in contentsEnum) {
	NSString *path = [[NSFileManager defaultManager] suggestFilePath:[APPLICATION_DOC_DIR stringByAppendingPathComponent:[item path]]];
	BOOL result = [item extractTo:path delegate:self];
	if (!result) {
		NSString *err = [NSString stringWithFormat:NSLocalizedString(@"Failed to extract %@.", nil), [item path]];
		UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:err delegate:actionSheet cancelButtonTitle:nil otherButtonTitles:@"OK",nil];
		[alert show];
		[alert release];
	}
}

// HetimaUnZipItemDelegate
// HetimaUnZipItem#extractTo:delegate:の解凍状況はdelegateのメソッドで取得できる。
- (void)item:(HetimaUnZipItem *)item didExtractDataOfLength:(NSUInteger)length {
	progressBar.progress = progressBar.progress + ((long double)length / (long double)expectedUmcompressedContentSize) * 0.5f;
	LOG(@"Extracting %f", progressBar.progress);
}

iPhoneレシピ4:UIActionSheetにプログレスバーを表示する

ポイント

UIActionSheetの高さはタイトルの改行か、setNumberOfRowsメソッドで設定する。ただしsetNumberOfRowsはドキュメントには載っていないメソッド。
UIActionSheetはメインスレッドで表示しないと、ボタンが効かないっぽい。また、表示とその他の処理を同一スレッド内で行うと、処理終了までUIActionSheetが表示されない。

ソース

Download source

UIActionSheetの組み立て部分はこんな感じ。

- (void)viewDidLoad {
    [super viewDidLoad];
	actionSheet = [[UIActionSheet alloc] initWithTitle:@"Please wait\n\n\n\n" 
											  delegate:self 
									 cancelButtonTitle:NSLocalizedString(@"Cancel",nil) 
								destructiveButtonTitle:nil 
									 otherButtonTitles:nil];
	progressBar = [[UIProgressView alloc] initWithFrame:CGRectMake(30.0f, 40.0f, 240.0f, 90.0f)];
	progressBar.progressViewStyle = UIProgressViewStyleDefault;
	progressBar.progress = 0.0f;
	[actionSheet addSubview:progressBar];
	
	progressLabel = [[UILabel alloc] initWithFrame:CGRectMake(30.0f, 50.0f, 240.0f, 20.0f)];
	progressLabel.backgroundColor = [UIColor clearColor];
	progressLabel.textColor = [UIColor whiteColor];
	progressLabel.font = [UIFont systemFontOfSize:[UIFont systemFontSize]];
	[actionSheet addSubview:progressLabel];
}

表示と処理のスレッド分けはこんな感じ。

- (IBAction)start {
	cancelFlag = NO;
	// UIActionSheetはメインスレッドで表示しないとボタンが効かないっぽい。
	[self showSheet];
	[self performSelectorInBackground:@selector(doProcess) withObject:nil];
}

- (void)showSheet {
	progressBar.progress = 0;
	progressLabel.text = NSLocalizedString(@"Processing...", nil);
	[actionSheet showInView:self.view];
}

// 何かしらの処理
- (void)doProcess {
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	@try {
		for (int i=0; i<100; i++) {
			if (cancelFlag) return;

			// Background threadで動いている時はUIProgressViewをMainThreadで動かすこともできる。
			// どちらで動かすべきかはよく分からない。
//			[self performSelectorOnMainThread:@selector(progress:) withObject:[NSNumber numberWithFloat:0.01f] waitUntilDone:NO];
			[self performSelectorInBackground:@selector(progress:) withObject:[NSNumber numberWithFloat:0.01f]];
			[NSThread sleepForTimeInterval:0.1f];
		}
		[actionSheet dismissWithClickedButtonIndex:-1 animated:YES];
	}
	@finally {
		[pool release];
	}
}

- (void)progress:(NSNumber *)amount {
	progressBar.progress += [amount floatValue];
	NSLog(@"%f", progressBar.progress);
}

///////////////////////////////////////////////////////////////////
// UIActionSheetDelegate implements
//
- (void)actionSheet:(UIActionSheet *)sheet didDismissWithButtonIndex:(NSInteger)buttonIndex {
	if (buttonIndex == sheet.cancelButtonIndex) {
		cancelFlag = YES;
	}
}

iPhone SDKレシピ3:UIProgressViewの使い方

ポイント

UIProgressViewのprogressプロパティは、ある処理をしているスレッド中で変更しても、その処理が終了するまで画面に反映されない。
そこで、performSelectorInBackground:withObject:メソッドを利用して、バックグラウンドでprogressプロパティを変更する。

ソース

// tmpViewController.h
#import <UIKit/UIKit.h>

@interface tmpViewController : UIViewController {
	IBOutlet UIProgressView *progressBar;
}

@property(nonatomic, retain) IBOutlet UIProgressView *progressBar;

- (IBAction)start;
- (void) progress:(NSNumber *)amount;

@end


// tmpViewController.m
#import "tmpViewController.h"

@implementation tmpViewController
@synthesize progressBar;

- (IBAction)start {
	progressBar.progress = 0;
	for (int i=0; i<100; i++) {
		[self performSelectorInBackground:@selector(progress:) withObject:[NSNumber numberWithFloat:0.01f]];
		[NSThread sleepForTimeInterval:0.1f];
	}
}

// バックグラウンドでprogressを変更
- (void)progress:(NSNumber *)amount {
	progressBar.progress += [amount floatValue];
}

- (void)dealloc {
	[progressBar release];
    [super dealloc];
}

@end