How to Draw a single point line in iOS - 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
}
}

Related

Crazy rounded rect UIBezierPath behavior on iOS 7. What is the deal?

The simple UIView below draws a rounded rectangle. When I pass a corner radius of 65 or below it rounds correctly, but 66 and above and it generates a perfect circle! What is going on here? It should only show a circle when the corner radius is equal to 1/2 the frame width, but it seems that it is drawing a circle when the radius is about 1/3rd, no matter what the size of the view is. This behavior appears on iOS 7. On iOS 6 I get expected behavior.
#import "ViewController.h"
#interface MyView : UIView
#end
#implementation MyView
-(void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 200, 200) cornerRadius:65];
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, path.CGPath);
[[UIColor redColor] set];
CGContextStrokePath(c);
}
#end
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MyView *v = [[MyView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[self.view addSubview:v];
}
#end
This will probably never be fixed. Here is why.
The math to calculate if you can make a squircle is: radius * magicMultiplier * 2. If the result is longer than the side, it can't make a squircle so it makes a circle.
The magicMultiplier is required because to make it look like a squircle, the bezier curve needs to start from a longer distance than the radius. The magic multiplier provides that extra distance.
From my research and playing around with the bezier function, I believe the magic multiplier might be something around 1.0 + 8.0 / 15.0 = 1.533.
So 66*(1+8/15)*2 is 202.4 which is longer than the shortest side (200), thus it makes it a circle.
However! 65*(1+8/15)*2 is 199.33 which is smaller than 200, so it squircles it correctly.
Possible solutions
Code your own bezier curve function (or get one online)
Use the view's layer.cornerRadius to achieve the same thing since Apple doesn't clamp the corner radius here.
layer.cornerCurve = .continuous
layer.cornerRadius = min(radius, min(bounds.width, bounds.height)/2.0)
// You might want to clamp it yourself
Bear in mind that draw(in ctx) doesn't work with layer.maskedCorners. So you can't use SnapshotTesting with those.
FYI, to->circle bug happens at approx. 65%:
- "INFO --- width/height (SQUARE): 12.121212121212121"
- "INFO --- halfSize == maxRadius: 6.0606060606060606"
- "INFO --- cornerRadius: 3.967272727272727"
- "INFO --- ratioBug: 0.6546"
extension CGRect
{
// For generic Rectangle
// 28112022 (bug happens at 65%) (cornerRadius / maxRadius)
// radiusFactor: [0, 1]
func getOptimalCornerRadius(radiusFactor: CGFloat) -> CGFloat
{
let minSize = self.sizeMin()
let maxRadius = minSize / 2
let cornerRadius = maxRadius * radiusFactor
return cornerRadius
}
}

How to draw a coin (a grey circle with black text centered on it) in iOS?

I understand that the question might sound too simple, but only after struggling for several hours with this simple thing am I posting this here. (Yup, I am new to custom drawing and iOS dev in general).
I am trying to draw a coin. It is a gray circle with some text centered on it.
What I have tried so far:
CGFloat *radius = 30;
CGPoint center = self.view.center;
CGRect someFrame = CGRectMake(center.x - radius, center.y - radius,
2 * radius, 2 * radius);
UIView *circularView = [[UIView alloc] initWithFrame:someFrame];
circularView.backgroundColor = [UIColor grayColor];
UILabel *label = [[UILabel alloc] initWithFrame:someFrame];
label.text = #"5";
label.textColor = [UIColor blackColor];
label.textAlignment = NSTextAlignmentCenter;
[circularView addSubview:label];
circularView.clipsToBounds = YES;
circularView.layer.cornerRadius = radius;
[self.view addSubview:circularView];
Have tried this and other variants. But none of it is working. The label is not appearing, and the view is not being drawn in the center of the view as expected in landscape mode.
Is there any better way to do this, using CALayer or Quartz or Core Graphics? As I said, I am new to this.
First things first:
CGFloat *radius = 30;
That...shouldn't really even compile. Or at least there should be one mean warning. That's not what you want. You want to say this:
CGFloat radius = 30;
That asterisk is going to, just, that's bad. You'll know when you want to use a pointer to a primitive value and this just ain't one o' those times.
With that outta the way, you're on the right track, but it looks like you have a misunderstanding about coordinate spaces.
You initialize the circle with the frame someFrame. This is a frame that makes sense in the coordinate space of self.view, which is good, because that's where you're adding it (mostly, see side note below).
But then you set the label's frame to the same thing. Which might be okay, except that you're adding it to the circularView -- not to self.view. Which means that the frame that made sense as a frame for a child of self.view suddenly doesn't make very much sense at all. What you really want is just the following:
// The label will take up the whole bounds of the circle view.
// Labels automatically center their text vertically.
UILabel *label = [[UILabel alloc] initWithFrame:circularView.bounds];
// Center the text in the label horizontally.
label.textAlignment = NSTextAlignmentCenter;
// Make it so that the label's frame is tied to the bounds of
// the circular view, so that if its size changes in the future
// the label will still look right.
label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
The problem that you have now is that label is way off somewhere else, sticking off the edge of circularView (and thus getting clipped) because someFrame doesn't make sense in circularView's coordinate space.
Side note:
This won't work well if self.view.frame.origin isn't CGPointZero. It usually is, but what you really want is the center of the view's bounds, not the center of the view's frame. self.view.center gives you a point in the coordinate space of self.view.superview. It just so happens that this will appear to work as long as self.view.frame.origin is {0, 0}, but to be more technically correct, you should say:
CGPoint center = CGPointMake(CGRectGetMidX(self.view.bounds),
CGRectGetMidY(self.view.bounds));
You can also make circularView remain in the center of the view even as the view's bounds change (for example, if you rotate the device), with the following:
circularView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin;
Yes, typing out autoresizingMasks manually is the worst thing ever, but it's worth it in the long run (actually, in the long run, you'll probably go crazy and make a shorter helper method like me...).
You need to change the origin of the label's frame. When you add the label to the background view, the origin of its frame is relative to its superview.
So it would be something like CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius) for the background view, CGRectMake(0, 0, 2 * radius, 2 * radius) for the label.
Besides that, you can skip the background view and tint the UILabel's background color.
Your first problem is that you are creating a frame for the view container (circularView) with some non zero x and y. Then you are creating a label and giving it the same frame (with non zero x and y). Then you add the label to the container view. The label's x and y will be relative to the container view's x and y (offset, not centered). If you want the label and the container to share the same location on screen then change it to this:
UILabel *label = [[UILabel alloc] initWithFrame: circularView.bounds];
Another problem is with how simple you are making this you could do it all with the label (ignore the container view).
UILabel *label = [[UILabel alloc] initWithFrame:someFrame];
label.text = #"5";
label.textColor = [UIColor blackColor];
label.textAlignment = NSTextAlignmentCenter;
label.clipsToBounds = YES;
label.layer.cornerRadius = radius;
label.backgroundColor = [UIColor grayColor];
]self.view addSubview:label];

Not doing CG drawings correctly

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!

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.

Create a complex CGRect

I have a view with a border of 10 pixels drawn on the method.
I need to update the border color and I use [self setNeedsDisplay] to make it redraw
the view.
Since I need to update only the border I want to use : [self setNeedsDisplayInRect:rect] so it will draw only the border.
How can I get a rect of only the border with out the other areas of the view?
Thanks
Shani
You can't because a CGRect is rectangle, so it is a convex shape that can't have holes in it.
But you can decompose the border into four rectangles and call [self setNeedsDisplayInRect:rect] four times.
Also, if you import QuartzCore, you can probably use the property borderColor of the view's layer:
#import <QuartzCore/QuartzCore.h>
// ...
view.layer.borderWidth = 10;
view.layer.borderColor = [UIColor redColor].CGColor;
// And to change it later
view.layer.borderColor = [UIColor greenColor].CGColor;
You could get four CGRects around each part of the border (top, right, bottom, and left) and call the method four times with each of them.

Resources