Search code examples
iosobjective-cgestures

iOS drawing context NULL in gesture handler


I am having trouble trying to draw in iOS in response to Gesture events, and I'm sure it's because I'm probably missing some non-obvious fundamental iOS programming concept.

Anyway, it seems that when code is executing in response to an event, UIGraphicsGetCurrentContext() returns NULL. I don't understand why. Other drawing code, in the View's drawRect method, works just fine.

I can see that the events are being fired as I am logging that. It's just that UIGraphicsGetCurrentContext returns null when executing inside event handling code (even when that code is a function in the View class that set up the event handling?

Additionally, this error gets logged:

<Error>: CGContextSetFillColorWithColor: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context  and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.

Here's the relevant code bits, I'm hoping something obvious sticks out:

@implementation MainView: UIView

- (id)initWithCoder:(NSCoder *)aDecoder;
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self setupEvents];
    }
    return self;
}

- (void)setupEvents {
    UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
                                             initWithTarget:self
                                             action:@selector(showGestureForTapRecognizer:)];
    tapRecognizer.numberOfTapsRequired = 1;
    [self addGestureRecognizer:tapRecognizer];
}

- (IBAction)showGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer {

    // Get the location of the gesture
    CGPoint location = [recognizer locationInView:self];
    NSLog(@"got a tap at (%f,%f)", location.x, location.y);
    [self drawCircleX:location.x Y:location.y Radius:5 Color:([UIColor blackColor].CGColor)];
}

- (void) drawCircleX:(int)x Y:(int)y Radius:(int)radius Color:(CGColorRef)fillColor
{
    CGContextRef context = UIGraphicsGetCurrentContext();  // null if called from event handler
    NSLog(@"context=0x%08lx", context);
    //    CGContextSaveGState(context);
    CGRect rect = CGRectMake(x-radius,y-radius, radius*2, radius*2);
    CGContextSetFillColorWithColor(context, fillColor);
    CGContextFillEllipseInRect(context, rect);
    //    CGContextRestoreGState(context);
}

I've burned days trying to figure this out, reading tons of docs, but to no avail. What gives? (ios newbie)


Solution

  • Drawing on iOS doesn't work the way you think it works. You cannot just draw whenever you want. You have to tell UIKit that something needs to be redrawn, and then, at some point in the future, it will start drawing and your drawing code will be executed.

    The reason it works this way is simple: what if something else is currently on top of the circle you want to draw? That means you can't just redraw your circle, the other thing has to be redrawn at the same time - because it might have anti-aliasing or something that requires knowing what is underneath it.

    So, at the end of showGestureForTapRecognizer, just do this:

    [self setNeedsDisplayInRect:...rect that needs display...];
    

    Or alternatively (if you can't be bothered figuring out the rect):

    [self setNeedsDisplay];
    

    And then implement drawRect:

    - (void)drawRect:(CGRect)rect
    {
      // do your drawing here
    }
    

    Finally... this is the "old" method of implementing view drawing. It has serious performance problems especially on ARM processors.

    What you should really be doing is creating a series of CALayer objects (for example, CAShapeLayer to create an ellipse) which will draw themselves. You subclass CALayer and implement drawInContext: if there is no built in class. This is the modern approach, and perhaps more complicated than you're looking for. But it is the better option, as it has better performance and supports animation nicely.

    If you've ever done HTML/CSS or worked with SVG, that is roughly the same as how CALayer works. You have a nested tree of things that will be drawn to the screen, and you modify the data rather than drawing directly to the GPU.