Saving and restoring CGContext - iphone

I'm trying to save and restore a CGContext to avoid doing heavy drawing computations for a second time and I'm getting the error <Error>: CGGStackRestore: gstack underflow.
What am I doing wrong? What is the correct way to do this?
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
if (initialized) {
CGContextRestoreGState(context);
//scale context
return;
}
initialized = YES;
//heavy drawing computation and drawing
CGContextSaveGState(context);
}

I think you might be misinterpreting what CGContextSaveGState() and CGContextRestoreGState() do. They push the current graphics state onto a stack and pop it off, letting you transform the current drawing space, change line styles, etc., then restore the state to what it was before you set those values. It does not store drawing elements, like paths.
From the documentation on CGContextSaveGState():
Each graphics context maintains a
stack of graphics states. Note that
not all aspects of the current drawing
environment are elements of the
graphics state. For example, the
current path is not considered part of
the graphics state and is therefore
not saved when you call the
CGContextSaveGState() function.
The graphics state stack should be reset at the start of your drawRect:, which is why you're getting errors when you try to pop a graphics state off the stack. Since you hadn't pushed one on, there was none to pop off. All of this means that you can't store your drawing as graphics state on the stack, then restore it later.
If all you are worried about is caching your drawing, that is done for you by the CALayer that backs your UIView (on the iPhone). If all you are doing is moving your view around, it won't be redrawn. It will only be drawn if you manually tell it to do so. If you do have to update part of the drawing, I recommend splitting the static elements off into their own views or CALayers so that only the part that changes is redrawn.

Don't you want to Save first and then Restore? If you are restoring before a save, then there is no context to restore, and you'd get an underflow.
Here is the way I have used it:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextClipToRect(context, CGRectMake(stripe[i][8], stripe[i][9], stripe[i][10], stripe[i][11]));
CGContextDrawLinearGradient(context, gradient, CGPointMake(15, 5), CGPointMake(15, 25), 0);
CGContextRestoreGState(context);
or:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextAddRect(context, originalRect);
CGContextClip(context);
[self drawInRect:rect];
CGContextRestoreGState(context);
Maybe you are trying to do something else.

.. Based on your code !,
It seems that you are Restoring the Context before Saving it.
First Thing First:
Create a context
Save its state, aka push
Do some stuff with the context
Restore the context aka Pop
General rule for each Store(push) there must be Restore(pop)
Release the context when you are done with it !, This refers to those context/s which they have CGCreate, CGCopy,
Sample code:
[super drawRect:rect];
CGContextRef ctx = UIGraphicsGetCurrentContext();
// save context
CGContextSaveGState(ctx);
// do some stuff
CGContextSetRGBStrokeColor(ctx, 1.0, 0.5, 0.5, 1.0);
// drawing vertical lines
CGContextSetLineWidth(ctx, 1.0);
for (int i = 0; i < [columns count]; i++) {
CGFloat f = [((NSNumber*) [columns objectAtIndex:i]) floatValue];
CGContextMoveToPoint(ctx, f+(i*20.5), 0.5);
CGContextAddLineToPoint(ctx, f+(i*20.5), self.bounds.size.height);
}
// restore context
CGContextRestoreGState(ctx);
// do some other stuff
// drawing hozizontal lines
CGContextSetLineWidth(ctx, 1.0);
CGContextSetRGBStrokeColor(ctx, 0.12385, 0.43253, 0.51345, 1.0);
for (int i = 0; i < [columns count]; i++) {
CGFloat f = [((NSNumber*) [columns objectAtIndex:i]) floatValue];
CGContextMoveToPoint(ctx, 0.5, f+(i*20.5));
CGContextAddLineToPoint(ctx,self.bounds.size.width,f+(i*20.5));
}
CGContextStrokePath(ctx);
}
// No context CGContextRelease , since we never used CGContextCreate

Related

Questions regarding drawing in iOS

