Skip to main content

Is there a way to make drawRect work right NOW?



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





If you are an advanced user of drawRect on your ipop*, 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, in a word, 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 that, all that happens is that drawRect is run once only after step 6. What you want is for the ^&£@%$@ view to be refreshed at point 5. This can lead to you smashing your ipops on the floor, scanning Stackoverflow for hours, screaming at the kids more than necessary about the dangers of crossing the street, etc etc. What to do?





Footnotes:


* ipop: i Pad Or Phone !





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.



Source: Tips4all

Comments

  1. 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.

    ReplyDelete
  2. In order to get drawRect called the soonest (which is not necessarily immediately, as the OS may still wait until, for instance, the next hardware display refresh, etc.), an app should idle it's UI run loop as soon as possible, by exiting any and all methods in the UI thread, and for a non-zero amount of time.

    You can either do this in the main thread by chopping any processing that takes more than an animation frame time into shorter chunks and scheduling continuing work only after a short delay (so drawRect might run in the gaps), or by doing the processing in a background thread, with a periodic call to performSelectorOnMainThread to do a setNeedsDisplay at some reasonable animation frame rate.

    A non-OpenGL method to update the display near immediately (which means at the very next hardware display refresh or three) is by swapping visible CALayer contents with an image or CGBitmap that you have drawn into. An app can do Quartz drawing into a Core Graphics bitmap at pretty much at any time.

    New added answer:

    Please see Brad Larson's comments below and Christopher Lloyd's comment on another answer here as the hint leading towards this solution.

    [ CATransaction flush ];


    will cause drawRect to be called on views on which a setNeedsDisplay request has been done, even if the flush is done from inside a method that is blocking the UI run loop.

    Note that, when blocking the UI thread, a Core Animation flush is required to update changing CALayer contents as well. So, for animating graphic content to show progress, these may both end up being forms of the same thing.

    New added note to new added answer above:

    Do not flush faster than your drawRect or animation drawing can complete, as this might queue up flushes, causing weird animation effects.

    ReplyDelete
  3. You can do this repeatedly in a loop and it'll work fine, no threads, no messing with the runloop, etc.

    [CATransaction begin];
    // modify view or views
    [view setNeedsDisplay];
    [CATransaction commit];


    If there is an implicit transaction already in place prior to the loop you need to commit that with [CATransaction commit] before this will work.

    ReplyDelete
  4. Have you tried doing the heavy processing on a secondary thread and calling back to the main thread to schedule view updates? NSOperationQueue makes this sort of thing pretty easy.



    Sample code that takes an array of NSURLs as input and asynchronously downloads them all, notifying the main thread as each of them is finished and saved.

    - (void)fetchImageWithURLs:(NSArray *)urlArray {
    [self.retriveAvatarQueue cancelAllOperations];
    self.retriveAvatarQueue = nil;

    NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];

    for (NSUInteger i=0; i<[urlArray count]; i++) {
    NSURL *url = [urlArray objectAtIndex:i];

    NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(cacheImageWithIndex:andURL:)]];
    [inv setTarget:self];
    [inv setSelector:@selector(cacheImageWithIndex:andURL:)];
    [inv setArgument:&i atIndex:2];
    [inv setArgument:&url atIndex:3];

    NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithInvocation:inv];
    [opQueue addOperation:invOp];
    [invOp release];
    }

    self.retriveAvatarQueue = opQueue;
    [opQueue release];
    }

    - (void)cacheImageWithIndex:(NSUInteger)index andURL:(NSURL *)url {
    NSData *imageData = [NSData dataWithContentsOfURL:url];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *filePath = PATH_FOR_IMG_AT_INDEX(index);
    NSError *error = nil;

    // Save the file
    if (![fileManager createFileAtPath:filePath contents:imageData attributes:nil]) {
    DLog(@"Error saving file at %@", filePath);
    }

    // Notifiy the main thread that our file is saved.
    [self performSelectorOnMainThread:@selector(imageLoadedAtPath:) withObject:filePath waitUntilDone:NO];

    }

    ReplyDelete
  5. Without questioning the wisdom of this (which you ought to do), you can do:

    [myView setNeedsDisplay];
    [[myView layer] displayIfNeeded];


    -setNeedsDisplay will mark the view as needing to be redrawn.
    -displayIfNeeded will force the view's backing layer to redraw, but only if it has been marked as needing to be displayed.

    I will emphasize, however, that your question is indicative of an architecture that could use some re-working. In all but exceptionally rare cases, you should never need to or want to force a view to redraw immediately. UIKit with not built with that use-case in mind, and if it works, consider yourself lucky.

    ReplyDelete

Post a Comment

Popular posts from this blog

[韓日関係] 首相含む大幅な内閣改造の可能性…早ければ来月10日ごろ=韓国

div not scrolling properly with slimScroll plugin

I am using the slimScroll plugin for jQuery by Piotr Rochala Which is a great plugin for nice scrollbars on most browsers but I am stuck because I am using it for a chat box and whenever the user appends new text to the boxit does scroll using the .scrollTop() method however the plugin's scrollbar doesnt scroll with it and when the user wants to look though the chat history it will start scrolling from near the top. I have made a quick demo of my situation http://jsfiddle.net/DY9CT/2/ Does anyone know how to solve this problem?

Why does this javascript based printing cause Safari to refresh the page?

The page I am working on has a javascript function executed to print parts of the page. For some reason, printing in Safari, causes the window to somehow update. I say somehow, because it does not really refresh as in reload the page, but rather it starts the "rendering" of the page from start, i.e. scroll to top, flash animations start from 0, and so forth. The effect is reproduced by this fiddle: http://jsfiddle.net/fYmnB/ Clicking the print button and finishing or cancelling a print in Safari causes the screen to "go white" for a sec, which in my real website manifests itself as something "like" a reload. While running print button with, let's say, Firefox, just opens and closes the print dialogue without affecting the fiddle page in any way. Is there something with my way of calling the browsers print method that causes this, or how can it be explained - and preferably, avoided? P.S.: On my real site the same occurs with Chrome. In the ex