Help us understand the problem. What is going on with this article?

setNeedsDisplayを呼んでもdrawRectされない!場合の1つの解決事例

More than 5 years have passed since last update.

別でsocket.ioで落書きをリアルタイム共有するアプリを試しに作っていて、その過程でわかったことについて書きます。結論からいうとなぜ解決したか厳密にはわかってないのですが、一事例として書いてみます(分かる方教えて下さい!)。

僕が遭遇した事象

こんな要領で描画更新処理を書かいていました

  1. owner(自身)の落書き線は、touchイベント発生時にCGPointが入ったバッファ配列を更新
  2. member(他socket)の落書き線は、socket.ioのコールバックから取得し、CGPointが入ったバッファ配列を更新
  3. viewControllerでタイマーを動かし、一定間隔でsetNeedsDisplayを呼ぶ

が、結果としては、自身の落書き線は描画されるが、他socketの落書き線は描画されないという事象に陥りました。

1のtouchイベント発生時にバッファ配列を更新する部分

PerlinLineView.m
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];

    CGPoint location = [[touches anyObject] locationInView:self];
    // NSLog(@"%@", NSStringFromCGPoint(location));
    [self prepareDotsWithPoint:location];
}

2のバッファ配列を差し替える部分

PerlinLineView.m
- (void)assignOtherPtDicWithAry:(NSArray*)ary ownSocketId:(NSString*)ownSid
{
    // ary must be like @[ @[CGPoint's ary], socketid]
    if(!ary ||
       ![ary isKindOfClass:[NSArray class]] ||
       ary.count < 2 ||
       !ownSid ||
       ![ownSid isKindOfClass:[NSString class]] ||
       ownSid.length <= 0){
        NSLog(@"%s", __PRETTY_FUNCTION__);
        return;
    }

    NSString *socketId = ary[1];
    if(!socketId){return;}

    // If own socket, do nothing
    if([ownSid isEqualToString:socketId]){
        return;
    }

    // Replace dots data
    self.otherPointsDic[socketId] = ary[0];

    ~ 以下省略 ~
}

3のviewControllerでタイマーを動かし、一定間隔でsetNeedsDisplayを呼ぶコード

ViewController.m
    // Start draw update timer
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{
        while (1) {
            NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
            [mainQueue addOperationWithBlock:^{
                [self.view setNeedsDisplay];
            }];
            [NSThread sleepForTimeInterval:0.05];
        }
    }];

そして、drawRectでこんなふうに書いてました。

PerlinLineView.m
- (void)drawRect:(CGRect)rect {

    /* Drawing code below */

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 2.0);
    CGContextSetStrokeColorWithColor(context,
                                     self.lineColor.CGColor);

    if(self.points.count <= 0) return;

    /* まず自身の落書き線を描画 */

    // Move to first point
    CGPoint pt, cpt;
    CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)self.points[0],
                                            &pt);
    CGContextMoveToPoint(context, pt.x, pt.y);
    [self.points removeObjectAtIndex:0];

    // Connecting the dots
    int i = 0;
    for(NSDictionary * ptDic in self.points){

        if(i+1 < self.points.count){
            // make curve to end point
            CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)ptDic,
                                                    &cpt);
            CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)self.points[i+1],
                                                    &pt);
            CGContextAddQuadCurveToPoint(context, cpt.x, cpt.y, pt.x, pt.y);
        }else{
            // make line to end point...
            CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)ptDic,
                                                    &pt);
            CGContextAddLineToPoint(context, pt.x, pt.y);
        }

        i++;
    }

    //CGContextClosePath(context);
    CGContextStrokePath(context);

    /* 他socketの落書き線を描画 */

    // Draw Other points
    for(NSString *sidKey in self.otherPointsDic){
        NSMutableArray *pts = [self.otherPointsDic[sidKey]mutableCopy];

        if(pts.count <= 0 ) continue;

        CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)pts[0],
                                                &pt);
        CGContextMoveToPoint(context, pt.x, pt.y);
        [pts removeObjectAtIndex:0];
        self.otherPointsDic[sidKey] = pts;  // replace current one

        CGContextSetStrokeColorWithColor(context,
                                         [(UIColor*)self.otherPointColorDic[sidKey] CGColor]);
        i=0;
        for(NSDictionary *ptDic in pts){

            if(i+1 < pts.count){
                // make curve to end point
                CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)ptDic,
                                                        &cpt);
                CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)pts[i+1],
                                                        &pt);
                CGContextAddQuadCurveToPoint(context, cpt.x, cpt.y, pt.x, pt.y);
            }else{
                // make line to end point...
                CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)ptDic,
                                                        &pt);
                CGContextAddLineToPoint(context, pt.x, pt.y);
            }

            i++;
        }

        CGContextStrokePath(context);
    }
}

まずsetNeedsDisplayに関するドキュメントを読む

ドキュメントを呼んでみて、まず最初に言いたいのは、setNeedsDisplayは、drawRectに"再描画が必要だよ"と伝えるだけで、再描画のタイミングはシステム側が決めます。なので、別にsetNeedsDisplayを呼んだからといってすぐにdrawRectが呼ばれるわけではないということ。

公式のiOS描画および印刷ガイドでは下記のように書いてありました。

