iOS 7のための開発ノウハウ #2:大容量ファイルのバックグラウンド転送

前回のブログでは、iOS 7に新たに追加されたバックグラウンド処理(Backgound Fetch)について紹介しました。これは一回のバックグラウンド処理時間が30秒までという、比較的短いタスクを定期的に実行するものであるため、大容量ファイルを転送処理には適していません。でもご安心を。iOS 7では“バックグラウンド転送”(Background Transfer)に関する新しい機能がきちんと用意されています。

今回は、iOS 7で大容量ファイルのバックグラウンド転送を実装する方法を解説します。

◎iOS 6とiOS 7におけるバックグラウンド転送の違い

iOS 6でもバックグラウンドでのファイル転送は可能でしたが、以下のような制約があったため、決して使い勝手の高いものではありませんでした。

  • バックグラウンド処理は10分間に制限
  • 大容量ファイルを効率的にやりとりできない

iOS 7ではファイル転送をOS側が受け持つ事で、この部分が見直されてマルチタスクに近い転送処理が可能となっています。

  • 時間制限なし
  • アプリケーションの状態によらず転送キューを追加可能
  • アプリケーションがクラッシュしても転送は継続

◎バックグラウンド転送のキモとなる NSURLSession

iOS 7でバックグラウンド転送を司るのが、新しい通信アーキテクチャである NSURLSession です。NSURLSession の特徴は、通信に関する処理を「どんな通信(セッション)」で「どんな処理(タスク)」を行なうかというケースで指定することで、あとはOS側が取り仕切ってくれるという優れたAPIです。

セッションは「NSURLSessionConfiguration」で指定し、以下の3種類が用意されています。

  • default … 指定したURLからダウンロードするためセッション。通常の通信の場合に指定します。
  • ephemeral … 指定したURLから取得したデータをメモリ内に蓄積するセッション。
  • background … バックグラウンド通信のためのセッション。

タスクには、以下の3種類があります。

  • NSURLSessionDataTask … HTMLやJSONといったデータの送受信処理を行ないます。これを指定するとバックグラウンド転送の機能は働きません。
  • NSURLSessionDownloadTask … ファイルのダウンロード処理を行ないます。
  • NSURLSessionUploadTask … ファイルのアップロード処理を行ないます。

◎バックグラウンド転送の実装方法

NSURLSession によるバックグラウンド転送には、大きく分けて2パターンの実装方法があります。単純に大容量ファイルを転送するだけであれば、completionHandlerでダウンロードした結果を検知し、ダウンロード時の処理を記述するのが、手軽に実装できるパターンです。

一方、セッション・コンフィギュレーションでbackgroundを指定した場合は、デリゲート・パターンによる記述を行なう必要があります(completionHandlerを使おうとするとその旨のエラーメッセージが出てクラッシュする)。デリゲート・パターンによってコードや処理の流れは複雑になりますが、ダウンロード中にプログレス・バーで進行状況を可視化したり、転送途中でのリジューム処理、接続先との認証処理といったきめ細やかな実装ができます(図参照)。

デリゲート・パターンによる実装のフロー

デリゲート・パターンによる実装のフロー

今回は、デリゲート・メソッドを用いてプログレス・バーを表示しながら画像ファイルをダウンロードするサンプル・プログラムを作っていくことにします。GitHubにプロジェクト・ファイルをアップしてありますので、Xcode 5をお持ちの方は是非ダウンロードしてみてください。

まず最初に、ダウンロードをバックグラウンドで処理させるために、AppDelegate.m に以下のような handleEventsForBackgroundURLSession を記述します。


- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
	self.sessionHandler = completionHandler;
	BTViewController *controller = (BTViewController*)self.window.rootViewController;
	if (!controller.session) {
		[controller createSessionWithIdentifier:identifier];
	}
}

今回、NSURLSessionの実装は ViewController で行ないます。ここでは BTViewController.m クラスファイルに、バックグラウンド転送によるダウンロード処理をタスク(NSURLSessionDownloadTask)として指定します。


