Erasing after drawing with CGContext - objective-c

I'm trying to do a simple drawing app for the iPad where you can draw on a picture, and I'm using CGContext stuff to do it but the way I originally planned on handling erasing was to just draw over stuff with white...except I just realized today that it doesn't work when you're drawing onto another image because then when you "erase" you'll also "erase" the background image as well.
Is there any way to support actual erasing?
Thanks!

I also needed erasing functionality. Based on #Jeremy's answer, here is what worked for me:
CGContextRef cgref = UIGraphicsGetCurrentContext();
if(erase == TRUE) // Erase to show background
{
CGContextSetBlendMode(cgref, kCGBlendModeClear);
}
else // Draw with color
{
CGContextSetBlendMode(cgref, kCGBlendModeNormal);
}

Display the user's drawing in a layer above the image. Then erasing is as simple as drawing a transparent patch on the drawing layer in order to let the image pixels below it show through.

Clear all CGContextRef drawings:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, self.bounds);
[self setNeedsDisplay];

Related

Using cornerRadius on a UIImageView in a UITableViewCell

I'm using a UIImageView for each of my UITableViewCells, as thumbnails. My code uses SDWebImage to asynchronously grab those images from my backend and load them in, and then caching them. This is working fine.
My UIImageView is a 50x50 square, created in Interface Builder. Its background is opaque, white color (same as my UITableViewCell background color, for performance). However, I'd like to use smooth corners for better aesthetics. If I do this:
UIImageView *itemImageView = (UIImageView *)[cell viewWithTag:100];
itemImageView.layer.cornerRadius = 2.5f;
itemImageView.layer.masksToBounds = NO;
itemImageView.clipsToBounds = YES;
My tableview drops around 5-6 frames immediately, capping about 56 FPS during fast scrolling (which is okay), but when I drag and pull the refresh control, it lags a bit and drops to around 40 FPS. If I remove the cornerRadius line, all is fine and no lag. This has been tested on an iPod touch 5G using Instruments.
Is there any other way I could have a rounded UIImageView for my cells and not suffer a performance hit? I'd already optimized my cellForRowAtIndexPath and I get 56-59 FPS while fast scrolling with no cornerRadius.
Yes, that's because cornerRadius and clipToBounds requires offscreen rendering, I suggest you to read these answer from one of my question. I also quote two WWDC session thatyou should see. The best thing you can do is grab the image right after is downloaded and on another thread dispatch a method that round the images. Is preferable that you work on the image instead of the imageview.
// Get your image somehow
UIImage *image = [UIImage imageNamed:#"image.jpg"];
// Begin a new image that will be the new image with the rounded corners
// (here with the size of an UIImageView)
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
// Add a clip before drawing anything, in the shape of an rounded rect
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
cornerRadius:10.0] addClip];
// Draw your image
[image drawInRect:imageView.bounds];
// Get the image, here setting the UIImageView image
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
// Lets forget about that we were drawing
UIGraphicsEndImageContext();
Method grabbed here
You can also subclass the tableviewcell and override the drawRect method.
The dirty but very effective way is draw a mask in photoshop with inside alpha and around the matching color of the background of the cell and add another imageView, not opaque with clear background color, on the one with images.
There is another good solution for this. I did it a few times in my projects. If you want to create a rounded corners or something else you could just use a cover image in front of your main image.
For example, you want to make rounded corners. In this case you need a square image layer with a cut out circle in the center.
By using this method you will get 60fps on scrolling inside UITableView or UICollectionView. Because this method not required offscreen rendering for customizing UIImageView's (like avatars and etc.).
Non blocking solution
As a follow up to Andrea's response, here is a function that will run his code in the background.
+ (void)roundedImage:(UIImage *)image
completion:(void (^)(UIImage *image))completion {
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Begin a new image that will be the new image with the rounded corners
// (here with the size of an UIImageView)
UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
CGRect rect = CGRectMake(0, 0, image.size.width,image.size.height);
// Add a clip before drawing anything, in the shape of an rounded rect
[[UIBezierPath bezierPathWithRoundedRect:rect
cornerRadius:image.size.width/2] addClip];
// Draw your image
[image drawInRect:rect];
// Get the image, here setting the UIImageView image
UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
// Lets forget about that we were drawing
UIGraphicsEndImageContext();
dispatch_async( dispatch_get_main_queue(), ^{
if (completion) {
completion(roundedImage);
}
});
});
}
Swift 3 version of thedeveloper3124's answer
func roundedImage(image: UIImage, completion: #escaping ((UIImage?)->(Void))) {
DispatchQueue.global().async {
// Begin a new image that will be the new image with the rounded corners
// (here with the size of an UIImageView)
UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
let rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Add a clip before drawing anything, in the shape of an rounded rect
UIBezierPath(roundedRect: rect, cornerRadius: image.size.width/2).addClip()
// Draw your image
image.draw(in: rect)
// Get the image, here setting the UIImageView image
guard let roundedImage = UIGraphicsGetImageFromCurrentImageContext() else {
print("UIGraphicsGetImageFromCurrentImageContext failed")
completion(nil)
return
}
// Lets forget about that we were drawing
UIGraphicsEndImageContext()
DispatchQueue.main.async {
completion(roundedImage)
}
}
}

