UIPickerVew によるメニュー選択 (1/3)不思議篇2012年11月10日 12:11

これでも、60秒タイマーとして動作する(左側)
不思議に思って、試作プロジェクトを作成してピッカーを組み込んでいろいろと実験してみた(写真右側)。プログラミングやデバッグの参考になれば幸いである。

  

タイマーの設定方法
分の数値 0がグレーにしてあるように、この位置で停止することは想定されていないし、普通に操作している限りまずはこのようにはならない。しかし、1 --> 0 方向にゆっくり戻しながら、素早くタップを繰り返し 0の位置でそっと指を離すと再現できる。最初は難しいが、慣れるとすぐにできるようになるから興味のある方は実際に試しててもらいたい (なお、逆方向 59 --> 0 に移動しながらはできない) 。

勝手にロジックを推測してみる
ドラムを回転して 0の位置で指を離した場合はつぎの行に強制的に移動させる。この辺は基本的にはだれが考えても同じのはず。

// 停止時のデリゲート
- (void) pickerView:(UIPickerView*)pickerView didSelectRow:(NSInteger)row
 inComponent:(NSInteger)component
{
    nowIndex_ = row;   
    // ドラムの表示 0の位置を index=0 と想定
    if (nowIndex_ == 0) {
        // 次の行に移動させる
        nowIndex_++;
        [picker_ selectRow:nowIndex_ inComponent:component animated:YES];
    }
}
これで、実際に試してみると意図した動作をすることがわかる。タイマーと同様にエンドレスのドラムを実現しないと逆方向は確かめられないないが、これだけでも解決すべき問題点の再現には十分だろう。

さらに推測をふくらます
この状態でタイマーをスタートしてから、キャンセルして戻ってくると 0 --> 1になっている。あるいはこの状態でアプリを終了してから再起動しても 0 --> 1に設定されている。多分タイマーを起動した時や、プログラムがバッググラウンドに遷移する際に呼ばれるデリゲードなどに、下記のようなコードが追加されているのだろう。

// 試作の場合は inComponent:0
[picker_ selectRow:nowIndex_ inComponent:1 animateed:NO]
これでも実用上は問題ないが問題点は未解決のままである。

(以下、 (2/3) 試行錯誤篇、(3/3)解決篇へ)

PickerVew によるメニュー選択 (2/3)試行錯誤篇2012年11月11日 18:50

興味のない方は、ここはスルーして次の (3/3)解決篇へ移ってほしい。

マルチタッチイベントの競合の影響を疑ってみる
ピッカー本来の動作と、想定していなかったタップ動作が競合している可能性を疑ってみたくなった。そこで、ピッカーに Gesture Recongnizer のタップジェスチャーを登録し、さらに複数のジェスチャーを認識できるような設定をしておく。この状態でデレゲートメソッドが呼び出された時にドラムの位置をチェックするようにしてみた。

// タップジェスチャーをピッカーに登録 (viewDidLoadに設定)
picker_.userInteractionEnabled = YES;
UITapGestureRecognizer *tapGesture =[[UITapGestureRecognizer alloc]
    initWithTarget:self action:@selector(pickerTap:)];
[picker_ addGestureRecognizer:tapGesture];
[tapGesture release];

// 複数のジェスチャーの認識を許可
// (UIGestureRecognizerDelegateプロトコルのオプションメソッド)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:
    (UIGestureRecognizer *)otherGestureRecognizer
{
    return YES; //許可
}

// デレゲートメソッド
- (void)pickerTap:(UITapGestureRecognizer *)sender {
    // indexを取得
    NSUInteger index = [picker_ selectedRowInComponent:0];
    NSLog(@"タップ! index : %d", index);    
    if (!index) index++;
        // ピッカーを進める
        [picker_ selectRow:index inComponent:0 animated:YES];
        nowIndex_ = index;
}
これで確認してみると、少しは改善されたような気もするが残念ながら決定打にはなり得ない。

ダメついでに少し荒っぽいことを試してみたくなった・・・
ふりだしに戻して、今度は逆に他の Gesture Recongnizer のタッチ解析を禁止してみたらどう反応するかやってみた。他にもっと気の利いた方法もあると思うが、以下の UIGestureRecognizer のサブクラスを作成した。

// *** MyFlagGesture.h ***
#import 
#import 
@interface MyFlagGesture : UIGestureRecognizer
@end

// *** MyFlagGesture.m ***
#import "MyFlagGesture.h"
@implementation MyFlagGesture

// ジェスチャーの開始
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
}
// ジェスチャーの終了
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{    
    [super touchesEnded:touches withEvent:event];
    NSLog(@"GestureEnded!");
    // Recognized をセットすると親クラスにメッセージを送信する
    self.state = UIGestureRecognizerStateRecognized;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
}
- (void)reset
{
    [super reset];
}

