Not doing CG drawings correctly - ios

Just started working with Core Graphics and I probably got no idea what's going on.
In the following code I'm trying to create a small rounded translucent black square overlaid on top of the UINavigationController, but so far nothing showed up...
UIView *notificationView = [[UIView alloc] initWithFrame:[[[self navigationController] view] frame]];
CGRect rect = CGRectMake(self.view.frame.size.width / 2 - 50, self.view.frame.size.height / 2, 100, 100);
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
[[UIColor colorWithWhite:0 alpha:0.5] setFill];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:10];
[path fill];
[notificationView setNeedsDisplay];
[[[self navigationController] view] addSubview:notificationView];

Option 1 (the most design-friendly one)
UIViews are not really meant for other objects to draw into them. It makes much more design sense to subclass UIView and let it do its own drawing in drawRect. That way, you don't have to paste so much code every time you want to use a notification view.
Option 2 (the easiest one, and probably best)
If you just want a translucent black rounded rectangle (I'm assuming for a loading indicator), you can do it much more easily by creating the UIView at the size you want and centering it in the view. Set its background color to the translucent color, [UIColor colorWithWhite:0.0 alpha:0.5]. Finally, add the line
notificationView.layer.cornerRadius = 10.0;
You may also need to put #import <QuartzCore/QuartzCore.h> in your header file, since this is a CALayer trick.
Option 3 (the roundabout one)
If you really want to do it the way you're already doing, change the notificationView to a UIImageView, then set the frame of the view to be the size of the black rounded rect. Then add this after you fill the path:
UIImage *indicatorImage = UIGraphicsGetImageFromCurrentImageContext();
notificationView.image = indicatorImage;
You don't need to call setNeedsDisplay anymore.
Hopefully one of these sounds good to you!

Related

How to improve performance in rendering image?

I am drawing image on a custom UIView. On resizing the view, the drawing performance goes down and it starts lagging.
My image drawing code is below:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *bpath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, width, height)];
CGContextAddPath(context, bpath.CGPath);
CGContextClip(context);
CGContextDrawImage(context, [self bounds], image.CGImage);
}
Is this approach correct?
You would be better using Instruments to find where the bottleneck is than asking on here.
However, what you will probably find is that every time the frame changes slightly the entire view will be redrawn.
If you're just using the drawRect to clip the view into an oval (I guess there's an image behind it or something) then you would be better off using a CAShapeLayer.
Create a CAShapeLayer and give it a CGPath then add it as a clipping layer to the view.layer.
Then you can change the path on the CAShapeLayer and it will update. You'll find (I think) that it performs much better too.
If your height and width are the same, you could just use a UIImageView instead of needing a custom view, and get the circular clipping by setting properties on the image view's layer. That approach draws nice and quickly.
Just set up a UIImageView (called "image" in my example) and then have your view controller do this once:
image.layer.cornerRadius = image.size.width / 2.0;
image.layer.masksToBounds = YES;

How to Draw a single point line in iOS