I want to create a little navigation bar on the bottom of my iPhone screen where I basically just draw 5 rectangles next to each other. However, only the active page should have the opacity of 1.0 and others should be slightly transparent (alpha=0.4). That is what I already have.
Now my questions:
How do I change the opacity of the individual elements of my navigation ? Do I have to redraw the whole thing whenever something changes ? So I would have global variables called nav1Opacity,nav2Opacity...nav5Opacity, change them when the navigation changes and redraw the whole thing ? If so,
How do I clear what I have drawn before ? Do i create the rectangles as CGMutablePathRef()s and store them in an array and clear them all ?
I have very little experience with drawing, so I am a little lost there. I have read the documentation of Quartz2d and contexts, but still, as I mentioned I have not fully figured out how it works.
Here is some code I use:
-(void)drawRect:(CGRect)rect{
CGContextRef context = UIGraphicsGetCurrentContext();
//save state
CGContextSaveGState(context);
//NAV1
CGMutablePathRef nav1 = CGPathCreateMutable();
CGPathAddRect(nav1, NULL, CGRectMake(0 , 15, 64, 10));
UIColor *blueColor = UIColorFromRGB(0x35BFE5,0.1);
CGColorRef bC = [blueColor CGColor];
[colorArray addObject:(__bridge id)bC];
[navArray addObject:(__bridge id)nav1];
CGPathRelease(nav1);
/*
*
*
... I do this for all 5 navigation elements
*
*
*/
//then I go through all my rectangles and add/fill them
for(int i=0;i<[navArray count];i++){
CGContextAddPath(context, (__bridge CGMutablePathRef)[navArray objectAtIndex:i]);
CGContextSetFillColorWithColor(context, (__bridge CGColorRef)[colorArray objectAtIndex:i]);
CGContextFillPath(context);
}
// restore to last saved context state
CGContextRestoreGState(context);
}
//and this is how I redraw
-(void)updateActiveNav{
[navArray removeAllObjects];
[colorArray removeAllObjects];
[self setNeedsDisplay];
}
If you actually draw the interface, you will have to redraw it whenever it changes, at least the rectangles that change. You can reuse CGPaths, but they aren't graphical objects on screen, they are just instructions on how to draw shapes, so you will have to draw everything again.
That being said, you can use individual UIViews instead, which represent onscreen objects, and you can change their opacity, which will reflect on screen.
This is the problem:
for(int i=0;i<[navArray count];i++){
CGContextAddPath(context, (__bridge CGMutablePathRef)[navArray objectAtIndex:i]);
CGContextSetFillColorWithColor(context, (__bridge CGColorRef)[colorArray objectAtIndex:i]);
CGContextFillPath(context);
}
You are added the path to your context, then setting a fill color, then filling it. Then without restoring your context you're doing it again so your filling the previous path and the new one. Its not the drawing from the last drawRect its the drawing from here. Try something like below so that after you fill the path you reset the context and draw the next block by itself instead of both the 1st and 2nd etc.
for(int i=0;i<[navArray count];i++){
CGContextSaveGState(context);
// Add Path, Fill
CGContextRestoreGState(context);
}

drawRect called after creating a custom UI?

i need to draw two lines. can i use the same UIView subclass to make both draws? after i create the UIView
draw2D *myView = [[draw2D alloc] initWithFrame:myRect];
if i change the method to use variables, can i change those values and recall the drawRect method to draw a different line?
- (void)drawRect:(CGRect)rect
{
CGContextRef context01 = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context01, 1.0);
CGContextSetStrokeColorWithColor(context01, [[UIColor blackColor]CGColor]);
CGContextMoveToPoint(context01, 0, 0);
CGContextAddLineToPoint(context01, 800, 0);
CGContextStrokePath(context01);
CGContextRef context02 = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context02, 1.0);
CGContextSetStrokeColorWithColor(context02, [[UIColor blackColor]CGColor]);
CGContextMoveToPoint(context02, 453, 0);
CGContextAddLineToPoint(context02, 453, 800);
CGContextStrokePath(context02);
}
Just call setNeedsDisplay on a view to force its drawRect method to be called again.
It doesn't redraw the view immediately, but it flags it as needing to be drawn again in the next view update cycle (updates happen roughly every 60th second). That means you can call setNeedsDisplay multiple times with no performance penalty.
can i use the same UIView subclass to make both draws?
sure
if i change the method to use variables, can i change those values and recall the drawRect method to draw a different line?
normally, you'd just create a new function or method with parameters for those variables:
static inline void imp_DrawLine(CGContextRef gtx, CGPoint start, CGPoint end) {
CGContextMoveToPoint(gtx, start.x, start.y);
CGContextAddLineToPoint(gtx, end.x, end.y);
CGContextStrokePath(gtx);
}
- (void)drawRect:(CGRect)rect
{
CGContextRef gtx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(gtx, 1.0);
CGContextSetStrokeColorWithColor(gtx, [UIColor blackColor].CGColor);
imp_DrawLine(gtx, CGPointMake(0, 0), CGPointMake(800, 0));
imp_DrawLine(gtx, CGPointMake(453, 0), CGPointMake(453, 800));
}

Problem in drawing square in sample app