// 他のジェスチャーの検出を禁止させる (オプションメソッド)
//  (Possible から遷移しようとする時に呼び出される)
- (BOOL)canPreventGestureRecognizer:
    (UIGestureRecognizer *)preventedGestureRecognizer
{
     return NO; // 禁止する (ディフォルトは禁止しない:YES)
}
// 他のジェスチャーの検出を禁止させる (オプションメソッド)
// (touchesBegan: を呼び出す直前に呼び出される)
- (BOOL)canBePreventedByGestureRecognizer:
    (UIGestureRecognizer *)preventingGestureRecognizer
{
    return NO;  // 禁止する (ディフォルトは禁止しない:YES)
}
@end
このクラスをインスタンス化してピッカーに登録してメッセージを受信できるようにする。
// MyFlagGesture のインスタンスを登録 (viewDidLoadに設定)
picker_.userInteractionEnabled = YES;
MyFlagGesture *gesture = [[MyFlagGesture alloc] initWithTarget:self
    action:@selector(pickerGestureFlag:)];
[picker_ addGestureRecognizer:gesture];
[gesture release];

// サブクラスからのジェスチャー完了メッセージを受け取る
- (void)pickerGestureFlag:(UIGestureRecognizer*)sender
{
    NSLog(@"MyFlagGesture Ended! (state = %d)",sender.state);
    // indexを取得
    NSUInteger index = [picker_ selectedRowInComponent:0];
    NSLog(@"タップ! index : %d", index);    
    if (!index) index++;
        // ピッカーを進める
        [picker_ selectRow:index inComponent:0 animated:YES];
        nowIndex_ = index;
    }
}
ロジック的には、サブクラスのジェスチャーの終了時に呼び出されるメソッドは、ピッカーのドラムを回転して操作が終了してしまうともう呼び出されない。だから後は想定外のタップ動作だけに反応してくれるから上手く行く筈であるが残念ながらそうはならないところが悩ましい。
以上のいずれも問題の解決策にはならなかった。もうお手上げであろうか、もしこれで上手く行ったとしても少々複雑すぎるのも気になるが...。

(以下、(3/3)解決篇へ)

PickerVew によるメニュー選択 (3/3)解決篇2012年11月12日 18:07

解決策はロジックを飛び越えて訪れるものらしい・・・?
もう面倒になって、タイマーのあら捜しをあきらめかけていた。そんな時ふとスレッドのことが気になりだした。UIPickerView も名前どうり UIだからメインスレッドで表示しているからである。そこに横槍を入れるならサブスレッドしかないだろう。早速やってみた。
最初 (1)不思議篇 で示したピッカーの停止した時に呼ばれるデリゲードメソッドの次の行に移動させる部分 (太字) だけをサブスレッドで実行するのだ。

.....
.....
if (nowIndex_ == 0) {
    // 次の行に移動させる
    nowIndex_++;
    [picker_ selectRow:nowIndex_ inComponent:component
        animated:YES];
}

これで決まり!
きちんと書くとこんな感じ・・・

// 停止時のデリゲート
- (void) pickerView:(UIPickerView*)pickerView didSelectRow:(NSInteger)row
 inComponent:(NSInteger)component
{
    // 位置をキープするインスタンス変数
    nowIndex_ = row;
    // ドラムの表示 0の位置を index=0 と想定
    if (nowIndex_ == 0) {
        // 次の行に移動させる
        nowIndex_++;
        [self adjustPickerPosition:nowIndex_]; // 移動メソッドを呼ぶ
    }
}

// ドラムの移動メソッド
- (void)adjustPickerPosition:(NSInteger)row 
{
    NSLog(@"adjusted!");
    // サブスレッド
    NSOperationQueue *queue = [[[NSOperationQueue alloc]init] autorelease];
    [queue addOperationWithBlock:^{
        // 行の表示位置をデーターに合わせる
        [picker_ selectRow:row inComponent:0 animated:YES];
    }];
}
NSOperationQueueクラスを使用しているが、iOS 4以降は内部でGCDを使用しているため結果的には同じである (参照:「詳解 iOS 5プログラミング」) 。これでやっと決定打となった。

簡単に検証してみる
さて、せっかくだから逆方向に検証してみよう。手順は、この NSOperationQueueクラスの mainQueueメソッドで取得したキューを使って、このサブスレッドの中からメインのスレッドで実行してみたらどうなるか? これで最初の時と同じように上手く行かなくなれば理屈は成り立つ。
    .....
    .....
    // サブスレッド
    NSOperationQueue *queue = [[[NSOperationQueue alloc]init] autorelease];
    [queue addOperationWithBlock:^{
        // メインスレッドで実行
        NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
        [mainQueue addOperationWithBlock:^{
            // 行の表示位置をデーターに合わせる
            [picker_ selectRow:row inComponent:0 animated:YES];
        }];
    }];
    .....
さて、これは予想どうりに失敗する。これでメインスレッド同士の競合が原因であることの検証もできたが、ここから先の解析は力不足であるし追求しないことにする。

(おわり)