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
Related
I have two square images in my UIVIew. Once I drag my finger from one image to another image I want to draw a straight line between them.
I have handled touchesMoved method to check when my touchLocation reaches the frame of either of the images. So I have handled the logic part of when to start drawing and between which two points.
I just cant figure out how to do that using (void)drawRect:(CGRect)rect. For one thing, I addded an NSlog in my drawRect and I wrote code to draw a line between two lines, even that's not happening.
I checked this question too, but I want to continue to drag and draw lines between multiple points.
- (void)drawRect:(CGRect)rect
{
NSLog(#"Draw Rect Entered");
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor]CGColor]);
CGContextSetLineWidth(context, 1.0);
CGContextMoveToPoint(context, 0, 0);
CGContextAddLineToPoint(context, 20, 20);
CGContextStrokePath(context);
}
In order for the view to update you need to call -setNeedsDisplay or -setNeedsDisplayInRect: on the view that needs to be redrawn. You should do this in the touch handler once you have determined you need to draw the line. This will trigger the -drawRect: method on the view- you don't call -drawRect directly. For more information, read the UIView docs. I don't know whether you want your line to exist permanently once created- if there's a possibility it might be deleted, I'd add an if statement to -drawRect: controlled by a boolean determining whether to draw the line or not. Then you can easily switch it off if necessary.
Based on your comments below, I'd create an ivar array in which to store the points for the currently traced line and then loop over them in -drawRect:, using the Core graphics functions to draw the line a segment at a time. That way the new segment and all previous segments will be drawn with each redraw. Since CGPoint isn't an obj-c object, you'll either have to create a wrapper class or use obj-C++ and std::vector. You could also use a fixed size C-Array, (you mention you have precisely 10 points)- in that case you'd have to preset the unused coordinates to some arbitrary value defined as invalid (e.g. -500,-500) and add logic to not draw line segments if those values are encountered.
Minor point- I wouldn't hardcode the line coordinates - instead you could derive them from the images' frame property. That way your code will be more readable and won't break if you change the images or resize them.
There are two issues I have run into that could cause this problem:
1) Make sure you are calling setNeedsDisplay on the main thread
2) If you are working with multiple views make sure you are calling setNeedsDisplay against the correct one.
It is easy to work with one view and forget to call setNeedsDisplay against the parent view.
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.
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.
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.
I have an application with a custom CALayer subclass. In this layer subclass i have overwritten the - (void) drawInContext:(CGContextRef)ctx method. This works fine and all my custom content is drawn.
Now I wanted to add a custom property which gets animated when it's content is changed. And I need to redraw while the animation is running. I added the property like given in the answer to the following question: Animating a custom property of CALayer subclass
And I also added an CABasicAnimation instance for this property to my layer:
CABasicAnimation* theAnimation=[CABasicAnimation animationWithKeyPath:#"moveContentToLeft"];
theAnimation.duration=1.0;
theAnimation.fromValue=[NSNumber numberWithFloat:0.0];
theAnimation.toValue=[NSNumber numberWithFloat:10.0];
[area addAnimation:theAnimation forKey:#"moveContentToLeft"];
In my draw method I have a NSLog statement in which I output the value of my property I'm animating.
My problem is that as soon as the animation starts all the content of the layer subclass is cleared. The NSLog outputs the values of my property that gets interpolated (so the drawInContext method) is called. But somehow the things it draws are not visible during the animation. At the end of the animation the original content gets visible again.
At the moment the animated property is not yet used while drawing the layer so I would expect that the normal content would get drawn again and I get the output of the NSLog with the interpolated values.
My layer is a child of another layer (inside a UIView). I tried to redraw the super layer at the end of the drawing method (with calling _parent setNeedsDisplay [parent is a ivar to the parent view]). This did not help. And I also have the feeling that this is not a good idea to do so.
I hope somebody can help me with this problem.
Update
Thanks to the example project from Matt Long I could see that my problem lies inside the drawing method. I extended his project so it shows the same problem. I think it is simpler to show than the original code ;)
- (void)drawInContext:(CGContextRef)ctx
{
NSLog(#"Getting called: Bob: %i", [self bob]);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(50 + [self bob], 50, 100, 100));
CGContextAddPath(ctx, path);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 1.0);
CGContextStrokePath(ctx);
CGPathRelease(path);
[_test testInContext:ctx]; // here is the problem
}
The ivar _test is a custom class which contains some drawing code (at the moment it draws a green box). The problem now is that this green box will not be drawn while the animation runs. As soon as the animation is ended the green box is visible again. The extended example project: http://dl.dropbox.com/u/5426092/ArbitraryPropertyAnimationNew.zip
It's hard to say what's going wrong without seeing more code. I have an example app that animates a custom property and the position of a layer at the same time. You can take a look at my source and see what's different.