Is there a way to make drawRect work right now?

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

If you are an advanced drawRect user, you will find out that, of course, drawRect will not actually work until “all processing is complete”.

setNeedsDisplay places the view as invalid and the OS, and basically waits until all processing has been completed. This can be infuriating in the general situation in which you want to:

  • view controller 1
  • runs some function 2
  • which step by step 3
    • creates increasingly complex images and 4
    • at each step you setNeedsDisplay (wrong!) 5
  • until all work is done 6

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

Your goal is to update the view at point 5. What to do?




The solution to the original question ..............................................

In a word, you can (A) the background of the big picture, and call to the forefront for updating the user interface or (B) there may be debatable there are four “immediate” methods that do not use the background process. For the result, run the demo program. It has #defines for all five methods.




A truly startling alternative solution introduced by Tom Swift ..................

Tom Swift explained the amazing idea of ​​a fairly simple manipulation of the execution cycle . Here's how you run the run loop:

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

This is truly an amazing piece of technology. Of course, special care should be taken when manipulating the launch cycle, and many of them pointed out this approach strictly for experts.




An unusual problem that occurs ..........

Although a number of methods work, they actually do not work, because there is an unusual artifact of progressive slowdown that you will see in the demo.

Scroll to the “answer” that was inserted below, showing the console output - you can see how it gradually slows down.

Here's a new SO question:
Mysterious "gradual slowdown" problem in run loop / drawRect

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

You will see that he 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 don't.

  • you can see the weird "progressive slowdown." Why is this happening?

  • You can see the controversial TOMSWIFT method, there are virtually no problems with responsiveness. Click to reply at any time. (but still the bizarre problem of "gradual slowdown")

Thus, the overwhelming thing is the strange "progressive slowdown": at each iteration, for unknown reasons, the time spent on the loop decreases. Please note that this applies to both the "correct" (background) and the use of one of the "direct" methods.




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

For those who read in the future, if you really cannot get this to work in production code because of the “gradual slowdown of secrecy” ... Felz and Void each of them presented amazing solutions in another specific question, I hope that this helps.

+43
ios iphone cocoa quartz-graphics runloop
Jan 19 '11 at 19:32
source share
9 answers

User interface updates occur at the end of the current run through the run loop. These updates are performed in the main thread, so anything that lasts in the main thread in the main thread (lengthy calculations, etc.) will prevent the launch of interface updates. In addition, anything that works for some time in the main thread will also make your sensory processing unresponsive.

This means that it is not possible to "force" update the user interface from any other point in the process running in the main thread. The previous statement is not entirely correct, because Tom answers shows. You can enable the execution of a run loop in the middle of operations performed in the main thread. However, this can still slow down your application.

In general, it is recommended that you move everything you need to run to the background thread so that the user interface remains responsive. However, any updates you want to perform in the user interface must be done in the main thread.

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

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

Do some work part of the above can be a long calculation or an operation that traverses multiple values. In this example, the user interface is updated only at the end of the operation, but if you want to track continuous progress tracking in your user interface, you can send the dispatch to the main queue where the user interface has ever been updated.

For older versions of the OS, you can disable the background thread manually or through NSOperation. For manual streaming, you can use

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

or

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

and then update the user interface that you can use

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

Note that I found the NO argument in the previous method to get constant user interface updates when working with a continuous progress bar.

This is an example application that I created for my class that illustrates how to use both NSOperations and queues to do background work, and then update the interface after completion. In addition, my Molecules application uses background threads to process new structures, with the status bar being updated as it moves. You can download the source code to find out how I did it.

+26
Jan 19 '11 at 19:53
source share

If I understand your question correctly, this is a simple solution. During your long work, you need to specify the current runloop to handle one iteration (or more, runloop) at specific points in your own processing. for example, when you want to refresh the display. Any views with dirty update areas will have their drawRect: methods called when runloop starts.

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

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

