Is there a way to make drawRect work right NOW?
Asked Answered
F

10

44

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?

Fibroid answered 19/1, 2011 at 19:32 Comment(4)
He's not forcing a UIView to redraw--he's forcing a run loop to loop. Different stage of the game, and not something you should generally do in production code (as you will preempt your code to run everything else on the loop too, including timers, sockets, and the like.)Persia
Re the slowdown: if you are nesting UI run loops or drawRects without specific handling for race conditions, you could end up redrawing the same pixels multiple times, perhaps even having old UI events or pixel updates overwrite the newer stuff.Theatricalize
Sorry, but the whole forcing update design is broken. Just do the data crunching in the background and update (setNeedsDisplayInRect:) the needed sections of the screen sometime - i.e. every 1/10th second or when the crunching did some real progress. Don't fight the framework.Individuate
Don't post solutions as a part of the question; [post an answer instead](stackoverflow.com/help/self-answer).Pyroelectric
G
27

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.

Gandzha answered 19/1, 2011 at 19:53 Comment(4)
@Joe - I also recall this being documented somewhere, but I can't find it in the standard places. Maybe it was mentioned in one of the WWDC talks. UIKit and AppKit certainly have different ways of going about things, given the fresh start provided by UIKit and how heavily it's based on Core Animation.Gandzha
@Joe - As I commented in hotpaw2's answer, I've only been able to do quick updates to CALayers by using a flush operation in Core Animation, but that leads to undesirable effects elsewhere.Gandzha
@Brad Larson : What kind of CA flush artifacts are you seeing?Theatricalize
@Joe - I believe the issue with waitUntilDone: set to YES is that your background thread will block in its execution until the main thread has had a chance to let the run loop go to completion. This can cause halting of your processing thread and has prevented regular UI updates in at least one case for me. You processing thread shouldn't really care if the UI has completed its update, so it makes sense to have it fire off the selector and keep doing what it's doing.Gandzha
R
37

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.

Rainer answered 22/1, 2011 at 3:40 Comment(15)
Hi Tom -- this is ASTOUNDING. Nobody else has mentioned this trick -- whoa??! How did you come to learn about it. I am going to investigate thoroughly. Really that is exactly what one needs to do in certain circumstances .. force drawRect to work "right now", ie return to the run loop when you're stuck in the run loop. [NSDate date], amazing... I have to test this out.Fibroid
@Joe - Tom's right, this isn't a trick, it's a simple manipulation of the run loop. It should work across all OS versions on Mac and iOS, given how fundamental NSRunLoop is. Neither I nor many others I know have spent much time with NSRunLoop, so it's not surprising that there are elements of it of which I'm unaware. Apple talks some about run loops in the WWDC 2010 Session 208 - Network Apps for iPhone OS, Part 2, if you're interested in more ways that they can be used.Gandzha
-1 : I hate to vote this down, since I actually used to try to do this. But I just double checked to make sure, and Apple's DTS does NOT recommend nested UI run loops in iOS, especially in NSDefaultRunLoopMode. Says "wacky problems" can occur.Theatricalize
@Theatricalize I'm not recommending the use of this. I'm answering the question. I don't think that deserves a downvote. Agreed, there are better ways to design an app, hands down. The question was about "is it possible", and yes, it is.Rainer
Not sure that an answer which DTS says could cause unspecified improper behavior or even a crash under a different OS version is really an answer. Clarify?Theatricalize
@Theatricalize - It was also pointed out to me that there is the potential for a race condition involving event handlers here. Even with that caveat, I think this is a viable solution, and does answer the letter of Joe's question, even if it might not be the recommended approach in most cases. At the very least, it's an interesting technique that I had not seen before, which is why I recommended Joe to choose this answer over mine. If Tom could edit in a few words of caution about potential problems for people using this, I definitely think it's a worthy answer.Gandzha
Running the run loop once just commits the current implicit CATransaction initiated by changing the view and underlying layer, you can just use an explicit CATransaction directly to accomplish the same thing without involving the run loop.Redan
@Christopher Lloyd : We have a winner (Just tested it on my iPhone 4)! A [ CATransaction flush ] after a setNeedsDisplay on a view will cause that view's drawRect to be called, even from inside a method that is blocking the UI run loop for several seconds.Theatricalize
@Joe Blow : I updated my answer with my device testing results, but if I were awarding this bounty, I would ask C.Lloyd to put his comment in an answer, as it seems to be the final hint that is the most deserving. IMHO.Theatricalize
No big head here. Choose the best answer. I've learned a few things from this thread, which is why I visit Stackoverflow in the first place.Rainer
@Joe Blow : one dangerous race condition with calling the run loop is that you could very well preempt your current code to run another copy of itself, a problem if you are not very careful or your current code is not completely re-entrant safe.Theatricalize
@Brad Larson : per your suggestion I updated my solution with a caveat, that this is a factual response to the question as stated but not necessarily the best way to achieve the end-goal.Rainer
This trick is pretty awesome. Here is another case where it is useful: #8377480.Torrie
I also use this technique in my code. Beware though, I just discovered a crash in this runloop code. When the clock changes from one second to another, the date can be autoreleased! Use this instead: NSDate *d = [[NSDate date] retain]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:d]; [d release];Worrisome
iOS 6: Both device and simulator, this DOES NOT work (if called from within a different drawRect: for a different view).Crosson
G
27

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.