@interface BTViewController ()
{
	NSURLSessionDownloadTask *_downloadTask;
}
@property (nonatomic, readwrite) NSURLSession *session;
 

次に、画面上にあるダウンロード・ボタンがタップされた際のアクションです。createSessionWithIdentifier メソッドを呼び出し、その中で
NSURLSessionConfiguration backgroundSessionConfiguration: としてバックグラウンド通信のセッションを定義します。


- (IBAction)downloadAction:(id)sender
{
	NSString *BTImageURL = @"http://farm9.staticflickr.com/8471/8138794459_749be1bfee_k.jpg";

	if (!self.session) {
		[self createSessionWithIdentifier:@"BgTransferId"];
	}

	_downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:BTImageURL]];
	[_downloadTask resume];
}

- (void)createSessionWithIdentifier:(NSString*)identifier
{
	NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];
	self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}

[_downloadTask resume]; が実行されると、ダウンロードが開始されます。ダウンロード中はプログレス・バーを書き替える必要があるので、DownloadTaskでダウンロード中に定期的に呼び出される downloadTask didWriteData: デリゲート・メソッドにプログレス・バーを伸ばす処理を加えます。


- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
	if (_downloadTask != downloadTask) return;

	dispatch_async(dispatch_get_main_queue(), ^{
		// update progress
		_downloadProgressView.progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
	});
}

DownloadTaskが完了して転送が成功した際に呼び出される downloadTask didFinishDownloadingToURL では、転送された画像ファイルを、アプリケーション内の書類ディレクトリにコピーし、画像を表示します。


- (void)URLSession:(NSURLSession*)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
	// copy image to document directory
	NSURL *docUrl = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject;
	docUrl = [docUrl URLByAppendingPathComponent:BTImageName];

	BOOL success = [[NSFileManager defaultManager] copyItemAtURL:location toURL:docUrl error:NULL];
	if (!success) {
		NSLog(@"Failed to copy. src=%@, dest=%@", location, docUrl);
	}

	if (_downloadTask == downloadTask) {
		dispatch_async(dispatch_get_main_queue(), ^{
			[_indicatorView stopAnimating];
			_downloadButton.enabled = YES;
			_downloadProgressView.progress = 1;

			_imageView.image = [UIImage imageWithContentsOfFile:[self _eiffelImagePath].path];
			_removeButton.hidden = NO;
		});
	}
}
 

DownloadTaskが完了した際に呼ばれる didCompleteWithError: で、エラーがあった場合のハンドリングを行ないます。


- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
	if (error) {
		NSLog(@"failed to download: error=%@", error);
		if (task == _downloadTask) {
			dispatch_async(dispatch_get_main_queue(), ^{
				if (_indicatorView.isAnimating) {
					[_indicatorView stopAnimating];
				}
			});
		}
	}
}

URLSessionDidFinishEventsForBackgroundURLSession: が呼ばれます。ここでは、ローカル通知を発行して、バックグランドでのダウンロード処理が完了したことをユーザーに知らせています。また、画像を表示する処理もここで行なうことで、OSは処理後の画面スナップショットを自動生成し、タスク・スイッチャー画面(ホームボタンを2度押し)に表示される画面を更新します。


- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
	dispatch_sync(dispatch_get_main_queue(), ^{
		UILocalNotification *notif = [UILocalNotification new];
		notif.alertBody = @"Download Complete.";
		[[UIApplication sharedApplication] presentLocalNotificationNow:notif];

		_imageView.image = [UIImage imageWithContentsOfFile:[self _eiffelImagePath].path];
	});

	BTAppDelegate *appDel = (BTAppDelegate*)[UIApplication sharedApplication].delegate;
	if (appDel.sessionHandler) {
		appDel.sessionHandler();
		appDel.sessionHandler = nil;
	}
}

以上がNSURLSessionを使ったバックグラウンド転送の概要および実装方法となります。

Comments are closed.