I was wondering what is the best way to draw a single point line?
My goal is to draw this line in a tableViewCell to make it look just like the native cell separator.
I don't want to use the native separator because i want to make in a different color and in a different position (not the bottom..).
At first i was using a 1px UIView and colored it in grey. But in Retina displays it looks like 2px.
Also tried using this method:
- (void)drawLine:(CGPoint)startPoint endPoint:(CGPoint)endPoint inColor:(UIColor *)color {
CGMutablePathRef straightLinePath = CGPathCreateMutable();
CGPathMoveToPoint(straightLinePath, NULL, startPoint.x, startPoint.y);
CGPathAddLineToPoint(straightLinePath, NULL, endPoint.x, endPoint.y);
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = straightLinePath;
UIColor *fillColor = color;
shapeLayer.fillColor = fillColor.CGColor;
UIColor *strokeColor = color;
shapeLayer.strokeColor = strokeColor.CGColor;
shapeLayer.lineWidth = 0.5f;
shapeLayer.fillRule = kCAFillRuleNonZero;
[self.layer addSublayer:shapeLayer];
}
It works in like 60% of the times for some reason.. Is something wrong with it?
Anyway ,i'd be happy to hear about a better way.
Thanks.
I did the same with a UIView category. Here are my methods :
#define SEPARATOR_HEIGHT 0.5
- (void)addSeparatorLinesWithColor:(UIColor *)color
{
[self addSeparatorLinesWithColor:color edgeInset:UIEdgeInsetsZero];
}
- (void)addSeparatorLinesWithColor:(UIColor *)color edgeInset:(UIEdgeInsets)edgeInset
{
UIView *topSeparatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, - SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
[topSeparatorView setBackgroundColor:color];
[self addSubview:topSeparatorView];
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, self.frame.size.height + SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
[separatorView setBackgroundColor:color];
[self addSubview:separatorView];
}
Just to add to Rémy's great answer, it's perhaps even simpler to do this. Make a class UILine.m
#interface UILine:UIView
#end
#implementation UILine
-(id)awakeFromNib
{
// careful, contentScaleFactor does NOT WORK in storyboard during initWithCoder.
// example, float sortaPixel = 1.0/self.contentScaleFactor ... does not work.
// instead, use mainScreen scale which works perfectly:
float sortaPixel = 1.0/[UIScreen mainScreen].scale;
UIView *topSeparatorView = [[UIView alloc] initWithFrame:
CGRectMake(0, 0, self.frame.size.width, sortaPixel)];
topSeparatorView.userInteractionEnabled = NO;
[topSeparatorView setBackgroundColor:self.backgroundColor];
[self addSubview:topSeparatorView];
self.backgroundColor = [UIColor clearColor];
self.userInteractionEnabled = NO;
}
#end
In IB, drop in a UIView, click identity inspector and rename the class to a UILine. Set the width you want in IB. Set the height to 1 or 2 pixels - simply so you can see it in IB. Set the background colour you want in IB. When you run the app it will become a 1-pixel line, that width, in that colour. (You probably should not be affected by any default autoresize settings in storyboard/xib, I couldn't make it break.) You're done.
Note: you may think "Why not just resize the UIView in code in awakeFromNib?" Resizing views upon loading, in a storyboard app, is problematic - see the many questions here about it!
Interesting gotchya: it's likely you'll just make the UIView, say, 10 or 20 pixels high on the storyboard, simply so you can see it. Of course it disappears in the app and you get the pretty one pixel line. But! be sure to remember self.userInteractionEnabled = NO, or it might get over your other, say, buttons!
2016 solution ! https://stackoverflow.com/a/34766567/294884
shapeLayer.lineWidth = 0.5f;
That's a common mistake and is the reason this is working only some of the time. Sometimes this will overlap pixels on the screen exactly and sometimes it won't. The way to draw a single-point line that always works is to draw a one-point-thick rectangle on integer boundaries, and fill it. That way, it will always match the pixels on the screen exactly.
To convert from points to pixels, if you want to do that, use the view's scale factor.
Thus, this will always be one pixel wide:
CGContextFillRect(con, CGRectMake(0,0,desiredLength,1.0/self.contentScaleFactor));
Here's a screen shot showing the line used as a separator, drawn at the top of each cell:
The table view itself has no separators (as is shown by the white space below the three existing cells). I may not be drawing the line in the position, length, and color that you want, but that's your concern, not mine.
AutoLayout method:
I use a plain old UIView and set its height constraint to 1 in Interface Builder. Attached it to the bottom via constraints. Interface builder doesn't allow you to set the height constraint to 0.5, but you can do it in code.
Make a connector for the height constraint, then call this:
// Note: This will be 0.5 on retina screens
self.dividerViewHeightConstraint.constant = 1.0/[UIScreen mainScreen].scale
Worked for me.
FWIW I don't think we need to support non-retina screens anymore. However, I am still using the main screen scale to future proof the app.
You have to take into account the scaling due to retina and that you are not referring to on screen pixels. See Core Graphics Points vs. Pixels.
Addition to Rémy Virin's answer, using Swift 3.0
Creating LineSeparator class:
import UIKit
class LineSeparator: UIView {
override func awakeFromNib() {
let sortaPixel: CGFloat = 1.0/UIScreen.main.scale
let topSeparatorView = UIView()
topSeparatorView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: sortaPixel)
topSeparatorView.isUserInteractionEnabled = false
topSeparatorView.backgroundColor = self.backgroundColor
self.addSubview(topSeparatorView)
self.backgroundColor = UIColor.clear
self.isUserInteractionEnabled = false
}
}

Use Bezier Path as Clipping Mask

