Overriding drawRect: and pushing new offset CGContexts onto the stack - iphone

I have overridden the drawRect: in my UIView and I want to draw several tiles. I'm looping through them all and I have a separate function that draws each individual tile.
The way I'm doing it now is I pass the tile's calculated CGRect to the function. At the moment, any drawing methods have to include the x & y offsets of the rect passed to it when drawing the tile images.
How can I push a new offset CGContext on the stack before calling the tile draw methods?
So for example, I could draw a square at [0, 0, 50, 50] inside the tile drawing method and that will actually be drawn at the correct tile's location?

You should take advantage of the CTM (current transform matrix) which makes use of affine transforms to scale drawing into the context. It's built for exactly this purpose.
First call CGContextSaveGState. This saves a bunch of information about the graphics context onto a (per context) stack, including the CTM.
Secondly, use CGContextTranslateCTM. Pass in the x & y coordinates of the rect's origin.
Then call your drawing subroutine.
Finally, call CGContextRestoreGState. This will undo the translation.
Hope that helps.

Related

Drawing a straight line between two points by dragging on IOS

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.

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.

How do I position `contents` png in CALayer?

Do I have to move the layer frame or apply translate matrix transformation to layer? Or perhaps I can move the contents inside of the layer? If contents is not movable inside of layer, how it would position initially?
A CALayer has a frame (or, equivalently, a bounds and an origin), which is used logically to determine what to draw. When drawInContext: or equivalent is called, it's the frame that determines how the contents are produced.
However, like OS X, iOS adopts a compositing window manager, which means that views know how to draw their output to a buffer and the buffers are combined to create the view, with the window manager figuring out what to do about caching and video memory management in between.
If you adjust the transform property of the view or of the layer class, then you adjust how the compositing happens. However, the results of drawInContext: should explicitly still be the same so the window manager knows it can just use the cached image.
So, for example, if you set a frame of size 128x128 and then a transform that scales the CALayer up to double, you'll occupy a 256x256 area of the screen but the image used for compositing will be only 128x128 in size, making each source pixel into four target pixels. If you set a frame of size 256x256 and the identity transform, you'll cover the same amount of screen space but with each source pixel being 1:1 related to a target pixel.
A side effect is that changing the frame causes a redraw from first principles. Changing the transform doesn't. So the latter is usually faster, and is also the thing to do if you decide to use something like CATiledLayer (as used in Safari, Maps, etc) that draws in a separate thread and may take a while to come up with results.
As a rule of thumb, you use the frame to set the initial position and update the frame for normal work stuff. You play with the transform for transitions and other special effects. However, all of the frame and transform properties of a CATiledLayer are animatable in the CoreAnimation sense, so that's really still at your discretion.
Most people don't work on the level of a CALayer, but prefer to work with UIViews. In which case the comments are mostly the same, with the caveat that you can then adjust the [2d] transform on the view or the [3d] transform on the view's layer and have the compositor figure it all out, but change the frame to prompt a redraw.

How do you display a pixel of color C to iPhone coordinates (x, y)?

I've read all Apple documentation on the iPhone development and there's nothing that describes how to do it.
The drawing on iPhone is not pixel-oriented in this way. You can draw a rectangle of size 1x1 using the Quartz API if you want in your drawRect: method:
CGColorRef colorRef = CGColorCreateGenericRGB(r, g, b, a);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, colorRef);
// This next line does the drawing; repeat for as many x,y pairs as you want.
CGContextFillRect(context, CGRectMake(x,y,1,1));
CGColorRelease(colorRef);
But you wouldn't want to fill the entire screen using this method, it's crazy slow. You should think in terms of vectors: lines, rectangles, other polygons. This applies to both Quartz (like the above) and OpenGL.
If you really are doing something that ends up rendering pixel-at-a-time filling a whole screen, your best bet may be using Quartz to create an offscreen bitmap context, writing to the bitmap memory directly per-pixel, and then drawing the whole thing to the drawing context inside drawRect:. You could also use OpenGL to draw a series of single-pixel sized GL_POINTs (instead of triangles), which may be faster depending on what you're doing.

Moving CGPaths by x,y pixels?

Is there a way to move a path on screen by (x,y) pixels using directly a CGPathRef instead of walking through its points and lines again in drawRect method? I want to be reusing my old CGPathRef when I want to move it on screen instead of recreating it with new pixels.
Depending on your needs, you could just translate the drawing context via CGContextTranslateCTM before drawing your path (then restore the old context, either with push/popping contexts, or inverting the translation). You might also like CGPathApply, which will call a function for every path element in the path (so you can translate the points by hand).