Quartz Drawing weirdly - iphone

what I'm trying to do is create a custom progress bar, with quartz drawing, what I do is the following,
- (void)drawRect:(CGRect)rect {
// Drawing code.
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect currentBounds = self.bounds;
CGContextSetLineWidth(context, 20.0f);
CGContextSetStrokeColorWithColor(context, [ProgressBarView redColor]);
CGContextBeginPath(context);
CGContextMoveToPoint(context, CGRectGetMinX(currentBounds), CGRectGetMidY(currentBounds));
CGContextAddLineToPoint(context, CGRectGetMaxX(currentBounds) * time, CGRectGetMidY(currentBounds));
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathStroke);
}
In the CGContextAddLineToPoint I multiply the maxX times time, which is calculated with this function every second.
- (void)pushTime {
if (time >= 1.0) {
time = 0.0;
} else {
time = time += 0.1;
}
[self setNeedsDisplay];
}
It works great the first time, but then the progress bar never gets back to the starting point, I already tried to change the start value to other than 0, so that quartz can create the path without a problem, but that didnt do it.
Anyway, thank you for your help.

It seems your view does not clear previously drawn content for some reason - you can fix that by explicitly forcing view to clear before redrawing itself:
- (void)drawRect:(CGRect)rect {
// Drawing code.
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
...
Although I expected that clearing should be done automatically (and tweaking the value of clearsContextBeforeDrawing property does not make any effect...)

Related

black background when overriding drawRect in UIScrollView

So I am trying to override drawRect in my UIScrolLView, however it gives me this black background instead of the background color that I've specified for my UIScrollView. Why is this? If I remove the drawRect code then everything is fine:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (shouldDrawVerticalLineForProfile){
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef separatorColor = [UIColor colorWithRed:47.0/255.0 green:47.0/255.0
blue:47.0/255.0 alpha:1.0].CGColor;
// Add at bottom
CGPoint startPoint = CGPointMake(60, 0);
CGPoint endPoint = CGPointMake(60, 10000);
CGContextSaveGState(context);
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextSetStrokeColorWithColor(context, separatorColor);
CGContextSetLineWidth(context, 5.0);
CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
}
I guess what you are searching for is:
myScrollViewInstance.opaque = NO
After that the background of your scroll view should not be black anymore.
I ran into a similar situation when overriding drawRect in my UIScrollView subclass.
Simply overriding with:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
}
resulted in a black background instead of the desired clear background I'd get without overriding.
I found it was because the View Background was set to Default in Interface Builder and that setting it to Clear Color resolved the issue for me.
I'm not sure if this is related to your problem but thought I'd share in case it might help.
The answer here is very clearly documented in the AppleDocs for UIView's drawRect: method. The first sentence is:
The default implementation of this method does nothing.
So, the suggestions here to call super are not going to help. The rest of the answer is contained further down in the discussion:
... if the opaque property of your view is set to YES, your drawRect: method must totally fill the specified rectangle with opaque content.
Meaning that if you don't intend to have a transparent background, you need to actually draw the background. The most correct way to do this is to use the following drawRect: template:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect]; // optional if a direct UIView-subclass, should be called otherwise.
CGContextRef context = UIGraphicsGetCurrentContext();
// Fill the background color, if needed
if (self.opaque) {
UIGraphicsPushContext(context);
CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
CGContextFillRect(context, rect);
UIGraphicsPopContext();
}
// Your code here...
}
Swift Update
Since you are responsible to add background color on your custom view, make sure to fill rect with color:
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.saveGState()
// Add you custom color
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
defer { context.restoreGState() }
// Your custom drawing ....
}
By the way, you could also set your custom view background color on init. Then you do not need to fill rect with color as stated above.
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.white
}
This should help
CGContextSetFillColorWithColor(context, colorBack);
CGContextFillRect(context, self.bounds);
// Choose bounds and colorBack accordingly
Please try this its Work for me
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setBackgroundColor:[UIColor clearColor]];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (shouldDrawVerticalLineForProfile){
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef separatorColor = [UIColor colorWithRed:47.0/255.0 green:47.0/255.0
blue:47.0/255.0 alpha:1.0].CGColor;
// Add at bottom
CGPoint startPoint = CGPointMake(60, 0);
CGPoint endPoint = CGPointMake(60, 10000);
CGContextSaveGState(context);
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextSetStrokeColorWithColor(context, separatorColor);
CGContextSetLineWidth(context, 5.0);
CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
}
The only thing that worked for me was to explicitly set the background after any call to setNeedsDisplay. In my example, I was using a subclass of an MKAnnotationView over a map, instead of a UIScrollView, so I don't really know how well this applies to the OP scenario. In my case, I was calling setNeedsDisplay from setAnnotation, and I found that doing so resets by backgroundColor. Simply re-setting by backgroundColor after setNeedsDisplay fixes the problem.
FWIW, I too observed that simply overriding drawRect to delegate to the super class caused this black background problem.