I am wondering if it is possible to clip a view to a Bezier Path. What I mean is that I want to be able to see the view only in the region within the closed Bezier Path. The reason for this is that I have the outline of an irregular shape, and I want to fill in the shape gradually with a solid color from top to bottom. If I could make it so that a certain view is only visible within the path then I could simply create a UIView of the color I want and then change the y coordinate of its frame as I please, effectively filling in the shape. If anyone has any better ideas for how to implement this that would be greatly appreciated. For the record the filling of the shape will match the y value of the users finger, so it can't be a continuous animation. Thanks.
Update (a very long time later):
I tried your answer, Rob, and it works great except for one thing. My intention was to move the view being masked while the mask remains in the same place on screen. This is so that I can give the impression of the mask being "filled up" by the view. The problem is that with the code I have written based on your answer, when I move the view the mask moves with it. I understand that that is to be expected because all I did was add it as the mask of the view so it stands to reason that it will move if the thing it's tied to moves. I tried adding the mask as a sublayer of the superview so that it stays put, but that had very weird results. Here is my code:
self.test = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.test.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.test];
UIBezierPath *myClippingPath = [UIBezierPath bezierPath];
[myClippingPath moveToPoint:CGPointMake(100, 100)];
[myClippingPath addCurveToPoint:CGPointMake(200, 200) controlPoint1:CGPointMake(self.screenWidth, 0) controlPoint2:CGPointMake(self.screenWidth, 50)];
[myClippingPath closePath];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
self.test.layer.mask = mask;
CGRect firstFrame = self.test.frame;
firstFrame.origin.x += 100;
[UIView animateWithDuration:3 animations:^{
self.test.frame = firstFrame;
}];
Thanks for the help already.
You can do this easily by setting your view's layer mask to a CAShapeLayer.
UIBezierPath *myClippingPath = ...
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;
You will need to add the QuartzCore framework to your target if you haven't already.
In Swift ...
let yourCarefullyDrawnPath = UIBezierPath( .. blah blah
let maskForYourPath = CAShapeLayer()
maskForYourPath.path = carefullyRoundedBox.CGPath
layer.mask = maskForYourPath
Just an example of Rob's solution, there's a UIWebView sitting as a subview of a UIView called smoothView. smoothView uses bezierPathWithRoundedRect to make a rounded gray background (notice on right). That works fine.
But if smoothView has only normal clip-subviews, you get this:
If you do what Rob says, you get the rounded corners in smoothView and all subviews ...
Great stuff.

drawInRect method works too slow

Now i have following:
- (void)drawRect
{
// some drawing
[bgImage drawinRect:self.bounds];
// some drawing
}
I have more than 40 views with text and some marks inside. I need to repaint all these views on user tapping - it should be really fast!
I instrumented my code and saw: 75% of all execution time is [MyView drawRect:] and 95% of my drawRect time is [bgImage drawinRect:self.bounds] call. I need to draw background within GPU nor CPU. How is it possible?
What i have tried:
Using subviews instead of drawRect. In my case it is very slow because of unkickable color blending.
Adding UIImageView as background don't helps, we can't draw on subviews ...
Adding image layer to CALayer? Is it possible?
UPDATE:
Now i am trying to use CALayer instead of drawInRect.
- (void)drawRect
{
// some drawing
CALayer * layer = [CALayer layer];
layer.frame = self.bounds;
layer.contents = (id)image.CGImage;
layer.contentsScale = [UIScreen mainScreen].scale;
layer.contentsCenter = CGRectMake(capInsects.left / image.size.width,
capInsects.top / image.size.height,
(image.size.width - capInsects.left - capInsects.right) / image.size.width,
(image.size.height - capInsects.top - capInsects.bottom) / image.size.height);
// some drawing
}
Nothing instead of this new layer is visible right now. All my painting is under this layer i think...
I would use UILabels; you can reduce the cost of blending by giving the labels a background color or setting shouldRasterize = YES on the layer of the view holding those labels. (You'll probably also want to set rasterizationScale to be the same as [[UIScreen mainScreen] scale]).
Another suggestion:
There are lots of open-source calendar components that have probably had to try and solve the same issues. Perhaps you could look and see how they solved them.

Mirroring UIView

I have a UIView with a transparent background, and some buttons. I would like to capture the drawing of the view, shrink it, and redraw (mirror) it elsewhere on the screen. (On top of another view.) The buttons can change, so it isn't static.
What would be the best way to do this?
Check a nice sample code http://saveme-dot-txt.blogspot.com/2011/06/dynamic-view-reflection-using.html following WWDC sessions. It uses
CAReplicatorLayer
for reflection, pretty easy to implement and looks really smooth and impressive.
The general idea will be to get a UIView's layer to draw itself into a context and then grab a UIImage out of it.
UIGraphicsBeginImageContext(view.frame.size);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
You will also need to #import <QuartzCore/QuartzCore.h>
If you don't really need to capture the drawing (from what you describe, it seems unlikely that you need an image), create another instance of the view and apply a transform. Something like...
UIView* original [UIView alloc] initWithFrame:originalFrame];
UIView* copy = [[UIView alloc] initWithFrame:copyFrame];
// Scale down 50% and rotate 45 degrees
//
CGAffineTransform t = CGAffineTransformMakeScale(0.5, 0.5);
copy.transform = CGAffineTransformRotate(t, M_PI_4);
[someSuperView addSubview:original];
[someSuperView addSubview:copy];
// release, etc.
I added the rotation just to show that you can do a variety of different things with transformations.

Resources