superviewの範囲外に配置されたviewにタッチイベントを送信する おまけ

かんばんわ。

前回の記事の最後で問題にぶつかりました。

なんか素早くドラッグしたらパネルビューが付いて来れずに置いてけぼりになっちゃってる〜〜〜!!!

えー・・・なんでよ・・・。

犯人はコイツだ!

    // 範囲外だったら無視する 
    if ((CGRectGetWidth(panel.frame)  >= touchPoint.x) && (touchPoint.x > 0) &&
        (CGRectGetHeight(panel.frame) >= touchPoint.y) && (touchPoint.y > 0) ) {

タッチ座標がパネルビューからはみ出てmoveを拾っていないだけでした。
コメントにも範囲外タッチイベントだったら無視するよ〜って書いてたのに…。

さらに上記とは別に、パネルビューの範囲外からドラッグしながらパネルビューに触れるとパネルが動いてしまう、という問題を発見してしまいました。
だめだめやん。。。

でも「superviewの範囲外に配置されたviewに対してタッチイベントを送信する」という本題は前回の記事で実装できているので、今回はそのオマケみたいな感じとなります。


と、言うわけで、今回解決する問題は以下の二つです。

  • ドラッグ中にパネルから指がはみ出るとパネルが置いてけぼりになってしまう
  • パネル外からドラッグしながらパネルに触れるとパネルが移動してしまう

この二つの問題は、以下の実装で解決することができそうです。

  • タッチ座標がパネルビューの範囲内か確認するのはUITouchPhaseBeganの時のみにする
  • パネルビューのクラスに「タッチ状態」を保持するBOOL値を宣言し、touchesBeganが呼び出された時にYESにする
  • touchesMovedが呼び出された時にタッチ状態がYESなら問答無用でパネルビューの移動を行う
  • touchesEndedやtouchesCancelledが呼び出された時にタッチ状態をNOにする

実装してみました。
パネルビュークラスにタッチ状態を保持するためのインスタンス変数、isTouchedを宣言します。
そしてこんな感じで各メソッドを実装。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    isTouched = YES;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    isTouched = NO;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    isTouched = NO;
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    if (isTouched) {
        CGRect rect = self.frame;
        UITouch* touch = [touches anyObject];
        
        CGPoint prePoint = [touch locationInView:self];
        CGPoint befPoint = [touch previousLocationInView:self];
        rect.origin.x += prePoint.x - befPoint.x;
        rect.origin.y += prePoint.y - befPoint.y;
        
        self.frame = rect;
    }
}

あとはCustomWindowクラスのsendEvent:メソッドの実装をちょっと変更します。

- (void)sendEvent:(UIEvent *)event {
    NSSet* touches = [event allTouches];
    AppDelegate* appdelegate = [[UIApplication sharedApplication] delegate];
    NSArray* panels = [appdelegate.viewController getPanelViews];
    
    for (UIView* panel in panels) {
        NSMutableSet* began = nil;
        NSMutableSet* moved = nil;
        NSMutableSet* ended = nil;
        NSMutableSet* cancelled = nil;
        
        for (UITouch* touch in touches) {
            CGPoint touchPoint = [touch locationInView:panel];
            
            switch ([touch phase]) {
                case UITouchPhaseBegan:
                    // UITouchPhaseBeganの時だけタッチ座標を気にする 
                    if (((CGRectGetWidth(panel.frame)  >= touchPoint.x) && (touchPoint.x > 0) &&
                         (CGRectGetHeight(panel.frame) >= touchPoint.y) && (touchPoint.y > 0))) {
                        if (!began) began = [NSMutableSet set];
                        [began addObject:touch];
                    }
                    break;
                    
                    //UITouchPhaseBegan以外は気にせずにイベントを送信する 
                case UITouchPhaseMoved:
                    if (!moved) moved = [NSMutableSet set];
                    [moved addObject:touch];
                    break;
                    
                case UITouchPhaseEnded:
                    if (!ended) ended = [NSMutableSet set];
                    [ended addObject:touch];
                    break;
                    
                case UITouchPhaseCancelled:
                    if (!cancelled) cancelled = [NSMutableSet set];
                    [cancelled addObject:touch];
                    break;
                    
                default:
                    break;
            }
            // 各種タッチメソッドの呼び出し 
            if (began) [panel touchesBegan:began withEvent:event];
            if (moved) [panel touchesMoved:moved withEvent:event];
            if (ended) [panel touchesEnded:ended withEvent:event];
            if (cancelled) [panel touchesCancelled:cancelled withEvent:event];
        }
    }
    [super sendEvent:event];
}

これでストレスなくパネルビューの移動ができるようになりました。
とてもいい感じ。

ただしこの実装だと、UITouchPhaseBegan以外はタッチ座標が範囲外でもパネルビューのtouchesMoved等が呼び出されてしまいます。
でも今回は複雑な処理はしていないし、このままでも問題ないでしょう。たぶん。オマケだし。

さて、実は前回の記事を書いてから「タッチイベントを拾う用のUIViewを前面に配置してイベントを割り振るのはダメなの?」という質問を頂きました。
今回は配置されているビューがUIViewのみという超単純な構造なのでそれでも問題ないのですが、後々UIScrollViewを置いたりするとかなり面倒くさいことになります。
前面にあるUIViewがUIScrollView用のピンチインアウト等のジェスチャーも全部吸収しちゃうんですよね。
ジェスチャー自体はUIGestureRecognizerで判別してUIScrollViewに割り振っちゃうこともできると思いますが、sendEvent:をオーバーライドする実装ならそんなことをしなくても知らないところで勝手に良きに計らってくれます。
他にはzoomScaleを考慮した座標計算とか、地味に手間がかかりそうなところがあったり。
そんな理由があって比較的楽に実装できる今回の方法をとらせて頂きました!

ご意見を頂けるのは嬉しいので大歓迎です。
「こんな実装どうよ?」とか「ここバグってんだけど、死ね」とかあればコメントください。

※でもこのブログのコード使って何か不利益被っても私は一切関知しないのでご了承下さい。