Animate CGContextFillRect

I have a progress view that I'm trying to animate the change in progress. How do you go about doing that? Below is the code to draw the progress.
Thanks in advance
- (void)drawRect:(CGRect)rect {
// Drawing code.
CGContextRef context = UIGraphicsGetCurrentContext();
// draw the background
CGContextSaveGState(context);
UIBezierPath *outerPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:0];
[outerPath addClip];
CGPoint backgroundStartPoint = CGPointMake(0.0f, 0.0f);
CGPoint backgroundEndPoint = CGPointMake(0.0f, CGRectGetHeight(rect));
CGContextDrawLinearGradient(context, _backgroundGradient, backgroundStartPoint, backgroundEndPoint, 0);
CGContextRestoreGState(context);
// draw the progress
CGContextSaveGState(context);
UIBezierPath *innerPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, 1.0f, 1.0f) cornerRadius: 0];
[innerPath addClip];
[_glossTintColor setFill];
CGRect progressRect = CGRectMake(0, 0, CGRectGetWidth(rect)*_progress, CGRectGetHeight(rect));
CGContextFillRect(context, progressRect);
CGContextRestoreGState(context);
}
Whenever the progress changes, trigger a redraw using [view setNeedsDisplay];
See the UIView reference on setNeedsDisplay.
For example, lets say that custom view you have created has a property called progress, the instance variable is called _progress. Lets also suppose that property gets updated by some event/timer/additional thread.
- (void)setProgress:(float)progress
{
_progress = progress;
[self setNeedsDisplay];
}
That overrides the original setter and adds the functionality mentioned above.

iPhone Quartz Drawing Eraser

I am trying to make an iPhone application with erasing. I am running into 2 problems, if you have a solution for either one please answer this. I would like to erase part of an image.
1) I am currently just clearing the rect but it has a square edge. I would like it to be round, I have previously tried translating but this does not work. I also need it to translate/rotate as few times as possible to maintain the same performance.
2) In addition I wanted to know if there are any other ways of erasing. When erasing fast it is erasing ever 1/2 inch. Is there a way to stroke a path and clear the rect or something? Sorry if this is hard to understand.
CGRect circleRect = CGRectMake([touch CGPointValue].x, [touch CGPointValue].y, 25, 25);
CGContextClearRect(currentContext,circleRect);
This code should do what you're looking for:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, self.strokeWidth);
CGContextSetBlendMode(context, kCGBlendModeClear);
CGContextSetStrokeColorWithColor(context, [[UIColor clearColor] CGColor]);
CGContextBeginPath(context);
CGContextMoveToPoint(context, lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(context, currentPoint.x, currentPoint.y);
CGContextStrokePath(context);
CGContextFlush(context);
The key points are
1) CGContextSetLineCap(context, kCGLineCapRound), which makes it round
2) CGContextSetBlendMode(context, kCGBlendModeClear) which clears the context.
I realize this is an older question but I thought I would expand on Eric Reids answer as I was trying to use his example outside of the drawRect method and had to modify it to get it to work with a context I created in a 'touchesMoved' method in a UIView subclass.
I call this method from 'touchesMoved' and pass it the CGPoint of the touch in the view I'm drawing in.
-(void) processEraseAtPoint:(CGPoint)point
{
// setup a context with the size of our canvas view (the canvas view is the UIView instance I'm drawing into)
UIGraphicsBeginImageContext(self.canvasView.bounds.size);
// get a reference to the context we just created
CGContextRef context = UIGraphicsGetCurrentContext();
// draw the image we want to edit into this context (this is the image containing the drawing I want to erase part of)
[self.canvasView.incrementalImage drawAtPoint:CGPointZero];
// set our context options
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, self.canvasView.settings.brushDiameter);
CGContextSetBlendMode(context, kCGBlendModeClear);
// make the color clear since we're erasing
CGContextSetStrokeColorWithColor(context, [[UIColor clearColor] CGColor]);
// start our path in this context
CGContextBeginPath(context);
// set our first point
CGContextMoveToPoint(context, lastTouch.x, lastTouch.y);
// draw from our last point to this point
CGContextAddLineToPoint(context, point.x, point.y);
// stroke this path (in this case it's clear so it will erase what's there)
CGContextStrokePath(context);
// set our incrementalImage in the canvasView with the updated image from this context
// Note that in the canvasView 'drawRect' method I am calling
// '[self.incrementalImage drawInRect:rect]', so this new image will get drawn
// in my canvasView when I call 'setNeedsDisplay'
self.canvasView.incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
// cleanup our context
CGContextFlush(context);
UIGraphicsEndImageContext();
// set our last touch point for the next line segment
lastTouch = point;
// update our view
[self.canvasView setNeedsDisplay];
}