Hi i am making a sample app in which i wanto create a square for which i used the following code
- (void)viewDidLoad {
[super viewDidLoad];
[self drawRect:CGRectMake(0, 0, 300, 200)];
[[self view] setNeedsDisplay];
}
- (void) drawRect:(CGRect)rect
{
NSLog(#"drawRect");
CGFloat centerx = rect.size.width/2;
CGFloat centery = rect.size.height/2;
CGFloat half = 100/2;
CGRect theRect = CGRectMake(-half, -half, 100, 100);
// Grab the drawing context
CGContextRef context = UIGraphicsGetCurrentContext();
// like Processing pushMatrix
CGContextSaveGState(context);
CGContextTranslateCTM(context, centerx, centery);
// Uncomment to see the rotated square
//CGContextRotateCTM(context, rotation);
// Set red stroke
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
{
CGContextSetRGBFillColor(context, 0.0, 1.0, 0.0, 1.0);
}
// Draw a rect with a red stroke
CGContextFillRect(context, theRect);
CGContextStrokeRect(context, theRect);
// like Processing popMatrix
CGContextRestoreGState(context);
[[self view] setNeedsDisplay];
}
But nothing is drawn on screen , dont know wheres the issue is .When i debug it the CGContextRef context was always 0x0 , i dont know why its 0x0 always am i missing something in my code.
It looks like you're trying to draw in a subclass of UIViewController. You need to subclass UIView to override the drawRect: method, which is then called automatically with a valid graphics context in place. You almost never call this method yourself.
To quote the Apple docs:
"To draw to the screen in an iOS application, you set up a UIView object and implement its drawRect: method to perform drawing. The view’s drawRect: method is called when the view is visible onscreen and its contents need updating. Before calling your custom drawRect: method, the view object automatically configures its drawing environment so that your code can start drawing immediately. As part of this configuration, the UIView object creates a graphics context (a CGContextRef opaque type) for the current drawing environment. You obtain this graphics context in your drawRect: method by calling the UIKit function UIGraphicsGetCurrentContext."
So essentailly, your code is on track, you just need to get it in the right place. It needs to be in the View object.

iPhone - How to draw something in a view

I'v got this piece of code :
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
CGFloat colors[] = {
1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
};
CGGradientRef gradientRef = CGGradientCreateWithColorComponents(rgb, colors, NULL, sizeof(colors) / (sizeof(colors[0]) * 4));
CGColorSpaceRelease(rgb);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = theCell.backgroundView.bounds;
CGPoint start = CGPointMake(rect.origin.x, 0);
CGPoint end = CGPointMake(rect.origin.x, rect.size.height/2);
CGContextDrawLinearGradient(context, gradientRef, start, end, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
And I wonder how I can make it draw into a designated view and a clipping rect passed in parameters to my function. Tip: I don't mind about drawRect, I'm not subclassing anything.
Tip 2: I don't want to insert any layers that I won't be able to remove later.
Tip 3: This piece of code does not draw anything that my eyes could see..... :-( Missing a graphic port ?
Tip 4: I'd like to erase the draw simply changing the background color, and it's done...
CGContextRef context = UIGraphicsGetCurrentContext(); will get the graphics context for the current view, so if you call this in the drawRect: method of a UIView it will draw.
I don't understand what you mean by:
I don't mind about drawRect, I'm not
subclassing anything
but if you want to do custom drawing you must either override the drawRect: method or use layers. To use layers you would call CGContextRef context = CGLayerGetContext(theLayer); instead of CGContextRef context = UIGraphicsGetCurrentContext();.
Ok, so I looked at the documentation and it says that you can get a CGContextRef by calling UIGraphicsGetCurrentContext() from the drawRectMethod. Here's what it says: In an iOS application, you set up a UIView object to draw to and implement the drawRect: method to perform drawing. Before calling your custom drawRect: method, the view object automatically configures its drawing environment so that your code can start drawing immediately. As part of this configuration, the UIView object creates a graphics context (a CGContextRef opaque type) for the current drawing environment. You obtain this graphics context by calling the UIKit function UIGraphicsGetCurrentContext. You save and restore graphics contexts using the functions UIGraphicsPushContext and UIGraphicsPopContext.
You can create custom graphics context objects in situations where you want to draw somewhere other than your view. For example, you may want to capture a series of drawing commands and use them to create an image or a PDF file. To create the context, you use the CGBitmapContextCreate or CGPDFContextCreate function. After you have the context, you can pass it to the drawing functions needed to create your content.
When creating custom contexts, the coordinate system for those contexts is different from the native coordinate system used by iOS. Instead of the origin being in the upper-left corner of the drawing surface, it is in the lower-left corner and the axes point up and to the right. The coordinates you specify in your drawing commands must take this into consideration or the resulting image or PDF file may appear wrong when rendered. See “Creating a Bitmap Graphics Context” and “Creating a PDF Graphics Context” for details on using CGBitmapContextCreate and CGPDFContextCreate.
Might I recommend you look into the CAGradientLayer, and add it as a sublayer of your view? Lots simpler, and it will be hardware accelerated which matters for table cells.
Example stolen partly from here:
http://tumbljack.com/post/188089679/gpu-accelerated-awesomeness-with-cagradientlayer
#import <QuartzCore/QuartzCore.h> // Also import this framework
......
CAGradientLayer grad = [CAGradientLayer layer];
UIColor *colorOne = [UIColor colorWithHRed:1.0f Green:1.0f Blue:1.0f alpha:1.0f];
UIColor *colorTwo = [UIColor colorWithHRed:0.0f Green:0.0f Blue:0.0f alpha:1.0f];
NSArray *colors = [NSArray arrayWithObjects:(id)colorOne.CGColor, colorTwo.CGColor, nil];
grad.colors = colors;
CGRect rect = theCell.backgroundView.bounds;
rect.size.height = rect.size.height / 2;
grad.frame = rect;
[self.layer addsublayer:grad]
You may have to play with the colors a bit, not sure if you had the gradient tilted or not...

Painting on UIImageView with better performance

I have a view in which I paint a dotted line when the user moves their finger across the screen of the iPad/iPhone. I draw the line between a point stored as LastLocation, and the point that is the CurrentLocation. The drawings are to be persistant on the screen.
This happens every time the touchMoved event is fired, and ends up letting me draw a dotted line tracing where the person has dragged their finger.... just like a painting application.
I have a function that gets called that does the following when the touch event is fired.
The view contains a UIImageView called drawImage. I use UIImageView as a means of persisting the lines drawn. This clearly isnt how people usually do paint applications, as it is quite slow.
Any insight in to a better way to do persistant painting using the CGContextRef calls would be appreciated.
/* Enter bitmap graphics drawing context */
UIGraphicsBeginImageContext(frame.size);
/* Draw the previous frame back in to the bitmap graphics context */
[drawImage.image drawInRect:CGRectMake(0, 0, drawImage.frame.size.width, drawImage.frame.size.height)]; //originally self.frame.size.width, self.frame.size.height)];
/* Draw the new line */
CGContextRef ctx = UIGraphicsGetCurrentContext();
/* Set line draw color to green, rounded cap end and width of 5 */
CGContextSetLineDash(ctx, 0, dashPattern, 2);
CGContextSetStrokeColorWithColor(ctx, color);
CGContextSetLineCap(ctx, kCGLineCapRound); //kCGLineCapSquare, kCGLineCapButt, kCGLineCapRound
CGContextSetLineWidth(ctx, 5.0); // for size
/* Start the new path point */
CGContextMoveToPoint(ctx, LastLocation.x, LastLocation.y);
CGContextAddLineToPoint(ctx, Current.x, Current.y);
CGContextStrokePath(ctx);
/* Push the updated graphics back to the image */
drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
The 'drawImage.image drawInRect' call is extremely slow, and is in essence redrawing the entire image.
Is there a faster way of doing this? I have seen drawing code like this in a few places on blogs for painting, but it's just a bit slow.
Would love to hear some thoughts on the topic.
There is no need to composite the image and line manually. Have the view that draws the line above another UIImageView that draws the image. Let the system do the compositing and draw the image.
In your code, just do the stuff between the two drawImage lines in the drawRect: method of the line drawing view.
-(void) drawRect:(CGRect)dirty {
CGContextRef ctx = UIGraphicsGetCurrentContext();
/* Set line draw color to green, rounded cap end and width of 5 */
CGContextSetLineDash(ctx, 0, dashPattern, 2);
CGContextSetStrokeColorWithColor(ctx, color);
CGContextSetLineCap(ctx, kCGLineCapRound); //kCGLineCapSquare, kCGLineCapButt, kCGLineCapRound
CGContextSetLineWidth(ctx, 5.0); // for size
/* Start the new path point */
CGContextMoveToPoint(ctx, LastLocation.x, LastLocation.y);
CGContextAddLineToPoint(ctx, Current.x, Current.y);
CGContextStrokePath(ctx);
}
When one end of the line moves, save the new point and mark the line drawing view as needing to display. So both Current and LastLocation should be members of the line drawing view, and in the setter method for each, call setNeedsDisplay.
Make sure clearsContextBeforeDrawing is YES and opaque is NO for the line drawing view.