Custom drawing on top of UIButton's background image - objective-c

What is the method calling sequence of UIButton's drawing?
I have a UIButton subclass, let's call it UIMyButton. I add it to a .xib controller and give it a background image. Then in the UIMyButton.m file I override drawRect method and draw a shape.
Something like:
- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(c, [UIColor orangeColor].CGColor);
CGContextFillRect(c, rect);
}
The problem is that it draws the orange rectangle underneath the background image. What methods are responsible for drawing/updating background images (it updates on "tap down" for example)? How do I force the draw on top without adding an additional custom subview to the button (or is the background image a subview itself)?
I'm just trying to get a clear understanding of how the UIButton drawing sequence works.

The solution is to not use a background for the button. Instead, draw the image inside the drawRect method like so:
-(void)drawRect:(CGRect)rect
{
CGContextRef ctx=UIGraphicsGetCurrentContext();
UIImage *image = [UIImage imageNamed:#"image.jpg"];
[image drawInRect:self.bounds];
CGContextSetFillColorWithColor(c, [UIColor orangeColor].CGColor);
CGContextFillRect(c, rect);
}

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

drawRect: crash by big rect after zomming

My UIView structure:
I have a "master" UIView (actually UIScrollView, but it's only purpose scrolling per pagination).
As a subView on this "master" I have my "pageView" (subclass of UIScrollView). This UIScrollView can have any content (e.g. a UIImageView).
The "master" has another subView: PaintView (subclass of UIView). With this view, I track the finger movements and draw it.
The structure looks like this:
[master.view addSubview: pageView];
[master.view addSubview: paintView];
I know when the user zooms in (pageView is responsible for this) and over delegate/method calls I change the frame of paintView according to the zoom change, during the zoom action.
After zooming (scrollViewDidEndZooming:withView:atScale) I call a custom redraw method of paintView.
Redraw method and drawRect:
-(void)redrawForScale:(float)scale {
for (UIBezierPath *path in _pathArray) {
//TransformMakeScale...
}
[self setNeedsDisplay];
}
-(void)drawRect:(CGRect)rect {
for (UIBezierPath *path in _pathArray) {
[path strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
}
The problem:
When I zoom-in, I receive a memory warning and the app crashes.
In the Allocations profiler I can see that the app own a lot of memory, but I can't see why.
When I don't call 'setNeedDisplay' after my 'redrawForScale' method the app isn't crashing.
When I log the rect in drawRect I see values like this: {{0, 0.299316}, {4688, 6630}}.
The problem is (re)drawing such a huge CGRect (correct me if this is wrong).
The solution for me is to avoid calling a custom drawRect method. In some stackoverflow answers (and I think somewhere in the Appls docs) it was mentioned that a custom drawRect: will reduce the performance (because iOS can't do some magic on a custom drawRect method).
My solution:
Using a CAShapeLayer. For every UIBezierPath I create a CAShapeLayer instance, add the UIBezierPath to the path attribute of the layer and add the layer as a sublayer to my view.
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] initWithLayer:self.layer];
shapeLayer.lineWidth = 10.0;
shapeLayer.strokeColor = [UIColor blackColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.path = myBezierPath.CGPath;
[self.layer addSublayer:shapeLayer];
With this solution I don't need to implement drawRect. So the App won't crash, even with a big maximumZoomScale.
After changing the UIBezierPath (translate, scale, change color) I need to set the changed bezierPath to the shapeLayer again (set the path attribute of shapeLayer again).

CG drawing in UIView to use as UIImage?

I am attempting to create a custom back button for UINavigationbar.
I know that I can use backButtonBackgroundImageForState:barMetrics: to set an image.
My problem is that I don't what to have to use an image file. I would rather draw the back button in code.
My question is can I subclass a UIView, override the drawrec: to draw a back button, then use that UIView as an image in backButtonBackgroundImageForState:barMetrics:
I'm thinking that it would go something like:
UIImage * backButtonImage = // somehow get image from my subclassed UIView;
backButtonImage = [backButtonImage stretchableImageWithLeftCapWidth: 15.0 topCapHeight: 30.0];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage: backButtonImage forState: UIControlStateNormal barMetrics: UIBarMetricsDefault];
Is this even the best way to be going about this? Or should I just suck it up and create an image in photoshop?
You can create a bitmap image context using UIGraphicsBeginImageContextWithOptions, without making a subclass of UIView and overriding drawRect:.
UIGraphicsBeginImageContextWithOptions(CGSizeMake(80, 30), NO, 0);
// drawing code here
// Use UIKit functions and objects
// or use CoreGraphics functions on UIGraphicsGetCurrentContext()
UIImage *image = UIGraphicsGetImageFromCurrentContext();
UIGraphicsEndImageContext();

Draw bezier curve above image of UIImageView

How to draw beziercurve above the image of the UIImageView. I am subclassing UIImageView and drawing circles using UIBezierCurve on the corners of image view. But as soon as i place the image in UIImageView the inside part of circles are overlapped by the image. So is there any solution which lets me draw the UIBezier path on the UIimage of uiimageview ?
You can try subclassing simple UIView and drawing an image yourself using drawInRect: method of UIImage instance. It should look something like this:
- (void)drawRect:(CGRect)dirtyRect {
// note you can get image from anywhere
UIImage *image = [UIImage imageNamed:#"MyImage"];
[image drawInRect:self.bounds];
// now that image is drawn, everything
// past this point will be drawn on top of it
...
}

Custom NSView Fill Paints Over Bottom Bar

I have a window which has a custom NSView and has a bottom bar with controls on it, one of which is an NSColorWheel.
For simplicity sake the Window is 332px high, with the custom NSView being 300px high and the bottom bar being 32px high.
The bottom bar is created as part of my awakeFromNib when the app loads the window using the following code:
[[self window] setAutorecalculatesContentBorderThickness:YES forEdge:NSMinYEdge];
[[self window] setContentBorderThickness: 32.0 forEdge: NSMinYEdge];
In my custom NSView class I fill the rectangle with color. Everything works fine when the app loads using the following in my NSView class:
- (void)drawRect:(NSRect)dirtyRect
{
dirtyRect = [self bounds];
NSColor * mNewColor = [NSColor blackColor];
[mNewColor set];
[NSBezierPath fillRect:dirtyRect];
}
However, if I subsequently call a method that changes the color of the custom NSView when a color wheel in the bottom bar is changed, the bottom bar gets overwritten with the color. The following code illustrates this method (this code is in the custom NSView class:
- (void)changeBackgroundColor:(NSNotification *)notification
{
NSLog(#"Changed background color");
NSRect mRect = [self bounds];
NSColor * mNewColor = [theColorWell color];
[mNewColor set];
[NSBezierPath fillRect:mRect];
[self setNeedsDisplay:YES];
}
Resizing the window instantly corrects the problem, but obviously I don't want the user to have to resize the window for an obvious bug!
What I don't understand is why my bounds appear to be mapping to the parent window and not the custom NSView when I call setNeedsDisplay and yet the bound correctly adjust when I resize the window using the mouse (even if just by 1 pixel).
Do I somehow need to account for the bottom bar on the redraw?
Any and all help much appreciated.
You should do all your drawing in the drawRect: method of your custom NSView. Cocoa automatically sets up the graphics context for you when it calls this method - things may not draw correctly if you perform drawing operations in other methods.
Your code in drawRect: could set the colour to the the current background colour as specified by your NSColorWell and fill the dirtyRect rectangle with this.
Then in the other method just call [self setNeedsDisplay:YES]; and then drawRect: will automatically be called to redraw the view.
See here for more information: http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/CocoaViewsGuide/SubclassingNSView/SubclassingNSView.html#//apple_ref/doc/uid/TP40002978-CH7-SW4 (in particular the Drawing View Content section)