iOSのプログラムをしていたらひょんな事にハマりました。処理の例としては以下の感じ。

  1. ユーザーに「DOWNLOAD」ボタンを押させる
  2. NSURLConnection:sendAsynchronousRequest:queue:completionHandler:を使って非同期的に所定のNSURLRequestにリクエストを送信、NSDataを取得する (もちろん、NSDataで受信時はプログレスバーを更新する)
  3. NSDataをGZipのストリームと判断し解凍する
  4. 2~3の操作を数回繰り返す

2. で受信プログレスを更新する際にはUIProgressView.progressにより随時更新の後、100%=1.0の場合はそのまま解凍処理を走らせるのだから処理上では問題ないですが、以下のプログラムにすると恐らくプログレスバーが100%にならないです。

// TODO: 受信したデータを結合する
...(略)...

// プログレスバーを更新する
_progressView.progress = CLAMP([data length] / filesize, 0.0f, 1.0f);
if ([data length] < filesize) { return; }

// TODO: 100%ダウンロードできたので解凍処理をする
...(略)...

// 次のダウンローダをリクエストする
[self requestNextRequest];

処理が連続する場合にコントロールの再描画が必要となるプロパティ変更を行っても即時で再描画されない、言い換えれば、UIView:setNeedsDisplayが画面に対して即時処理されないということ、です。ここでWindowsの話を持ち出すのはナンセンスだとは思っていますが、あえて出させてもらうとInvalidateRect(HWND, LPRECT, BOOL)だけコールしてUpdateWindow(HWND)をコールしてない事になります。さて、ではどのようにiOSのコントロールを再描画させるか。あ、ちなみに間違ってもUIView:drawRect:を直接コールしないでねっ!

答えは簡単、NSRunLoop:runUntilDate:をコールして現在のRunLoopに対して処理のゆとり?を与えるということ。再描画フラグ自体は立っているのでその処理時間を与える、というイメージだと思われます(iOS専門ぢゃないから怪しいけど)。引数がNSDateなのでどこまでの時間を渡せばいいかということですが、かなり適当です。

// TODO: 受信したデータを結合する
...(略)...


// プログレスバーを更新する
_progressView.progress = CLAMP([data length] / filesize, 0.0f, 1.0f);
if ([data length] < filesize) {
    return;
}

// 解凍処理(しばらくの間の重い処理)に入る前に、再描画をさせる
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];

// TODO: 100%ダウンロードできたので解凍処理をする
...(略)...


// 次のダウンローダをリクエストする
[self requestNextRequest];

という感じ。

0.05=50ミリ秒ですが、これで十分だと思われます。10ミリ秒でもiPod Touch 4genでは正常に再描画されましたので、恐らくそれでも大丈夫だと思います。

あ、後、CLAMPマクロは独自定義なのであしから。やってることは #define CLAMP(v, x, y) MIN(MAX((v), (x)), (y)) なだけです。