重いファイルのDLをバックグラウンド実行する方法
今回はアプリがバックグラウンドの状態でもファイルをDLできる部品を作ってみます。
アプリのバッジ表示をつかってDL進捗率を表し、DLが完了したらLocalNotificationでお知らせしてくれるものを作ります。
背景(なぜバックグラウンド実行が大事なのか)
ファイルをDLする時間を待たせることはユーザストレスの一因となります。
可能であればDL中はTwitterなどで時間を潰してもらい、準備ができたらまた戻ってきて欲しいですねよね。それを実現するのがTask Completionです。
Task Completion(バックグラウンド実行)とは
バックグラウンドで10分間だけタスクを処理できる機能です。
これを使うことでアプリがアクティブでない(=他のアプリを使用中)でも、ファイルのDLなどの作業を継続させることができます。
今回はこのTask Completionを使って、バックグラウンドでファイルDLを行う部品を作ってみます。
構想
以下の機能を作ります。
- ボタンを押すとファイルのDLを開始
- プログレスバーとラベルに進捗(%)が表示される
- ホームボタンを押してプロセスをバックグラウンドにしてもDLは継続
- DLの進捗(%)がアプリのバッジとして表示される
- DLが完了するとLocalNotificationで通知
実行結果
イメージを沸かすため、ソースコードより先に実行結果を見てみましょう。
ボタンを押すとファイルのDLが開始。プログレスバーが増えていきます。
ホームボタンを押してもDLは継続。バッジで進捗が表示されています。
DLが完了すると通知がきます。
もちろん他アプリを起動中にも通知は受け取れます。
このようなものを作っていきます。
ソースコード
通信を開始する
非同期通信でおなじみのNSURLConnectionを使って通信します。
// ボタンが押されたら通信開始 - (IBAction)startNetworkConnection:(id)sender { NSLog(@"通信を開始します..."); UIApplication *app = [UIApplication sharedApplication]; bgTask = [app beginBackgroundTaskWithExpirationHandler:^{ [app endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; // 適当な重いファイル NSURL *url = [NSURL URLWithString:@"xxxxxxxxxxx.zip"]; // プログレスバーをセット [self.progressView setProgress:0]; // ラベルをセット self.perLabel.text = @"0%"; // リクエスト開始 NSURLRequest *request = [NSURLRequest requestWithURL:url]; [NSURLConnection connectionWithRequest:request delegate:self]; }
bgTask
はUIBackgroundTaskIdentifier
型のインスタンス変数で、先に定義しておく必要があります。
上記のように書くことで、プロセスがバックグランドに回っても処理が継続されます。
データを受信する
NSURLConnectionDelegateのデリゲートを記載します。
#pragma mark - NSURLConnectionDelegate // レスポンス受信 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { self.receiveData = [NSMutableData data]; totalBytes = [response expectedContentLength]; // 全体のサイズ loadedBytes = 0; [self showProgress]; } // データ受信 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [_receiveData appendData:data]; loadedBytes += [data length]; [self showProgress]; } // データ受信完了 - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self showProgressDone]; [[UIApplication sharedApplication] endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; } // エラー発生 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"error!"); }
[response expectedContentLength]
で予想されるファイルサイズを取得できます。
(まれに正常に取得できずに-1が返されます。原因追求中です)
進捗を表示する
アプリのView上のプログレスバーや、アプリバッジに進捗を%単位で表示します。
// 進捗表示 - (void)showProgress { float rate = (loadedBytes/totalBytes); int per = rate*100; // プログレスバーに表示 [self.progressView setProgress:rate]; // ラベルに表示 self.perLabel.text = [NSString stringWithFormat:@"%d%%", per]; // バッジに表示 UIApplication *app = [UIApplication sharedApplication]; app.applicationIconBadgeNumber = per; // ログに表示 NSLog(@"Now Loading... [%f/%f]", loadedBytes, totalBytes); } // 進捗完了表示 - (void)showProgressDone { // プログレスバーに表示 [self.progressView setProgress:1]; // ラベルに表示 self.perLabel.text = @"100% (Complete!)"; // バッジに表示 UIApplication *app = [UIApplication sharedApplication]; app.applicationIconBadgeNumber = 0; // ログに表示 NSLog(@"Loading Done!"); // 完了したことを通知する [self notificateDone]; }
DL完了を通知する
LocalNotificationを使って以下のように記述します。
// 完了したことをLocalNotificationで通知 - (void)notificateDone { UILocalNotification *localNotif = [[UILocalNotification alloc] init]; // 日時を設定 localNotif.fireDate = [[NSDate date] dateByAddingTimeInterval:5]; localNotif.timeZone = [NSTimeZone defaultTimeZone]; // 通知メッセージ localNotif.alertBody = [NSString stringWithFormat:@"ダウンロードが完了しました"]; // 効果音は標準の効果音を利用する [localNotif setSoundName:UILocalNotificationDefaultSoundName]; // 通知アラートのボタン表示名を指定 localNotif.alertAction = @"Open"; // 作成した通知イベントをアプリケーションに登録 [[UIApplication sharedApplication] scheduleLocalNotification:localNotif]; }
ソースコードは以上となります。
まとめ
10分間だけバックグラウンド実行可能なTask Completionを使ってDL部品を作りました。
利点は以下の2点です。
- アプリを開いて待機する必要がないため、ユーザの負荷を少なくできる
- DL完了時に通知することで、ユーザの離脱を防ぐことができる
ソースコード
Githubで公開しています
BackGroundIndicator -Github
参考サイト
関連書籍
Webでは見つからないイケてるコード集。
iOS開発に少し慣れてきたかな?という方におすすめの一冊です!