drawRect called after creating a custom UI? - iphone

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));
}

Related

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...

CATiledLayer drawing crash

in my application I use have the following views hierarchy:
UIView
----UIScrollView
--------TiledView (UIView subclass, uses CATiledLayer for drawing)
----OverlayView (UIView subclass)
In short - TiledView displays large tiled image I also apply custom rotation to that view:
tiledView.transform = CGAffineTransformMakeRotation(angle);
Drawing method for TiledView:
- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
...
NSString *fileName = [NSString stringWithFormat:#"tile_%d_%d.jpg", y + 1, x + 1];
UIImage *image = [UIImage imageNamed:fileName];
[image drawInRect:rect];
}
UIScrollView allows both scrolling and zooming of its contents.
Overlay view lays over UIScrollView, has transparent background and performs some custom drawing. I use separate view to make sure that line width and font size are not affected by zoom scale in scroll view.
Drawing method for OverlayView:
- (void)drawRect:(CGRect)rect {
// Drawing code
[super drawRect:rect];
// Custom drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 0, 1);
CGContextSetLineWidth(context, lineWidth);
CGContextBeginPath(context);
CGContextMoveToPoint(context, (int)startPoint.x,(int) startPoint.y);
if (usesMidPoint)
CGContextAddLineToPoint(context, (int)midPoint.x, (int)midPoint.y);
CGContextAddLineToPoint(context, endPoint.x, endPoint.y);
CGContextStrokePath(context);
}
Initially everything works ok, but after some playing with the view (e.g. scrolling it to and fro etc) it crashes on some random line in one of the drawing functions. As an example app crashes on line:
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 0, 1);
with stack:
#0 0x00503088 in CGColorEqualToColor
#1 0x00505430 in CGGStateSetStrokeColor
#2 0x005053b6 in setStrokeColorWithComponents
#3 0x0056150f in CGContextSetRGBStrokeColor
#4 0x000764ab in -[GADrawView drawRect:] at GADrawView.m:38
#5 0x016a7a78 in -[UIView(CALayerDelegate) drawLayer:inContext:]
#6 0x0077c007 in -[CALayer drawInContext:]
Am I missing any synchronization required between several graphics contexts? Or may be there's better way to do what I'm trying?
Found solution to the problem. As described in Apple's technical note CATiledLayer uses separate thread to fetch the contents for its tiles:
The CATiledLayer implements its
drawing by using a background thread
to fetch the contents of each tile,
however the drawing functions provided
by UIKit rely on a global context
stack, and as such when a CATiledLayer
starts rendering, using any of UIKit's
drawing functions can result in a race
condition.
So the solution is to move all drawing code from -drawRect: method to -drawLayer:inContext: and draw using only Core Graphics functions. Drawing with CoreGraphics also requires some transformations between coordinates systems - this post from apple forums helped with them (see drawLayer: method implementation).
So the correct drawing code for TiledView:
- (void)drawRect:(CGRect)rect {
//Still need to have this method implemented even if its empty!
}
-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
{
// Do all your drawing here. Do not use UIGraphics to do any drawing, use Core Graphics instead.
// convert the CA coordinate system to the iPhone coordinate system
CGContextTranslateCTM(ctx, 0.0f, 0.0f);
CGContextScaleCTM(ctx, 1.0f, -1.0f);
CGRect box = CGContextGetClipBoundingBox(ctx);
// invert the Y-coord to translate between CA coords and iPhone coords
CGPoint pixelTopLeft = CGPointApplyAffineTransform(box.origin, CGAffineTransformMakeScale(1.0f, -1.0f));
NSString *tileUrlString = [self urlForPoint:pixelTopLeft];
UIImage *image = [UIImage imageNamed:tileUrlString];
CGContextDrawImage(ctx, box, [image CGImage]);
}

Saving and restoring CGContext

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

Am I responsible for clipping when doing custom rendering using drawRect:?

When I call setNeedsDisplayInRect on a UIView, and the drawRect: method fires, am I responsible for making sure I'm not rendering stuff that's outside the CGRect I called, or will the framework handle that for me?
Example:
-(void)drawRect:(CGRect)rect
{
//assume this is called through someMethod below
CGContextRef ctx = UIGraphicsGetCurrentContext();
[[UIColor redColor] setFill];
CGContextFillRect(ctx, rect);
[[UIColor blueColor] setFill];
// is this following line a no-op? or should I check to make sure the rect
// I am making is contained within the rect that is passed in?
CGContextFillRect(ctx, CGRectMake(100.0f, 100.0f, 25.0f, 25.0f));
}
-(void)someMethod
{
[self setNeedsDisplayInRect:CGRectMake(50.0f, 50.0f, 25.0f, 25.0f)];
}
To simplify what Barry said: Yes, the framework will handle it for you.
You can safely ignore the rect, anything you draw outside of it will be ignored.
On the other hand, if you draw outside of the rect you are wasting CPU time, so if you can limit your drawing based on the rect, you should.
The framework will clip your drawing. On OS X (AppKit), drawing is clipped to the dirty areas of the NSView (as of 10.3). I'm not sure what the exact clipping algorithm is in UIKit. Of course, you can speed drawing by checking what needs to be drawn and only drawing in the dirty areas of the view, rather than relying on the framework to clip unnecessary drawing.