Gandzha answered 19/1, 2011 at 19:53 Comment(4)
@Joe - I also recall this being documented somewhere, but I can't find it in the standard places. Maybe it was mentioned in one of the WWDC talks. UIKit and AppKit certainly have different ways of going about things, given the fresh start provided by UIKit and how heavily it's based on Core Animation.Gandzha
@Joe - As I commented in hotpaw2's answer, I've only been able to do quick updates to CALayers by using a flush operation in Core Animation, but that leads to undesirable effects elsewhere.Gandzha
@Brad Larson : What kind of CA flush artifacts are you seeing?Theatricalize
@Joe - I believe the issue with waitUntilDone: set to YES is that your background thread will block in its execution until the main thread has had a chance to let the run loop go to completion. This can cause halting of your processing thread and has prevented regular UI updates in at least one case for me. You processing thread shouldn't really care if the UI has completed its update, so it makes sense to have it fire off the selector and keep doing what it's doing.Gandzha
R
11

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.

Redan answered 23/1, 2011 at 6:10 Comment(8)
It works for me (calls the view's drawRect) if I do a CATransaction flush after the setNeedsDisplay, not a begin+commit.Theatricalize
Note that my [ CATransaction flush ] testing is not trying to re-enter the same drawRect or UI handler, but flushing the drawRect of, and thus animating, a separate progress view. That works for me, even from inside a UI-thread-blocking long computation.Theatricalize
@Joe Blow : Are you trying to update at a frame rate higher than your drawRect execution time allows? That could be a reason for the slow down. Try a lower fps, better matched to how long it takes your drawRect to complete. e.g. don't flush unless so many milliseconds (some multiple of 16.7) have passed since the last drawRect.Theatricalize
@Joe Blow : Also note that the (current!) iPhone and iPad have a single application CPU. Thus time taken for any Core Graphics drawing, view updates, or UI run loop handling, will take away time, and thus slow down, all other processing.Theatricalize
If you put a [CATransaction commit] at line 17 of SimpleDrawingView it will close the current implicit transaction, then my answer will work.Redan
Yes, V2, SimpleDrawingView line 17, inside of createFlowerGarden, before the loop is started [CATransaction commit]. This will commit the current implicit transaction so that your looping will be using a top level transaction.Redan
Yea, this is interesting and seems to be related to using a CGLayer, if you draw the image directly there is no slowdown.Redan
iOS 6: Both device and simulator, this DOES NOT work (if called from within a different drawRect: for a different view). NB: the NSRunLoop also doesn't work in this case - in iOS 6, it seems Apple is working harder to prevent you doing this (to be safer, maybe?). BUt it makes it impossible to keep the user updated on slow-running drawing code :( :(.Crosson
T
9

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.

Theatricalize answered 19/1, 2011 at 20:25 Comment(12)
@Joe - Exit the processing routine now, before it's done. The method I use is to break my long processing loops up into state machine states (from Algorithms 101), and just exit the routine ASAP, with subsequent states, if not the end of the loop, called by a timer with a one frame delay. If you can't do this because of a lot of nesting or recursion, you best bet is to look into doing all the processing in a background thread.Theatricalize
You should be able to run the run loop periodically for the UI to update. Doing so will not accept user input events though (in my experience), so you are far better off returning control of the run loop to the system as soon as possible.Bick
In my experience, swapping out content in a CALayer only updates immediately if you perform a [CATransaction flush] after you change that content. However, that can cause artifacts elsewhere in your interface.Gandzha
@Joe Blow : Yes. I have an app that calls setNeedsDisplay and does one drawRect only at view initialization. From thence on, all animation is done by moving UIViews and swapping out CALayer contents with CGImageRefs. No drawRects. Seems to provide a decently smooth frame rate for progress bars, etc., even without any CATransaction flushes (however the app does return to the UI run loop quickly).Theatricalize
@Brad Larson : I wrote and ran a test app on my iPhone that periodically updates CALayer contents within a method that blocks the UI thread for many seconds. Yes, a CATransaction flush is required to get a seemingly immediate update (I used 20 fps). But, perhaps because all my CALayers and subviews were non-overlapping and no other animations were scheduled to occur, I saw zero artifacts. YMMV.Theatricalize
@Theatricalize - Good to know. The artifacts I encountered may simply be due to the differences in rendering architecture between AppKit on the Mac and UIKit. Core Animation certainly plays a larger role in UIKit.Gandzha
@Brad Larson : I updated my answer with the results of a couple flush experiments that I ran on both the device and the simulator. Saw no visible problems at appropriate fps's. It would be good to know under exactly what situation there might be a risk of flush artifacts. (That might be a good DTS question?)Theatricalize
@Theatricalize - I only saw these issues (missing buttons, windows not fading in) on the Mac when doing a flush at 50 FPS in response to a CVDisplayLink timer source. This may not be a problem on iOS, given the different rendering architecture. It may also be due to the fact that CVDisplayLink triggers its callback on a background thread, not the main thread. This may have conflicted with the flush at the end of the main run loop.Gandzha
@Brad Larson : That brings up an interesting point. For Cocoa and UIKit thread safety, maybe the CATransaction flush method should only be called on the main UI thread (via a performSelectorOnMainThread with waitUntilDone set to YES)?Theatricalize
You don't really want to be flushing, you want to begin/commit a top level transaction, by commit'ing any outstanding implicit transaction you can begin/commit a top level transaction to make display changes immediately.Redan
@Christopher Lloyd : Why would committing a transaction which you didn't start be safer than flushing it? Neither seems perfectly safe, but a flush would still leave any implicit transaction open for whatever started it to work with.Theatricalize
I simply had some sequenced operations in my selector, which were working correctly on Mavericks (showing them onscreen, one by one, right after each operation completed) but were not (only showing the final result) on ElCapitan. THIS was the right solution!Mcdougald
C
4

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.

Ceres answered 23/1, 2011 at 19:8 Comment(4)
Delong : I tried this from inside a long running compute routine which was blocking the main UI thread and it did not cause drawRect to be called until after the long computation was finished.Theatricalize
@Joe of course Brad's approach makes more sense. My post was to strictly answer "how to make the view draw right now".Ceres
@Joe Blow : DDL might be saying that since UIKit was't designed with this in mind, this technique could break badly on some future OS update that remains completely consistant with the current API docs. An app not crashing on your current test device is not sufficient.Theatricalize
@Theatricalize correct. UIKit is designed to draw views once per spin of the run loop, without descending into a subspin (spinning it yourself). However, that may change in the future at Apple's discretion, so coding an app to rely on implementation details is never a good idea. It is a far better idea to do things correctly. Expensive things go on a background thread. UI goes on the main thread.Ceres
G
1

I think, the most complete answer comes from the Jeffrey Sambell's blog post 'Asynchronous Operations in iOS with Grand Central Dispatch' and it worked for me! It's basically the same solution as proposed by Brad above but fully explained in terms of OSX/IOS concurrency model.

The dispatch_get_current_queue function will return the current queue from which the block is dispatched and the dispatch_get_main_queue function will return the main queue where your UI is running.

The dispatch_get_main_queue function is very useful for updating the iOS app’s UI as UIKit methods are not thread safe (with a few exceptions) so any calls you make to update UI elements must always be done from the main queue.

A typical GCD call would look something like this:

// Doing something on the main thread
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{

// Perform long running process   
dispatch_async(dispatch_get_main_queue(), ^{
    // Update the UI   
    }); 
}); 