Here is an example of a (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 tweak this to do the rest of the user interface (i.e. user input) as well.

Refresh (caution for using this technique)

I just want to say that I agree with most of the reviews from others here, saying this solution (calling runMode: forcing drawRect :) is not necessarily a great idea. I answered this question with what, in my opinion, is the actual “here, how”, answering this question, and I am not going to advertise it as a “correct” architecture. In addition, I am not saying that there may be other (better?) Ways to achieve the same effect - of course, there may be other approaches that I did not know about.

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

The performance degradation you see is the overhead of running runloop at each iteration of your drawing code, which includes displaying the layer on the screen, as well as all other runloop processing, such as collecting and processing input.

One option might be to call runloop less often.

Another option would be to optimize the drawing code. In its current form (and I don’t know if this is really your application or just your sample ...) there are a few things you could do to make it faster. The first thing I would like to do is move all the UIGraphicsGet / Save / Restore code out of the loop.

However, from an architectural point of view, I highly recommend considering some of the other approaches mentioned here. I see no reason why you cannot structure your drawing in the background thread (the algorithm has not changed), and use a timer or other mechanism to signal the main thread to update its user interface at a certain frequency until the drawing is completed. I think most people who participated in the discussion would agree that this would be the “right” approach.

+37
Jan 22 '11 at 3:40
source share

You can do this several times in a loop, and it will work fine, without threads, without interacting with runloop, etc.

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

If there is an implicit transaction before the loop, you need to commit it with [CATransaction commit] before this will work.

+9
Jan 23 '11 at 6:10
source share

To get drawRect is called speedy (which is not necessary right away, since the OS can wait, for example, to update the next hardware display, etc.), the application should stand idle in this UI startup cycle as soon as possible by exiting any methods in UI thread and for non-zero time.

You can do this in the main thread by interrupting any processing that takes longer than the animation time into shorter pieces and planning to continue working only after a short delay (so drawRect can work in spaces), or by doing processing in the background thread with periodic a call to execute a SelectorOnMainThread to execute setNeedsDisplay at some reasonable animation frame rate.

A non-OpenGL method for immediately updating the display (which means that the next time the hardware display is updated or three), the visible contents of CALayer are exchanged with the image or CGBitmap that you pulled into. An application can draw quartz into a Core Graphics bitmap at any time.

Newly added answer:

Please see Brad Larson's comments below, and Christopher Lloyd will comment on another answer here as a hint leading to this decision.

 [ CATransaction flush ]; 

will cause drawRect to be called in the views on which the setNeedsDisplay request was executed, even if the flash was made from inside a method that blocks the user interface execution loop.

Note that when blocking the UI thread, a Core Animation flash is required to update the contents of CALayer. Thus, in order to animate graphic content in order to show progress, they may appear in the form of the same.

New added note to new added answer above:

Do not slow down faster than your drawRect or drawing animation may complete, as this may interfere with the creation of flushes, causing strange animation effects.

+8
Jan 19 2018-11-11T00:
source share

Without asking the wisdom of this (what you should do), you can do:

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

-setNeedsDisplay marks the view to be redrawn. -displayIfNeeded will cause the view support layer to redraw, but only if it has been marked as necessary for display.

I want to emphasize, however, that your question points to an architecture that may use some processing. In all but extremely rare cases, you should not need or want to force a redraw of the view . UIKit, not built with this use case in mind, and if it works, consider yourself happy.

+4
Jan 23 '11 at 19:08
source share

Have you tried to do heavy processing of the secondary stream and redirect the main stream to schedule updates? NSOperationQueue makes this pretty easy.




Sample code that takes an NSURL array as input and loads all of them asynchronously, notifying the main thread as each of them is completed 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]; } 
+1
Jan 19 '11 at 19:47
source share

I understand that this is an old thread, but I would like to offer a clean solution to this problem.

I agree with other posters that in an ideal situation, all the heavy lifting should be done in the background thread, but there are times when it is simply not possible, because the time-consuming part requires a lot of access to unsafe methods such as those offered by UIKit. In my case, initializing my user interface takes a lot of time, and I cannot start anything in the background, so my best option is to update the progress bar during init.

However, as soon as we think about the ideal GCD approach, the solution is actually simple. We do all the work in the background thread, dividing it into cartridges, which are synchronously called in the main thread. The start cycle will be run for each cartridge, user interface updates and any progress indicators, 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 method, but his answer does not completely solve the problem: run a lot of insecure code when the user interface is updated periodically.

+1
Oct 02
source share

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

The dispatch_get_current_queue function will return the current queue from which the block is sent, and the dispatch_get_main_queue function will return the main queue in which your user interface operates.

The dispatch_get_main_queue function is very useful for updating. The iOS application interface as UIKit methods UIKit not thread safe (with a few exceptions), so any calls you make to update user interface elements should always be made 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 is my working example (iOS 6+). It displays the frames of the saved 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 example can be found here in Damian's answer.

+1
Sep 28 '14 at 23:44
source share

Joe - if you are ready to configure it so that your lengthy processing is done inside drawRect, you can make it work. I just wrote a test project. It is working. See code below.

LengthyComputTestAppDelegate.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 
0
Jan 19 2018-11-11T00:
source share



All Articles