Optimize CGContextDrawRadialGradient in drawRect:

In my iPad app, I have a UITableView that alloc/inits a UIView subclass every time a new cell is selected. I've overridden drawRect: in this UIView to draw a radial gradient and it works fine, but performance is suffering - when a cell is tapped, the UIView takes substantially longer to draw a gradient programmatically as opposed to using a .png for the background. Is there any way to "cache" my drawRect: method or the gradient it generates to improve performance? I'd rather use drawRect: instead of a .png. My method looks like this:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
size_t gradLocationsNum = 2;
CGFloat gradLocations[2] = {0.0f, 1.0f};
CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.5f};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum);
CGColorSpaceRelease(colorSpace);
CGPoint gradCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ;
CGContextDrawRadialGradient (context, gradient, gradCenter, 0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation);
CGGradientRelease(gradient);
}
Thanks!
You can render graphics into a context and then store that as a UIImage. This answer should get you started:
drawRect: is a method on UIView used to draw the view itself, not to pre-create graphic objects.
Since it seems that you want to create shapes to store them and draw later, it appears reasonable to create the shapes as UIImage and draw them using UIImageView. UIImage can be stored directly in an NSArray.
To create the images, do the following (on the main queue; not in drawRect:):
1) create a bitmap context
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
2) get the context
CGContextRef context = UIGraphicsGetCurrentContext();
3) draw whatever you need
4) export the context into an image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
5) destroy the context
UIGraphicsEndImageContext();
6) store the reference to the image
[yourArray addObject:image];
Repeat for each shape you want to create.
For details see the documentation for the above mentioned functions. To get a better understanding of the difference between drawing in drawRect: and in arbitrary place in your program and of working with contexts in general, I would recommend you read the Quartz2D Programming Guide, especially the section on Graphics Contexts.

Adding a tint to an image

