On iOS, why UIBezierPath drawing doesn't require a context? - iphone

On iOS, we can draw a line in drawRect using
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginPath (context);
CGContextMoveToPoint(context, 0, 0);
CGContextAddLineToPoint(context, 100, 100);
CGContextStrokePath(context);
but we can also draw a rectangle if we remove the above code, and just use:
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 100)];
[path stroke];
Two related questions:
1) Why doesn't UIBezierPath need to get or use the current context?
2) What if I have two context: one for screen, and one is a bitmap context, then how to tell which context to draw to for UIBezierPath? I thought it might be UIGraphicsSetCurrentContext but it doesn't exist.

UIBezierPath does use a context. It uses the current UIKit graphics context. This is exactly the same thing that you're already getting with UIGraphicsGetCurrentContext().
If you want UIBezierPath to use a different context you can use UIGraphicsPushContext(), but you have to remember to use UIGraphicsPopContext() when you're done.

I thought it might be useful to mention that CGContextFillRect is ~8.5x faster than using a UIBezierPath from what I can tell (in case performance is a factor and assuming you don't need to use a UIBezierPath for more complex drawing).
I added some timing to Apple's HazardMap example (http://developer.apple.com/library/ios/#samplecode/HazardMap/Introduction/Intro.html) and the time in ms per rect is ~0.00064 ms/rect for the CGContextFillRect approach versus ~0.00543 ms/rect for the UIBezierPath approach, presumably b/c the latter requires more message passing overhead.
i.e. I'm comparing using
CGContextFillRect(ctx, boundaryCGRect);
versus using
UIBezierPath* path = [UIBezierPath bezierPathWithRect:boundaryCGRect];
[path fill];
in the inner loop in HazardMapView (plus the above-mentioned changes to push / pop the context that is passed to HazardMapView drawMapRect:zoomScale:inContext:).
ETA

On iOS, we can draw a line in drawRect using
I've highlighted the important part of this statement. Inside of drawRect:, a context has already been set up for you by UIKit, and any object-based drawing instructions go directly into that context. UIBezierPath is indeed using that context, it just doesn't need to be passed in explicitly.
In Cocoa Touch, there must always be a drawing context (in this case, the context will eventually be painted onto the screen). If you were not inside drawRect:, you'd have to create a context yourself.
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginPath (context);
CGContextMoveToPoint(context, 0, 0);
Notice that the first function call is GetCurrentContext(). When you're using CoreGraphics' functional drawing interface, you need to pass a context into each function, but you're not creating one here, you're just retrieving the one that already exists.
Graphics contexts are in a stack. If you want to draw into a context you've created, you push it onto the stack using UIGraphicsPushContext() (as Kevin already mentioned), then pop back to the previous one.

Related

Drawing in drawRect is always called but doesn't always produce anything

I'm having a strange issue at the moment. I'm generating a drawing in a UIViewControllers UIView which calls drawRect:, goes through the entire code process without crashing but produces nothing. If I click on a UI object inside of the UIView the drawRect gets reloaded and my image appears.
The only good thing with this is that it happens every time at first load but it also happens some times later.
This usually works so am I missing something like a reload/set function?
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
CGContextSetRGBFillColor(context, 1, 1, 1, 1.0f);
CGContextFillRect(context, rect);
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetLineWidth(context, 5.0);
CGContextMoveToPoint(context, 20, 20);
CGContextAddLineToPoint(context, 40, 40);
CGContextStrokePath(context);
Is your code calling -drawRect: directly? If so, stop that. Before calling -drawRect:, UIView sets up the drawing context so that all your -drawRect: override has to do is draw. If you try to call that method directly the drawing context won't be set up correctly and your drawing will end up in the wrong place, or nowhere at all. The drawing works correctly for you when you let the system manage drawing, in which case the context will be properly set before -drawRect: is called.
Sounds like your drawRect: code is not getting called when you expect it to be. Be sure you are calling setNeedsDisplay on your UIView as needed.

How to create a CGLayer from a UIView for off-screen drawing

I have read what I believe to be the relevant parts of the Quartz 2D Programming Guide, but cannot find an answer to the following (they don't seem to talk a lot about iOS in the document):
My application displays a drawing in a UIView. Every now and then I have to update the drawing in some way, e.g. change the fill colour of one of the shapes (I keep CGPathRefs to the important shapes to be able to redraw them with a different fill colour later). As described in the Section "Drawing With a CGLayer" on page 169 of the aforementioned document, I was thinking of drawing the entire drawing into a CGContext that I would obtain from a CGLayer, like so:
CGContextRef offscreenContext = CGLayerGetContext(offscreenLayer);
Then I could do my updating off-screen into the CGContext and draw the CGLayer into my UIView in the UIView's drawRect: method, like so:
CGContextDrawLayerAtPoint(viewContext, CGPointZero, offscreenLayer);
The problem I am having is, where do I get my CGLayer from? My understanding is I have to make it using CGLayerCreateWithContext and supply a CGContext as a parameter from which it inherits most of it's properties. Obviously, the right context would be the context of the UIView, that I am getting with
CGContextRef viewContext = UIGraphicsGetCurrentContext();
but if I am not mistaken, I can only get that within the drawRect: method and it is not valid to assume that the context I am given there will be the same one next time the method is called, i.e. I can only use that CGContext locally within the method.
So, how can I get a CGContext that I can use to initialise my CGLayer to create an offscreen CGContext to draw into and then draw the entire layer back into my UIView's CGContext?
PS: While you're at it; if anything above does not make sense or is not sane, please let me know. I am just starting to get my head around Quartz 2D.
First of all, if you are doing it from in an iOS environment, I think you are right. The documentation clearly said that the only way to obtain a CGContextRef is by
CGContextRef ctx = UIGraphicGetCurrentContext();
Then you use that context for creating the CGLayer with
CGLayerRef layer = CGLayerCreateWithContext(ctx, (CGSize){0,0}, NULL);
And if you want to draw on that layer, you have to draw it with the context you get from the layer. (It is somewhat different from the context you passed in earlier to create the CGLayer). Im guessing the CGLayerCreateWithContext saves the information it can get from the context passed in, but not everything. (One of the example is the ColorSpace information, you have to re-specify when you fill something with the context from CGLayer).
You can get the CGLayer context reference from the CGLayerGetContext() function and use that to draw.
CGContextRef layerCtx = CGLayerGetContext(layer);
CGContextBeginPath(layerCtx);
CGContextMoveToPoint(layerCtx, -10, 10);
CGContextAddLineToPoint(layerCtx, 100, 10);
CGContextAddLineToPoint(layerCtx, 100, 100);
CGContextClosePath(layerCtx);
One point that I found out is when you draw something offscreen, it automatically clips the thing offscreen. (make sense, so it doesnt draw things that is not seen) but when you move the layer (using the matrix transformation). The clipped path is not showing (missing).
One last thing, if you save the reference to a layer into a variable and later on you want to draw it, you can use CGContextDrawLayerAtPoint() method like
CGContextDrawLayerAtPoint(ctx, (CGPoint) {newPointX, newPointY}, layer);
It will sort of "stampt" or "draw" the layer at that newPointX and new PointY coordinate.
I hope that answer your question, if its not please let me know.

iPhone Core Graphics thicker dashed line for subview

I have a UIView and within it I've drawn a line using Core Graphics by overriding drawRect. This view also contains one subview which also draws a line. However, whilst both views are using pretty much the same code (for testing purposes at least), the lines drawn on them do not appear the same:
As you can see - the dashed line at the top is noticeably thicker than the bottom one and I have no idea why. Below is the code used by the two UIViews in their drawRect methods. If you have any idea why this is happening then I'd appreciate your help and advice!
First View:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [[UIColor whiteColor] CGColor]);
CGFloat dashes[] = {1,1};
CGContextSetLineDash(context, 0.0, dashes, 2);
CGContextSetLineWidth(context, 0.6);
CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMaxY(rect));
CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
CGContextStrokePath(context);
SubUIView *view = [[SubUIView alloc] initWithFrame:rect];
[self addSubview:view];
[view release];
The view is definitely only being drawn once. I appreciate drawRect may not be the best place for adding a subview but the problem remains even with it added in the main initWithFrame method.
Second View:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [[UIColor whiteColor] CGColor]);
CGFloat dashes[] = {1,1};
CGContextSetLineDash(context, 0.0, dashes, 2);
CGContextSetLineWidth(context, 0.6);
CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMidY(rect));
CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMidY(rect));
CGContextStrokePath(context);
It could be a result of anti-aliasing if your rect does not fall on integers. You can disable anti-aliasing with CGContextSetShouldAntialias( context, NO ). I think there's also a function for making a rect integral, but I can't remember what it is.
First, you should fix the problem of the drawing code being WET*. You say you're doing this “for testing purposes”, but this actually makes testing harder, since you have to change both pieces of code and/or keep straight which version you're working on. The worst case is when you change both pieces of code in different ways and have to merge them by hand.
I'd say move the dashed-line code to the subview, add properties for anything the two pieces of code need to do differently, and create two subviews (and not in drawRect:—seriously).
As for the actual problem: Well, I don't see a big image, I see a tiny image, and I can only guess that the greater boldness of the upper line than that of the lower line means that the upper line is thicker.
By the way, the rect is not necessarily the bounds of your image. Don't ever assume that it is, or you will get funky drawing when it isn't. Assume that it some section of the bounds—possibly, but possibly not, the whole thing. When you mean [self bounds], say [self bounds].
The problem is most likely the difference between CGRectGetMidY([self bounds]) and CGRectGetMaxY([self bounds]). One includes a fraction that splits a pixel, whereas the other is a multiple of one pixel or close to it. (Not necessarily a multiple of 1—on a Retina Display, 1 pt = 2 pixels, so 1 pixel = 0.5 pt.) Try flooring both numbers and optionally adding 0.5, and see which way you like better.
There's no way to make it work out perfectly with a 0.6-pt line width. There is simply no whole number of pixels that works out to. All you can do is work out what looks best and do that.
*Written Elsewhere Too, the opposite of DRY.

