Search code examples
iosiphonecocoacore-graphicsrunloop

Is there a way to make drawRect work right NOW?


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

  • Updates to the user interface happen at the end of the current pass through the run loop. These updates are performed on the main thread, so anything that runs for a long time in the main thread (lengthy calculations, etc.) will prevent the interface updates from being started. Additionally, anything that runs for a while on the main thread will also cause your touch handling to be unresponsive.

    This means that there is no way to "force" a UI refresh to occur from some other point in a process running on the main thread. The previous statement is not entirely correct, as Tom's answer shows. You can allow the run loop to come to completion in the middle of operations performed on the main thread. However, this still may reduce the responsiveness of your application.

    In general, it is recommended that you move anything that takes a while to perform to a background thread so that the user interface can remain responsive. However, any updates you wish to perform to the UI need to be done back on the main thread.

    Perhaps the easiest way to do this under Snow Leopard and iOS 4.0+ is to use blocks, like in the following rudimentary sample:

    dispatch_queue_t main_queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        // Do some work
        dispatch_async(main_queue, ^{
            // Update the UI
        });
    });
    

    The Do some work part of the above could be a lengthy calculation, or an operation that loops over multiple values. In this example, the UI is only updated at the end of the operation, but if you wanted continuous progress tracking in your UI, you could place the dispatch to the main queue where ever you needed a UI update to be performed.

    For older OS versions, you can break off a background thread manually or through an NSOperation. For manual background threading, you can use

    [NSThread detachNewThreadSelector:@selector(doWork) toTarget:self withObject:nil];
    

    or

    [self performSelectorInBackground:@selector(doWork) withObject:nil];
    

    and then to update the UI you can use

    [self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:NO];
    

    Note that I've found the NO argument in the previous method to be needed to get constant UI updates while dealing with a continuous progress bar.

    This sample application I created for my class illustrates how to use both NSOperations and queues for performing background work and then updating the UI when done. Also, my Molecules application uses background threads for processing new structures, with a status bar that is updated as this progresses. You can download the source code to see how I achieved this.