I'm creating an app which uses UIImagePickerController to present a camera to the user with a custom overlay which includes one of two grids/patterns over the camera "view" itself.
The grids themselves are .png files in a UIImageView which is added to the overlay, they're quite complex so I would really like to steer away from drawing the grid in code, even though that would present I nice clean and simple answer to my question.
I would like to be able to offer the grids in a variety of colours. The obvious solution is create more .png images in different colours, but for each colour there would have to be four separate images (regular and retina for each of the grids) so that would quickly add up to a lot of assets.
The solution which, I think, would be ideal, would be for me to just create the grids in white/gray and then apply a tint to it to colour it appropriately.
Is that possible? Or do I need to seek an alternative solution?
With thanks to Ananth for pointing me to iPhone - How do you color an image?
I've added this method to my code as suggested in the question, with the modification in willc2's answer:
-(UIImage *)colorizeImage:(UIImage *)baseImage color:(UIColor *)theColor {
UIGraphicsBeginImageContext(baseImage.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect area = CGRectMake(0, 0, baseImage.size.width, baseImage.size.height);
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -area.size.height);
CGContextSaveGState(ctx);
CGContextClipToMask(ctx, area, baseImage.CGImage);
[theColor set];
CGContextFillRect(ctx, area);
CGContextRestoreGState(ctx);
CGContextSetBlendMode(ctx, kCGBlendModeMultiply);
CGContextDrawImage(ctx, area, baseImage.CGImage);
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
...and I'm getting exactly what I'm after.

Draw rounded linear gradient (or extended radial gradient) with CoreGraphics

I want to do some custom drawing with CoreGraphics. I need a linear gradient on my view, but the thing is that this view is a rounded rectangle so I want my gradient to be also rounded at angles. You can see what I want to achieve on the image below:
So is this possible to implement in CoreGraphics or some other programmatic and easy way?
Thank you.
I don't think there is an API for that, but you can get the same effect if you first draw a radial gradient, say, in an (N+1)x(N+1) size bitmap context, then convert the image from the context to a resizable image with left and right caps set to N.
Pseudocode:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(N+1,N+1), NO, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
// <draw the gradient into 'context'>
UIImage* gradientBase = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImage* gradientImage = [gradientBase resizableImageWithCapInsets:UIEdgeInsetsMake(0,N,0,N)];
In case you want the image to scale vertically as well, you just have to set the caps to UIEdgeInsetsMake(N,N,N,N).
I just want to add more sample code for this technique, as some things weren't obvious for. Maybe it will be useful for somebody:
So, let's say, we have our custom view class and in it's drawRect: method we put this:
// Defining the rect in which to draw
CGRect drawRect=self.bounds;
Float32 gradientSize=drawRect.size.height; // The size of original radial gradient
CGPoint center=CGPointMake(0.5f*gradientSize,0.5f*gradientSize); // Center of gradient
// Creating the gradient
Float32 colors[4]={0.f,1.f,1.f,0.2f}; // From opaque white to transparent black
CGGradientRef gradient=CGGradientCreateWithColorComponents(CGColorSpaceCreateDeviceGray(), colors, nil, 2);
// Starting image and drawing gradient into it
UIGraphicsBeginImageContextWithOptions(CGSizeMake(gradientSize, gradientSize), NO, 1.f);
CGContextRef context=UIGraphicsGetCurrentContext();
CGContextDrawRadialGradient(context, gradient, center, 0.f, center, center.x, 0); // Drawing gradient
UIImage* gradientImage=UIGraphicsGetImageFromCurrentImageContext(); // Retrieving image from context
UIGraphicsEndImageContext(); // Ending process
gradientImage=[gradientImage resizableImageWithCapInsets:UIEdgeInsetsMake(0.f, center.x-1.f, 0.f, center.x-1.f)]; // Leaving 2 pixels wide area in center which will be tiled to fill whole area
// Drawing image into view frame
[gradientImage drawInRect:drawRect];
That's all. Also if you're not going to ever change the gradient while app is running, you would want to put everything except last line in awakeFromNib method and then in drawRect: just draw the gradientImage into view's frame. Also don't forget to retain the gradientImage in this case.

How to ignore transformations when drawing an image with drawAsPatternInRect:

I draw a background image in a view with the following code:
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
UIImage *image = [UIImage imageNamed:#"background.jpg"];
[image drawAsPatternInRect:rect];
CGContextRestoreGState(currentContext);
This works well. But when I change the size of that view animated the drawn image is scaled until it get's redraw. How can I ignore this transformation during the animation?
At the moment I don't think that's possible. Since the iPhone doesn't have a very fast processor Apple choose to disable the LiveRedraw-feature (that actually is available on Mac).
Try setting the view's contentMode to UIViewContentModeRedraw.
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
// Get the view to redraw when it's frame is changed
self.contentMode = UIViewContentModeRedraw;
}
return self;
}
I finally found a solution. I use contentMode UIViewContentModeTopLeft so it doesn't get scaled. And before the resize animation I only redraw if the new size is greater then the old one. And after the animation I always redraw.