// Continue doing other stuff on the  
// main thread while process is running.

And here goes my working example (iOS 6+). It displays frames of a stored video using the AVAssetReader class:

//...prepare the AVAssetReader* asset_reader earlier and start reading frames now:
[asset_reader startReading];

dispatch_queue_t readerQueue = dispatch_queue_create("Reader Queue", NULL);
dispatch_async(readerQueue, ^{
    CMSampleBufferRef buffer;
    while ( [asset_reader status]==AVAssetReaderStatusReading )
    {
        buffer = [asset_reader_output copyNextSampleBuffer];
        if (buffer!=nil)
        {
            //The point is here: to use the main queue for actual UI operations
            dispatch_async(dispatch_get_main_queue(), ^{
                // Update the UI using the AVCaptureVideoDataOutputSampleBufferDelegate style function
                [self captureOutput:nil didOutputSampleBuffer:buffer fromConnection:nil];
                CFRelease (buffer);
            });
        }
    }
});

The first part of this sample may be found here in Damian's answer.

Grory answered 19/1, 2011 at 19:32 Comment(1)
I'm trying to apply the above solution to the following: NSURLSESSION to get XML data from server; parse; draw lines and create labels based on the downloaded data. I have the create lines (tried both Core Graphics/CGRect and Bezier Path) and create labels in the 'Update the UI area under 'dispatch_async (dispatch_get_main_queue ()' section - labels appear, lines do not. And I'm confident the drawing code is accurate - taken from a previous version of the app that used the deprecated URL calls.Asco
C
1