Beginner iphone question: drawing a rectangle. What am I doing wrong?

Trying to figure out what I'm doing wrong here. Have tried several things, but I never see that elusive rectangle on the screen. Right now, that's all I want to do -- just draw a single rectangle on the screen.
I'm getting an "invalid context" on everything but the CGContextSetRGBFillColor(). Getting the context after that seems kinda wrong to me, but I'm not at home looking at the examples I was using last night.
Have I messed up something else as well? I really would like to get at least this much done tonight...
- (id)initWithCoder:(NSCoder *)coder
{
CGRect myRect;
CGPoint myPoint;
CGSize mySize;
CGContextRef context;
if((self = [super initWithCoder:coder])) {
NSLog(#"1");
currentColor = [UIColor redColor];
myPoint.x = (CGFloat)100;
myPoint.y = (CGFloat)100;
mySize.width = (CGFloat)50;
mySize.height = (CGFloat)50;
NSLog(#"2");
// UIGraphicsPushContext (context);
NSLog(#"3");
CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, currentColor.CGColor);
CGContextAddRect(context, myRect);
CGContextFillRect(context, myRect);
}
return self;
}
Thanks,
Sean.
Starting with a View-based template, create a project named Drawer. Add a UIView class to your project. Name it SquareView (.h and .m).
Double-click DrawerViewController.xib to open it in Interface Builder. Change the generic view there to SquareView in the Identity Inspector (command-4) using the Class popup menu. Save and go back to Xcode.
Put this code in the drawRect: method of your SquareView.m file to draw a large, crooked, empty yellow rectangle and a small, green, transparent square:
- (void)drawRect:(CGRect)rect;
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 0.0, 1.0); // yellow line
CGContextBeginPath(context);
CGContextMoveToPoint(context, 50.0, 50.0); //start point
CGContextAddLineToPoint(context, 250.0, 100.0);
CGContextAddLineToPoint(context, 250.0, 350.0);
CGContextAddLineToPoint(context, 50.0, 350.0); // end path
CGContextClosePath(context); // close path
CGContextSetLineWidth(context, 8.0); // this is set from now on until you explicitly change it
CGContextStrokePath(context); // do actual stroking
CGContextSetRGBFillColor(context, 0.0, 1.0, 0.0, 0.5); // green color, half transparent
CGContextFillRect(context, CGRectMake(20.0, 250.0, 128.0, 128.0)); // a square at the bottom left-hand corner
}
You don't have to call this method for the drawing to happen. Your view controller will tell the view to draw itself at least once when the program launches and the NIB files are activated.
You are not supposed to put CG code in initWithCoder. That message should only be used for INITIALIZATION purpose.
Put your drawing code in:
- (void)drawRect:(CGRect)rect
If you are subclassing a UIView...

Drawing incrementally in a UIView (iPhone)

