Think Big Act Local

iPhone開発を軸にブレブレの記事を書いていきます。

重いファイルのDLをバックグラウンド実行する方法

今回はアプリがバックグラウンドの状態でもファイルをDLできる部品を作ってみます。
アプリのバッジ表示をつかってDL進捗率を表し、DLが完了したらLocalNotificationでお知らせしてくれるものを作ります。

背景(なぜバックグラウンド実行が大事なのか)

ファイルをDLする時間を待たせることはユーザストレスの一因となります。
可能であればDL中はTwitterなどで時間を潰してもらい、準備ができたらまた戻ってきて欲しいですねよね。それを実現するのがTask Completionです。

Task Completion(バックグラウンド実行)とは

バックグラウンドで10分間だけタスクを処理できる機能です。
これを使うことでアプリがアクティブでない(=他のアプリを使用中)でも、ファイルのDLなどの作業を継続させることができます。

今回はこのTask Completionを使って、バックグラウンドでファイルDLを行う部品を作ってみます。

構想

以下の機能を作ります。

  • ボタンを押すとファイルのDLを開始
  • プログレスバーとラベルに進捗(%)が表示される
  • ホームボタンを押してプロセスをバックグラウンドにしてもDLは継続
  • DLの進捗(%)がアプリのバッジとして表示される
  • DLが完了するとLocalNotificationで通知

実行結果

イメージを沸かすため、ソースコードより先に実行結果を見てみましょう。

ボタンを押すとファイルのDLが開始。プログレスバーが増えていきます。

f:id:himaratsu:20130604035025p:plain

ホームボタンを押してもDLは継続。バッジで進捗が表示されています。

f:id:himaratsu:20130604035018p:plain

DLが完了すると通知がきます。
もちろん他アプリを起動中にも通知は受け取れます。

f:id:himaratsu:20130604035011p:plain

このようなものを作っていきます。

ソースコード

通信を開始する

非同期通信でおなじみの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];
    
}

bgTaskUIBackgroundTaskIdentifier型のインスタンス変数で、先に定義しておく必要があります。
上記のように書くことで、プロセスがバックグランドに回っても処理が継続されます。

データを受信する

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開発に少し慣れてきたかな?という方におすすめの一冊です!