I'd like to offer a clean solution to the given problem.

I agree with other posters that in an ideal situation all the heavy lifting should be done in a background thread, however there are times when this simply isn't possible because the time consuming part requires lots of accessing to non thread-safe methods such as those offered by UIKit. In my case, initialising my UI is time consuming and there's nothing I can run in the background, so my best option is to update a progress bar during the init.

However, once we think in terms of the ideal GCD approach, the solution is actually a simple. We do all the work in a background thread, dividing it into chucks that are called synchronously on the main thread. The run loop will be run for each chuck, updating the UI and any progress bars etc.

- (void)myInit
{
    // Start the work in a background thread.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // Back to the main thread for a chunk of code
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        // Next chunk
        dispatch_sync(dispatch_get_main_queue(), ^{
            ...

            // Update progress bar
            self.progressIndicator.progress = ...: 
        });

        ...
    });
}

Of course, this is essentially the same as Brad's technique, but his answer doesn't quite address the issue at hand - that of running a lot of non thread safe code while updating the UI periodically.

Carincarina answered 19/1, 2011 at 19:32 Comment(1)
Tried using this for my need: SESSION to read URL, then parse, then create labels and lines (using CGRect). Put the URL get and parse in your '//Back to main for a chunk' area and the label creation and line drawing in 'Update progress' area (since UI must be done in main. Labels get created; get CGContext: invalid context and no lines.Asco
S
1

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];

}
Speak answered 19/1, 2011 at 19:47 Comment(2)
Yes, you have discovered that you cannot refresh the screen at 500fps.Persia
Hi Jonathon - no, that is utterly unrelated to what I am talking about. Just put NSLogs in a drawRect and experiment to discover the "gap" after the current routine, where the OS will take a chance at trying to draw.Fibroid
F
0

Regarding the original issue:

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.


Alternately per 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.