As far as I have understood so far, every time I draw something in the drawRect: of a UIView, the whole context is erased and then redrawn.
So I have to do something like this to draw a series of dots:
Method A: drawing everything on every call
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, self.bounds, maskRef); //draw the mask
CGContextClipToMask(context, self.bounds, maskRef); //respect alpha mask
CGContextSetBlendMode(context, kCGBlendModeColorBurn); //set blending mode
for (Drop *drop in myPoints) {
CGContextAddEllipseInRect(context, CGRectMake(drop.point.x - drop.size/2, drop.point.y - drop.size/2, drop.size, drop.size));
}
CGContextSetRGBFillColor(context, 0.5, 0.0, 0.0, 0.8);
CGContextFillPath(context);
}
Which means, I have to store all my Dots (that's fine) and re-draw them all, one by one, every time I want to add a new one. Unfortunately this gives my terrible performance and I am sure there is some other way of doing this, more efficiently.
EDIT: Using MrMage's code I did the following, which unfortunately is just as slow and the color blending doesn't work. Any other method I could try?
Method B: saving the previous draws in a UIImage and only drawing the new stuff and this image
- (void)drawRect:(CGRect)rect
{
//draw on top of the previous stuff
UIGraphicsBeginImageContext(self.frame.size);
CGContextRef ctx = UIGraphicsGetCurrentContext(); // ctx is now the image's context
[cachedImage drawAtPoint:CGPointZero];
if ([myPoints count] > 0)
{
Drop *drop = [myPoints objectAtIndex:[myPoints count]-1];
CGContextClipToMask(ctx, self.bounds, maskRef); //respect alpha mask
CGContextAddEllipseInRect(ctx, CGRectMake(drop.point.x - drop.dropSize/2, drop.point.y - drop.dropSize/2, drop.dropSize, drop.dropSize));
CGContextSetRGBFillColor(ctx, 0.5, 0.0, 0.0, 1.0);
CGContextFillPath(ctx);
}
[cachedImage release];
cachedImage = [UIGraphicsGetImageFromCurrentImageContext() retain];
UIGraphicsEndImageContext();
//draw on the current context
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, self.bounds, maskRef); //draw the mask
CGContextSetBlendMode(context, kCGBlendModeColorBurn); //set blending mode
[cachedImage drawAtPoint:CGPointZero]; //draw the cached image
}
EDIT:
After all I combined one of the methods mention below with redrawing only in the new rect. The result is:
FAST METHOD:
- (void)addDotAt:(CGPoint)point
{
if ([myPoints count] < kMaxPoints) {
Drop *drop = [[[Drop alloc] init] autorelease];
drop.point = point;
[myPoints addObject:drop];
[self setNeedsDisplayInRect:CGRectMake(drop.point.x - drop.dropSize/2, drop.point.y - drop.dropSize/2, drop.dropSize, drop.dropSize)]; //redraw
}
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, self.bounds, maskRef); //draw the mask
CGContextClipToMask(context, self.bounds, maskRef); //respect alpha mask
CGContextSetBlendMode(context, kCGBlendModeColorBurn); //set blending mode
if ([myPoints count] > 0)
{
Drop *drop = [myPoints objectAtIndex:[myPoints count]-1];
CGPathAddEllipseInRect (dotsPath, NULL, CGRectMake(drop.point.x - drop.dropSize/2, drop.point.y - drop.dropSize/2, drop.dropSize, drop.dropSize));
}
CGContextAddPath(context, dotsPath);
CGContextSetRGBFillColor(context, 0.5, 0.0, 0.0, 1.0);
CGContextFillPath(context);
}
Thanks everyone!
If you are only actually changing a small portion of the UIView's content every time you draw (and the rest of the content generally stays the same), you can use this. Rather than redraw all the content of the UIView every single time, you can mark only the areas of the view that need redrawing using -[UIView setNeedsDisplayInRect:] instead of -[UIView setNeedsDisplay]. You also need to make sure that the graphics content is not cleared before drawing by setting view.clearsContextBeforeDrawing = YES;
Of course, all this also means that your drawRect: implementation needs to respect the rect parameter, which should then be a small subsection of your full view's rect (unless something else dirtied the entire rect), and only draw in that portion.
You can save your CGPath as a member of your class. And use that in the draw method, you will only need to create the path when the dots change but not every time the view is redraw, if the dots are incremental, just keep adding the ellipses to the path. In the drawRect method you will only need to add the path
CGContextAddPath(context,dotsPath);
-(CGMutablePathRef)createPath
{
CGMutablePathRef dotsPath = CGPathCreateMutable();
for (Drop *drop in myPoints) {
CGPathAddEllipseInRect ( dotsPath,NULL,
CGRectMake(drop.point.x - drop.size/2, drop.point.y - drop.size/2, drop.size, drop.size));
}
return dotsPath;
}
If I understand your problem correctly, I would try drawing to a CGBitmapContext instead of the screen directly. Then in the drawRect, draw only the portion of the pre-rendered bitmap that is necessary from the rect parameter.
How many ellipses are you going to draw? In general, Core Graphics should be able to draw a lot of ellipses quickly.
You could, however, cache your old drawings to an image (I don't know if this solution is more performant, however):
UIGraphicsBeginImageContext(self.frame.size);
CGContextRef ctx = UIGraphicsGetCurrentContext(); // ctx is now the image's context
[cachedImage drawAtPoint:CGPointZero];
// only plot new ellipses here...
[cachedImage release];
cachedImage = [UIGraphicsGetImageFromCurrentImageContext() retain];
UIGraphicsEndImageContext();
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, self.bounds, maskRef); //draw the mask
CGContextClipToMask(context, self.bounds, maskRef); //respect alpha mask
CGContextSetBlendMode(context, kCGBlendModeColorBurn); //set blending mode
[cachedImage drawAtPoint:CGPointZero];
If you are able to cache the drawing as an image, you can take advantage of UIView's CoreAnimation backing. This will be much faster than using Quartz, as Quartz does its drawing in software.
- (CGImageRef)cachedImage {
/// Draw to an image, return that
}
- (void)refreshCache {
myView.layer.contents = [self cachedImage];
}
- (void)actionThatChangesWhatNeedsToBeDrawn {
[self refreshCache];
}