How to reset the context to the original rectangle after clipping it for drawing?

I try to draw a sequence of pattern images (different repeated patterns in one view).
So what I did is this, in a loop:
CGContextRef context = UIGraphicsGetCurrentContext();
// clip to the drawing rectangle to draw the pattern for this portion of the view
CGContextClipToRect(context, drawingRect);
// the first call here works fine... but for the next nothing will be drawn
CGContextDrawTiledImage(context, CGRectMake(0, 0, 2, 31), [img CGImage]);
I think that after I've clipped the context to draw the pattern in the specific rectangle, I cut out a snippet from the big canvas and the next time, my canvas is gone. can't cut out another snippet. So I must reset that clipping somehow in order to be able to draw another pattern again somewhere else?
Edit: In the documentation I found this:
CGContextClip: "... Therefore, to
re-enlarge the paintable area by
restoring the clipping path to a prior
state, you must save the graphics
state before you clip and restore the
graphics state after you’ve completed
any clipped drawing. ..."
Well then, how to store the graphics state before clipping and how to restore it?
The functions you are looking for are:
CGContextSaveGState(context);
and
CGContextRestoreGState(context);

Quartz 2D Graphics Contexts

Why do Quartz 2D Graphics Contexts functions have to be called from within the drawRect method?
Because if I call a CGGraphics context function from anywhere except from within drawRect I get messages like this:
<Error>: CGContextFillRects: invalid context
<Error>: CGContextSetFillColorWithColor: invalid context
Actually in a subclass of UIView I do setup a Graphics Context in a method called Render. Bet when Render is called I get the above errors:
- (void)Render {
CGContextRef g = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(g, [UIColor blueColor].CGColor);
CGContextFillRect(g, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height));
CGContextSetFillColorWithColor(g, [UIColor blackColor].CGColor);
[#"It works!" drawAtPoint:CGPointMake(10.0, 20.0) withFont:[UIFont systemFontOfSize:[UIFont systemFontSize]]];
NSLog(#"gsTest Render");
}
They don't. Maybe you should elaborate a little more.
Cocoa sets up a context for you before calling your drawRect implementation. If you want to draw something somewhere else then that setup work is your responsibility.
UIGraphicsGetCurrentContext retrieves the graphics context from a stack which is set up by the framework. UIKit guarantees that when the drawRect method is called, a valid graphics context has been pushed onto this stack. After you return from it, this stack gets popped. If you call it outside the drawRect function it will not be valid.
Instead, if you want to call it outside drawRect, you need to create/obtain your own graphics context and draw onto that.
Some drawing functions, such as the NSString drawAtPoint:withFont: make use of this stack also; if the current context is not valid you will need to call UIGraphicsPushContext