Search code examples
objective-cmultithreadingiosuiviewdrawrect

Problem between context and thread in drawRect


I'm trying to draw a couple of UIImages in a UIView, and I'm doing it by hand with drawRect:. Given that the images are dynamically downloaded from the web, I create an NSOperation and I perform the image loading code from a second thread, in order to keep UI responsive. So once the images are downloaded, they appear on screen.

But... I get the following errors, for each of the images:

<Error>: CGContextSaveGState: invalid context 0x0
<Error>: CGContextSetBlendMode: invalid context 0x0
<Error>: CGContextSetAlpha: invalid context 0x0
<Error>: CGContextTranslateCTM: invalid context 0x0
<Error>: CGContextScaleCTM: invalid context 0x0
<Error>: CGContextDrawImage: invalid context 0x0
<Error>: CGContextRestoreGState: invalid context 0x0

For what I've seen, this means that the context in which I'm trying to make [image drawInRect...] is nil, because I'm not in drawRect: anymore.

Attempted doing

UIGraphicsPushContext(UIGraphicsGetCurrentContext());

before drawing the image but nothing changed. How can I overcome this thing? Multithreading is a must for responsiveness, and the context gets lost in the way.

UPDATE: Here's my code:

- (void)drawContentView:(CGRect)r
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    ....
    CGContextFillRect(context, r);
    UIApplication* app = [UIApplication sharedApplication];
        app.networkActivityIndicatorVisible = YES;


    NSOperationQueue *queue = [NSOperationQueue new];
    NSInvocationOperation *operation = [[NSInvocationOperation alloc]
                                            initWithTarget:self 
                                            selector:@selector(loadPreview) 
                                            object:nil];
    [queue addOperation:operation];
    [operation release];
}

And then

-(void)loadPreview
{
    self.preview = [UIImage imageWithData: [NSData dataWithContentsOfURL: [NSURL URLWithString:self.picStringPreview]]];
    [self.preview drawInRect:CGRectMake(256, 18, 80, 65)];
}

I tried adding a UIGraphicsPushContext... in loadPreview, even doing a [self performSelectorOnMainThread...] there, and nothing. And this is a custom cell based on Atebit's model, so my cell's superclass makes:

- (void)drawRect:(CGRect)r
{
    [(ABTableViewCell *)[self superview] drawContentView:r];
}

and the code in drawContentView: gets draw'd in superclass's drawRect:. Any hint?


Solution

  • You're quite correct about the context being gone by the time you get to drawing the images. UIKit sets up a drawing context for your view before drawRect: is called, and it's destroyed afterwards. As a general rule, there will only, and always, be a context set up for you like this in a draw... method that you've inherited from a framework class.

    So what can you do? It should actually be rather easy. Anomie's answer already told you, in fact. All you need to do is call the draw method on your image back in drawContentView: where you know you have a context. In the loadPreview method, you call setNeedsDisplay*, and in drawContentView: you check if preview is nil, drawing if not. Just to expand on Anomie's answer, here's what the code should look like:

    -(void)loadPreview
    {
        self.preview = [UIImage imageWithData: [NSData dataWithContentsOfURL: [NSURL URLWithString:self.picStringPreview]]];
        [self setNeedsDisplay];
    }
    

    - (void)drawContentView:(CGRect)r
    {
        CGContextRef context = UIGraphicsGetCurrentContext();
        ....
        CGContextFillRect(context, r);
        UIImage * prvw = self.preview;    // Avoid making two method calls
        if( prvw ){
            [prvw drawInRect:CGRectMake(256, 18, 80, 65)];
        }
        // etc.
    

    You would also probably do well to move the creation and dispatching of the NSOperation out of the drawing method. It's recommended that all drawing methods do only what they need to do and terminate as quickly as possible.


    *I think Anomie might be wrong about needing to call this through performSelector:...onMainThread:, because setNeedsDisplay doesn't call drawRect: directly, it just sets a flag.