2010年5月9日日曜日

[iOS] UIActivityIndicatorViewでハマった件

UIKitにはUI用の便利なクラスが色々あって、インジケータ(ぐるぐる画像)を表示できるUIActivityIndicatorViewクラスというのが提供されています。

時間のかかる処理を行っている間アプリに何の反応もないと、ユーザーにはアプリがちゃんと動いているのかどうか分かりません。
インジケータは「処理してますよ〜」というサインをユーザーに送ることができるのでけっこう重要です。



UIActivityIndicatorViewを使えば簡単にインジケータを扱えるんだろうなあとタカをくくっていたらちょっとハマったのでメモしておきます。




巷の解説書などによると基本的には下のような流れでUIActivityIndicatorViewを利用します。


まず、オブジェクトを初期化
UIActivityIndicatorView  *indicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
フレームをセット
indicator.frame = CGRectMake((100.0, 100.0, 50.0, 50.0);
親Viewに追加
[self.view addSubview:indicator];
startAnimatingメソッドを呼び出せばインジケーターが表示される。
[indicator startAnimating];
stopAnimatingメソッドを呼び出せばインジケーターが隠れる。
[indicator stopAnimating];



ところが startAnimating  stopAnimating  を同じスレッド内で実行してもインジケータがうまく切り替わりません。



ようするに
- (void)doSomething {
    ...
    [indicator startAnimating];

    // なにか重い処理を実行...

    [indicator stopAnimating];
}
こんな感じであるメソッド内でstartAnimatingとstopAnimatingを実行してもきちんとインジケータが隠れてくれません!
(出っぱなしになる)

どうやらstopAnimatingはstartAnimatingとは別スレッドで呼び出さなければならないらしいのです。


幸いNSObjectクラスにはperformSelectorInBackground:withObject:というメソッドが用意されているので、それを利用すれば簡単に別スレッドで何らかの処理をすることができます。



そんなわけでサンプルコードはこちら。
- (void)loadView {
    ...
    // 初期化したオブジェクトをインスタンス変数に追加しておく
    self.indicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];

    indicator.frame = CGRectMake((100.0, 100.0, 50.0, 50.0);
    
    [self.view addSubview:indicator];
}


- (void)doSomething {
    [self.indicator startAnimating];
    [self performSelectorInBackground:@selector(doInBackground) withObject:nil];
}

- (void)doInBackground {
    // 別スレッドで実行するメソッドではNSAutoreleasePoolを使わなければならない
    NSAutoreleasePool *pool;
    pool = [[NSAutoreleasePool alloc] init];

    // なにか重い処理を実行
    ...
    [self.indicator stopAnimating];

    [pool drain];
}

基本的にはこれでインジケータの表示/非表示の切り替えができるようになります。

3 件のコメント:

monyu さんのコメント...

わかりやすい記事で参考になりました!

pengwyn さんのコメント...

違いますよ、これ。
>どうやらstopAnimatingはstartAnimatingとは別スレッドで呼び出さなければならないらしいのです。
どちらもメインスレッドから呼出すべきなのです。
iOSは(その他一般イベントドリブンフレームワークも)メインスレッド(イベントループ)で重い処理をしてはいけません。
メインスレッドで重い処理すれば表示も入力も全て止まってしまいます。(いわゆるフリーズ)
元々の問題がメインスレッドで重い処理をしていた事なので、重い処理を別スレッドで表示するところまでは妥当ですが、stopAnimatingはメインスレッドで実行するべきです。
「動いたから良いじゃん」と言われてしまえば返す言葉は無いですが…
大きなお世話なのは重々承知しておりますが、この記事を真に受ける人がいる様なので念のため。

busyomono99 さんのコメント...

参考にまでに。
同じくUIActivityIndicatorViewが消えずに困ってました。
「UIKit のオブジェクトはメインスレッド以外からアクセスしてはいけない」らしいです。

- (void)doInBackground メソッド内の

[self.indicator stopAnimating];

[self.indicator performSelectorOnMainThred:@selector:(stopAnimating) withObject:nil waitUntilDonw:YES];

でも上手く動作しました。

多分こちらの方がお作法的によろしいと思いますので、良かったら試してみて下さい。

ではでは。