次に示すアクションにより、ビューの更新が引き起こされます。
・ビューの一部を隠している別のビューの移動または除去
・hidden非表示になっていたビューの再表示(プロパティをに設定)NO
・ビューを画面外までスクロールし、再び画面内に戻す
・ビューのsetNeedsDisplayメソッドまたはsetNeedsDisplayInRect:メソッドの明示的な呼び出し

  ~ 中略 ~

drawRect:メソッドを呼び出した後、ビューは自らを更新済みとしてマークを付け、新たなアクションが到着して別の更新サイクルがトリガされるのを待ちます。

注目したいのは、ビューは自らを更新済みとしてマークを付け と書いてある部分。

一方で、UIView Class Reference(Xcode上でShift+⌘+0[ゼロ]で表示されます)でsetNeedsDisplayの定義をみてみると、

The view is not actually redrawn until the next drawing cycle, at which point all invalidated views are updated.

(意訳)
(setNeedsDisplayを呼んでも)viewは次の描画サイクルまで再描画されません。再描画時には、invalidated viewsが再描画される。

とあります。

ここまでの僕の認識:
UIViewは独自の描画ループを持っており、drawRect内に記述された描画処理は、上記のアクションで描画フラグが立ち上がり、描画ループは、そのフラグが立っているview(invalidated viewのことかと)に関して再描画を行う。

上記のドキュメント通りであれば、setNeedsDisplayによって、フラグは0.05秒ごとに立っているので、drawRectは次の描画ループが回ってくるタイミングで描画されるはずです。
が、drawRectは0.05秒ごとに呼ばれるのではなく、常にtouchイベント発生をきっかけに描画されました。例えmember(他socket)からバッファ配列を更新したタイミングでsetNeedsDisplayしてもはdrawRectは呼ばれませんでした。

あれ、setNeedsDisplayで描画フラグは立っているので、drawRectは呼ばれるはず。でも呼ばれない。なぜだ!

やってみたこと その1

stackoverflowの記事 Is there a way to make drawRect work right NOW? で紹介されている方法もいくつかやってみましたが、ダメでした。

やってみたこと その2

setNeedsDisplay単体でだめなら、それ以外で、drawRectが反応するようなアクションをしてみよう。

次に示すアクションにより、ビューの更新が引き起こされます。
・ビューの一部を隠している別のビューの移動または除去
・hidden非表示になっていたビューの再表示(プロパティをに設定)NO
・ビューを画面外までスクロールし、再び画面内に戻す
・ビューのsetNeedsDisplayメソッドまたはsetNeedsDisplayInRect:メソッドの明示的な呼び出し

うーん、この中だと、

・ビューの一部を隠している別のビューの移動または除去

これだったらもしかしてaddSubViewでもdrawRectよばれるかも?

結果、解決した方法

owner(自身)とmember(他socket)を別のViewに分けて、memberをownerにaddSubViewして使ってみたらうまくきました。

UIViewController
 ┗ OwnerのUIViewサブクラス
   ┗ member1のUIViewサブクラス
   ┗ member2のUIViewサブクラス
   ┗ ...

という構成にして

  • OwnerのUIViewサブクラスはtouchイベントを起点にCGPointの入ったバッファ配列を更新
  • MemberのUIViewサブクラスはsocket.ioのコールバック時に必要であれば作成/既に存在すればバッファ配列を更新し、setNeedsDisplayを呼ぶ

というふうに変更しました。

viewControllerでタイマーを動かし、一定間隔でsetNeedsDisplayを呼ぶのは今まで通りです。

まとめ

最初の認識は、

drawRectが呼ばれるアクション一覧
・ビューの一部を隠している別のビューの移動または除去
・hidden非表示になっていたビューの再表示(プロパティをに設定)NO
・ビューを画面外までスクロールし、再び画面内に戻す
・ビューのsetNeedsDisplayメソッドまたはsetNeedsDisplayInRect:メソッドの明示的な呼び出し

UIViewは独自の描画ループを持っており、drawRect内に記述された描画処理は、上記のアクションで描画フラグが立ち上がり、描画ループは、そのフラグが立っているview(invalidated viewのことかと)に関して再描画を行う。

でしたが、setNeedsDisplay単体ではdrawRectはトリガーできないという認識が加わりました。

そして、不思議なのは、addSubViewするのは、各memberにつき1度だけです。それ以降は、当該viewのバッファ配列のプロパティを変更しているだけですが、それでも更新されます。つまり、

当該viewの描画に関わるプロパティの変更だけでもsetNeedsDisplayとセットで使うことでdrawRectをトリガーするアクションになりうる?

というのは推測の域をでません。他の要因の可能性もあります。
なので、まだsetNeedsDisplayの挙動と、drawRectのタイミングは厳密に判明してはいません。

もし厳密にわかる方がいらっしゃったら是非コメントいただきたいです。よろしくお願いいたしますm(_ _)m


サンプルソース:https://github.com/mitolog/AwkwardLineShare
※ NGだったファイルはPerlineLineView.m/hとして一応配置してありますが、ViewController側が対応してないので、動かすの大変かも。参考まで。

mito_log
ベトナム・ハノイ拠点の個人デベロッパ。東南アジア向けに素敵なサービスと文化を作るべく模索中。ベトナムで1年半農業。newbie skateboarder、ハノイITもくもく会毎週土曜朝9時頃 → https://bit.ly/2Oe9Ehk
http://mitolab.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした