How to make CGContextDrawPath draw on a nested subview? - ios

I'm new to context concept in Apple iOS programming. Currently, I'm thinking in views. I want to draw an arc using the easiest method possible, and then I found this question and answer. But then I'm confused with context concept. I thought to draw something, I will need to call the drawing function and supply the view where I want the drawing function to draw. But in there, I got confused with UIGraphicsGetCurrentContext(). What "current context" is that? Is it the UIScreen.mainScreen()? How if I want to set the "current context" to a view inside a UICollectionViewCell, so that all the pixel drawn will be relative to the view? Can I do that?

The way you draw in CocoaTouch is to subclass UIView to create a custom view and implement the drawRect:(CGRect)rect method. Put your drawing code inside drawRect. Something like:
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGPathRef path = CGPathCreateWithRect(rect, NULL);
[[UIColor blueColor] setStroke];
CGContextSetLineWidth(context, 4.0);
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathFillStroke);
CGPathRelease(path);
}
You can put your custom UIView inside of a UICollectionViewCell if you want.

Related

How don't clear UIView when I draw?

I'm drawing a line on UIView. When I call -setNeedsDisplay, my view becomes clear and drawing new line. How to continue current line? And how drawing with animation? Thanks.
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [_color CGColor]);
CGContextSetLineWidth(context, 10.0);
CGContextMoveToPoint(context, _startPointX, 0);
CGContextAddLineToPoint(context, _endPointX, 0);
CGContextStrokePath(context);
}
setNeedsDisplay totally redraw view. So you need to store all drawings somewhere and apply it every redraw.
What do you mean under animating drawing? Animating drawing is drawing itself. Just redraw the view after every small change and it will looks like you draw naturally.
For animation, you can create a CAShapeLayer with these lines.
Then create a CABasicAnimation with keypath #"storkeEnd"
toValue set #1 to draw all your lines from clear to drawn down.

UIGraphicsGetCurrentContext vs UIGraphicsBeginImageContext/UIGraphicsEndImageContext

I am new to these parts of iOS API and here are some questions that are causing an infinite loop in my mind
Why does ..BeginImageContext have a size but ..GetCurrentContext does not have a size? If ..GetCurrentContext does not have a size, where does it draw? What are the bounds?
Why did they have to have two contexts, one for image and one for general graphics? Isn't an image context already a graphic context? What was the reason for the separation (I am trying to know what I don't know)
UIGraphicsGetCurrentContext() returns a reference to the current graphics context. It doesn't create one. This is important to remember because if you view it in that light, you see that it doesn't need a size parameter because the current context is just the size the graphics context was created with.
UIGraphicsBeginImageContext(aSize) is for creating graphics contexts at the UIKit level outside of UIView's drawRect: method.
Here is where you would use them.
If you had a subclass of UIView you could override its drawRect: method like so:
- (void)drawRect:(CGRect)rect
{
//the graphics context was created for you by UIView
//you can now perform your custom drawing below
//this gets you the current graphic context
CGContextRef ctx = UIGraphicsGetCurrentContext();
//set the fill color to blue
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
//fill your custom view with a blue rect
CGContextFillRect(ctx, rect);
}
In this case, you didn't need to create the graphics context. It was created for you automatically and allows you to perform your custom drawing in the drawRect: method.
Now, in another situation, you might want to perform some custom drawing outside of the drawRect: method. Here you would use UIGraphicsBeginImageContext(aSize)
You could do something like this:
UIBezierPath *circle = [UIBezierPath
bezierPathWithOvalInRect:CGRectMake(0, 0, 200, 200)];
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
//this gets the graphic context
CGContextRef context = UIGraphicsGetCurrentContext();
//you can stroke and/or fill
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetFillColorWithColor(context, [UIColor lightGrayColor].CGColor);
[circle fill];
[circle stroke];
//now get the image from the context
UIImage *bezierImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *bezierImageView = [[UIImageView alloc]initWithImage:bezierImage];
I hope this helps to clear things up for you. Also, you should be using UIGraphicsBeginImageContextWithOptions(size, opaque, scale). For further explanation of custom drawing with graphics contexts, see my answer here
You are slightly confused here.
As the name suggests UIGraphicsGetCurrentContext grabs the CURRENT context, thus it doesn't need the size, it grabs an existing context and returns it to you.
So when is there an existing context? Always? No. When the screen is rendering a frame, a context is created. This context is available in the drawRect: function, which is called to draw the view.
Normally, your functions aren't called in drawRect:, so they don't actually have a context available. This is when you call UIGraphicsBeginImageContext.
When you do that, you create an image context, then you can grab said context with UIGraphicsGetCurrentContext and work with it. And thus, you have to remember to end it with UIGraphicsEndImageContext
To clear things up further - if you modify the context in drawRect:, your changes will be shown on screen. In your own function, your changes don't show up anywhere. You have to extract the image in the context through the UIGraphicsGetImageFromCurrentImageContext() call.
Hope this helps!

drawRect Doesn't want to draw anything

I have a ViewController on storyboard. I have used the interface builder to set a toolbar at the bottom of the screen. I have set the custom view to a view overrides drawRect. However, for the life of me, I cannot get anything ever to show up on screen called from that drawRect. drawRect itself is called just fine, but nothing shows up on screen.
Also, I have this ViewController with a method that uses AVCaptureSession to toggle its background to a live view from camera input. I had suspected that this might have been the cause for error, but after removing all references of AVCaptureSession, I still cannot get this to work.
Sorry for my writing and/or lack of logic, I don't have any sleep right now.
Edit: Here is a small example of code that won't work. Every method inside gets called, but nothing is to show.
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
CGContextMoveToPoint(context, 0,0); //start at this point
CGContextAddLineToPoint(context, 100, 100); //draw to this point
// and now draw the Path!
CGContextStrokePath(context);
}
The documentation for UIView says to not call [super drawRect] if you're overriding UIView. Remove that call! It's been known to cause strange behaviour.
According to the docs:
If you subclass UIView directly, your implementation of this method
does not need to call super. However, if you are subclassing a
different view class, you should call super at some point in your
implementation.

Apply a shadow at both sides of a UIBezierPath

I'm currently drawing on the screen. I get smooth lines, I can change the color of my drawings. But I can't find how to apply a shadow to that line.
To draw it, I use :
[path strokeWithBlendMode:[path blendMode] alpha:1.0];
I saw that I could use CGContextSetShadowWithColor() but even though, I'm not sure how to use it since here's what's said in the CGPath reference for strokeWithBlendMode:
This method automatically saves the current graphics state prior to
drawing and restores that state when it is done, so you do not have to
save the graphics state yourself.
So I don't really know where to put that CGContextSetShadowWithColor() or anything else if I can use it.
Regards
If you want to use CGContextSetShadowwithColor() then you will need to change the way to draw your bezierpath to the view so that you draw the CGPath representation to the CGContext. An example is below:
UIBezierPath *path; // this is your path as before
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path.CGPath);
CGContextSetLineWidth(context, 2.0);
CGContextSetBlendMode(context, path.blendMode);
CGContextSetShadowWithColor(context, CGSizeMake(1.0, 1.0), 2.0, [UIColor blackColor].CGColor);
CGContextStrokePath(context);
Another way you could do this is to create a new CAShapeLayer and draw you path to that by setting it as the path property. This will easily allow you to add a shadow that will only shadow your path.

Is there a way to add text using Paths Drawing

I have a map custom view that inherit from MKOverlayPathView. I need this custom view to display circle, line and text.
I already managed to draw circle and line using path drawing CGPathAddArc and CGPathAddLineToPoint functions.
However, I still need to add text.
I tried to add text using
[text drawAtPoint:centerPoint withFont:font];
but I got invalid context error.
any idea?
With MKOverlayPathView, I think the easiest way to add text is to override drawMapRect:zoomScale:inContext: and put the path and text drawing there (and do nothing in or don't implement createPath).
But if you're going to use drawMapRect anyway, you might want to just switch to subclassing a plain MKOverlayView instead of MKOverlayPathView.
With an MKOverlayView, override the drawMapRect:zoomScale:inContext: method and draw the circle using CGContextAddArc (or CGContextAddEllipseInRect or CGPathAddArc).
You can draw the text using drawAtPoint in this method which will have the required context.
For example:
-(void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
//calculate CG values from circle coordinate and radius...
CLLocationCoordinate2D center = circle_overlay_center_coordinate_here;
CGPoint centerPoint =
[self pointForMapPoint:MKMapPointForCoordinate(center)];
CGFloat radius = MKMapPointsPerMeterAtLatitude(center.latitude) *
circle_overlay_radius_here;
CGFloat roadWidth = MKRoadWidthAtZoomScale(zoomScale);
//draw the circle...
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetFillColorWithColor(context, [[UIColor blueColor] colorWithAlphaComponent:0.2].CGColor);
CGContextSetLineWidth(context, roadWidth);
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, 2 * M_PI, true);
CGContextDrawPath(context, kCGPathFillStroke);
//draw the text...
NSString *text = #"Hello";
UIGraphicsPushContext(context);
[[UIColor redColor] set];
[text drawAtPoint:centerPoint
withFont:[UIFont systemFontOfSize:(5.0 * roadWidth)]];
UIGraphicsPopContext();
}
In relation to a comment in another answer...
When the center coordinate or radius (or whatever) of the associated MKOverlay changes, you can make the MKOverlayView "move" by calling setNeedsDisplayInMapRect: on it (instead of removing and adding the overlay again). (When using a MKOverlayPathView, you can call invalidatePath instead.)
When calling setNeedsDisplayInMapRect:, you can pass the boundingMapRect of the overlay for the map rect parameter.
In the LocationReminders sample app from WWDC 2010, the overlay view uses KVO to observe changes to the associated MKOverlay and makes itself move whenever it detects a change to the circle's properties but you could monitor the changes in other ways and call setNeedsDisplayInMapRect: explicitly from outside the overlay view.
(In a comment on another answer I did mention using MKOverlayPathView and that is how the LocationReminders app implements a moving circle overlay view. But I should have mentioned how you can also use MKOverlayView to draw a circle. Sorry about that.)
Pushing the context with UIGraphicsPushContext generated a problem for me. Remind that the method drawMapRect:zoomScale:inContext: is called from different threads in the same time so I had to synchronize the piece of code starting where the UIGraphicsPushContext is called down to UIGraphicsPopContext call.
Also when calculating the font size like in [UIFont systemFontOfSize:(5.0 * roadWidth)] one should take into consideration the [UIScreen mainScreen].scale, which for iPad, iPad2, iPhone3 is 1 and for iPhone4 - 5 and iPad3 is 2. Otherwise the text size will be different from iPad2 to iPad3.
So for me it ended like this: [UIFont boldSystemFontOfSize:(6.0f * [UIScreen mainScreen].scale * roadWidth)]

Resources