However, a bizarre problem 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 decreases. 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.

Fibroid answered 19/1, 2011 at 19:32 Comment(0)
U
0

Joe -- if you are willing to set it up so that your lengthy processing all happens inside of drawRect, you can make it work. I just wrote a test project. It works. See code below.

LengthyComputationTestAppDelegate.h:

#import <UIKit/UIKit.h>
@interface LengthyComputationTestAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

LengthComputationTestAppDelegate.m:

#import "LengthyComputationTestAppDelegate.h"
#import "Incrementer.h"
#import "IncrementerProgressView.h"

@implementation LengthyComputationTestAppDelegate

@synthesize window;


#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    // Override point for customization after application launch.
    IncrementerProgressView *ipv = [[IncrementerProgressView alloc]initWithFrame:self.window.bounds];
    [self.window addSubview:ipv];
    [ipv release];
    [self.window makeKeyAndVisible];
    return YES;
}

Incrementer.h:

#import <Foundation/Foundation.h>

//singleton object
@interface Incrementer : NSObject {
    NSUInteger theInteger_;
}

@property (nonatomic) NSUInteger theInteger;

+(Incrementer *) sharedIncrementer;
-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval;
-(BOOL) finishedIncrementing;

incrementer.m:

#import "Incrementer.h"

@implementation Incrementer

@synthesize theInteger = theInteger_;

static Incrementer *inc = nil;

-(void) increment {
    theInteger_++;
}

-(BOOL) finishedIncrementing {
    return (theInteger_>=100000000);
}

-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval {
    NSTimeInterval negativeTimeInterval = -1*timeInterval;
    NSDate *startDate = [NSDate date];
    while (!([self finishedIncrementing]) && [startDate timeIntervalSinceNow] > negativeTimeInterval)
        [self increment];
    return self.theInteger;
}

-(id) init {
    if (self = [super init]) {
        self.theInteger = 0;
    }
    return self;
}

#pragma mark --
#pragma mark singleton object methods

+ (Incrementer *) sharedIncrementer { 
    @synchronized(self) {
        if (inc == nil) {
            inc = [[Incrementer alloc]init];        
        }
    }
    return inc;
}

+ (id)allocWithZone:(NSZone *)zone {
    @synchronized(self) {
        if (inc == nil) {
            inc = [super allocWithZone:zone];
            return inc;  // assignment and return on first allocation
        }
    }
    return nil; // on subsequent allocation attempts return nil
}

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

- (id)retain {
    return self;
}

- (unsigned)retainCount {
    return UINT_MAX;  // denotes an object that cannot be released
}

- (void)release {
    //do nothing
}

- (id)autorelease {
    return self;
}

@end

IncrementerProgressView.m:

#import "IncrementerProgressView.h"


@implementation IncrementerProgressView
@synthesize progressLabel = progressLabel_;
@synthesize nextUpdateTimer = nextUpdateTimer_;

-(id) initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame: frame]) {
        progressLabel_ = [[UILabel alloc]initWithFrame:CGRectMake(20, 40, 300, 30)];
        progressLabel_.font = [UIFont systemFontOfSize:26];
        progressLabel_.adjustsFontSizeToFitWidth = YES;
        progressLabel_.textColor = [UIColor blackColor];
        [self addSubview:progressLabel_];
    }
    return self;
}

-(void) drawRect:(CGRect)rect {
    [self.nextUpdateTimer invalidate];
    Incrementer *shared = [Incrementer sharedIncrementer];
    NSUInteger progress = [shared incrementForTimeInterval: 0.1];
    self.progressLabel.text = [NSString stringWithFormat:@"Increments performed: %d", progress];
    if (![shared finishedIncrementing])
        self.nextUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0. target:self selector:(@selector(setNeedsDisplay)) userInfo:nil repeats:NO];
}

- (void)dealloc {
    [super dealloc];
}

@end
Urana answered 19/1, 2011 at 20:56 Comment(1)
PLEASE, NO. -drawRect: is for drawing only. Do NOT attempt to use it for something else.Bick

© 2022 - 2024 — McMap. All rights reserved.