Is there a way to make drawRect work right NOW?

Fattie picture Fattie · Jan 19, 2011 · Viewed 23.2k times · Source

The original question...............................................

If you are an advanced user of drawRect, you will know that of course drawRect will not actually run until "all processing is finished."

setNeedsDisplay flags a view as invalidated and the OS, and basically waits until all processing is done. This can be infuriating in the common situation where you want to have:

  • a view controller 1
  • starts some function 2
  • which incrementally 3
    • creates a more and more complicated artwork and 4
    • at each step, you setNeedsDisplay (wrong!) 5
  • until all the work is done 6

Of course, when you do the above 1-6, all that happens is that drawRect is run once only after step 6.

Your goal is for the view to be refreshed at point 5. What to do?


Solution to the original question..............................................

In a word, you can (A) background the large painting, and call to the foreground for UI updates or (B) arguably controversially there are four 'immediate' methods suggested that do not use a background process. For the result of what works, run the demo program. It has #defines for all five methods.


Truly astounding alternate solution introduced by Tom Swift..................

Tom Swift has explained the amazing idea of quite simply manipulating the run loop. Here's how you trigger the run loop:

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];

This is a truly amazing piece of engineering. Of course one should be extremely careful when manipulating the run loop and as many pointed out this approach is strictly for experts.


The Bizarre Problem That Arises..............................................

Even though a number of the methods work, they don't actually "work" because there is a bizarre progressive-slow-down artifact you will see clearly in the demo.

Scroll to the 'answer' I pasted in below, showing the console output - you can see how it progressively slows.

Here's the new SO question:
Mysterious "progressive slowing" problem in run loop / drawRect

Here is V2 of the demo app...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html

You will see it tests all five methods,

#ifdef TOMSWIFTMETHOD
 [self setNeedsDisplay];
 [[NSRunLoop currentRunLoop]
      runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
#endif
#ifdef HOTPAW
 [self setNeedsDisplay];
 [CATransaction flush];
#endif
#ifdef LLOYDMETHOD
 [CATransaction begin];
 [self setNeedsDisplay];
 [CATransaction commit];
#endif
#ifdef DDLONG
 [self setNeedsDisplay];
 [[self layer] displayIfNeeded];
#endif
#ifdef BACKGROUNDMETHOD
 // here, the painting is being done in the bg, we have been
 // called here in the foreground to inval
 [self setNeedsDisplay];
#endif
  • You can see for yourself which methods work and which do not.

  • you can see the bizarre "progressive-slow-down". why does it happen?

  • you can see with the controversial TOMSWIFT method, there is actually no problem at all with responsiveness. tap for response at any time. (but still the bizarre "progressive-slow-down" problem)

So the overwhelming thing is this weird "progressive-slow-down": on each iteration, for unknown reasons, the time taken for a loop deceases. Note that this applies to both doing it "properly" (background look) or using one of the 'immediate' methods.


Practical solutions ........................

For anyone reading in the future, if you are actually unable to get this to work in production code because of the "mystery progressive slowdown" ... Felz and Void have each presented astounding solutions in the other specific question, hope it helps.

Answer

TomSwift picture TomSwift · Jan 22, 2011

If I understand your question correctly, there is a simple solution to this. During your long-running routine you need to tell the current runloop to process for a single iteration (or more, of the runloop) at certain points in your own processing. e.g, when you want to update the display. Any views with dirty update regions will have their drawRect: methods called when you run the runloop.

To tell the current runloop to process for one iteration (and then return to you...):

[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];

Here's an example of an (inefficient) long running routine with a corresponding drawRect - each in the context of a custom UIView:

- (void) longRunningRoutine:(id)sender
{
    srand( time( NULL ) );

    CGFloat x = 0;
    CGFloat y = 0;

    [_path moveToPoint: CGPointMake(0, 0)];

    for ( int j = 0 ; j < 1000 ; j++ )
    {
        x = 0;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = 0;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        x = self.bounds.size.width;
        y = (CGFloat)(rand() % (int)self.bounds.size.height);

        [_path addLineToPoint: CGPointMake( x, y)];

        y = self.bounds.size.height;
        x = (CGFloat)(rand() % (int)self.bounds.size.width);

        [_path addLineToPoint: CGPointMake( x, y)];

        [self setNeedsDisplay];
        [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
    }

    [_path removeAllPoints];
}

- (void) drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor( ctx, [UIColor blueColor].CGColor );

    CGContextFillRect( ctx,  rect);

    CGContextSetStrokeColorWithColor( ctx, [UIColor whiteColor].CGColor );

    [_path stroke];
}

And here is a fully working sample demonstrating this technique.

With some tweaking you can probably adjust this to make the rest of the UI (i.e. user-input) responsive as well.

Update (caveat for using this technique)

I just want to say that I agree with much of the feedback from others here saying this solution (calling runMode: to force a call to drawRect:) isn't necessarily a great idea. I've answered this question with what I feel is a factual "here's how" answer to the stated question, and I am not intending to promote this as "correct" architecture. Also, I'm not saying there might not be other (better?) ways to achieve the same effect - certainly there may be other approaches that I wasn't aware of.

Update (response to the Joe's sample code and performance question)

The performance slowdown you're seeing is the overhead of running the runloop on each iteration of your drawing code, which includes rendering the layer to the screen as well as all of the other processing the runloop does such as input gathering and processing.

One option might be to invoke the runloop less frequently.

Another option might be to optimize your drawing code. As it stands (and I don't know if this is your actual app, or just your sample...) there are a handful of things you could do to make it faster. The first thing I would do is move all the UIGraphicsGet/Save/Restore code outside the loop.

From an architectural standpoint however, I would highly recommend considering some of the other approaches mentioned here. I see no reason why you can't structure your drawing to happen on a background thread (algorithm unchanged), and use a timer or other mechanism to signal the main thread to update it's UI on some frequency until the drawing is complete. I think most of the folks who've participated in the discussion would agree that this would be the "correct" approach.