I want to write a UILabel that can print square root expressions with a roof on it instead of just a simple √x. There should be a line over the x there as if it's written on paper.
Using Quartz 2D (the QuartzCore framework), you could just draw the line over it:
Thus, given
self.label.text = #"√23+45";
[self addSquareRootTopTo:self.label];
This would draw a line over the characters after the √ symbol:
- (void)addSquareRootTopTo:(UILabel *)label
{
NSRange range = [label.text rangeOfString:#"√"];
if (range.location == NSNotFound)
return;
NSString *stringThruSqrtSymbol = [label.text substringToIndex:range.location + 1];
NSString *stringAfterSqrtSymbol = [label.text substringFromIndex:range.location + 1];
CGSize sizeThruSqrtSymbol;
CGSize sizeAfterSqrtSymbol;
// get the size of the string given the label's font
if ([stringThruSqrtSymbol respondsToSelector:#selector(sizeWithAttributes:)])
{
// for iOS 7
NSDictionary *attributes = #{NSFontAttributeName : label.font};
sizeThruSqrtSymbol = [stringThruSqrtSymbol sizeWithAttributes:attributes];
sizeAfterSqrtSymbol = [stringAfterSqrtSymbol sizeWithAttributes:attributes];
}
else
{
// for earlier versions of iOS
sizeThruSqrtSymbol = [stringThruSqrtSymbol sizeWithFont:label.font];
sizeAfterSqrtSymbol = [stringAfterSqrtSymbol sizeWithFont:label.font];
}
// create path for line over the stuff after the square root sign
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(label.frame.origin.x + sizeThruSqrtSymbol.width, label.frame.origin.y)];
[path addLineToPoint:CGPointMake(label.frame.origin.x + sizeThruSqrtSymbol.width + sizeAfterSqrtSymbol.width, label.frame.origin.y)];
// now add that line to a CAShapeLayer
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
layer.lineWidth = 1.0;
layer.strokeColor = [[UIColor blackColor] CGColor];
layer.fillColor = [[UIColor clearColor] CGColor];
[self.view.layer addSublayer:layer];
}
That yields a less than satisfactory gap:
You could play around with this to nudge the top of the square root symbol down, but the alternative approach would be to use Quartz 2D to draw the entire square root symbol. Thus, take the square root symbol out of the label's text value:
self.label.text = #"23+45";
[self addSquareRootTo:self.label];
And then draw the whole square root symbol in Quartz 2D:
static CGFloat const part1 = 0.12; // tiny diagonal will be 12% of the height of the text frame
static CGFloat const part2 = 0.35; // medium sized diagonal will be 35% of the height of the text frame
- (void)addSquareRootTo:(UILabel *)label
{
CGSize size;
if ([label.text respondsToSelector:#selector(sizeWithAttributes:)])
{
NSDictionary *attributes = #{NSFontAttributeName : label.font};
size = [label.text sizeWithAttributes:attributes];
}
else
{
size = [label.text sizeWithFont:label.font];
}
UIBezierPath *path = [UIBezierPath bezierPath];
// it's going to seem strange, but it's probably easier to draw the square root size
// right to left, so let's start at the top right of the text frame
[path moveToPoint:CGPointMake(label.frame.origin.x + size.width, label.frame.origin.y)];
// move to the top left
CGPoint point = CGPointMake(label.frame.origin.x, label.frame.origin.y);
[path addLineToPoint:point];
// now draw the big diagonal line down to the bottom of the text frame (at 15 degrees)
point.y += size.height;
point.x -= sinf(15 * M_PI / 180) * size.height;
[path addLineToPoint:point];
// now draw the medium sized diagonal back up (at 30 degrees)
point.y -= size.height * part2;
point.x -= sinf(30 * M_PI / 180) * size.height * part2;
[path addLineToPoint:point];
// now draw the tiny diagonal back down (again, at 30 degrees)
point.y += size.height * part1;
point.x -= sinf(30 * M_PI / 180) * size.height * part1;
[path addLineToPoint:point];
// now add the whole path to our view
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
layer.lineWidth = 1.0;
layer.strokeColor = [[UIColor blackColor] CGColor];
layer.fillColor = [[UIColor clearColor] CGColor];
[self.view.layer addSublayer:layer];
}
That looks a little better:
Clearly, you can implement this any way you want (using either of the above, or probably better, subclassing a UIView to putting this, or its CoreGraphics equivalent, in drawRect), but it illustrates the idea of drawing the square root symbol yourself.
By the way, if using Xcode version prior 5, you'll have to manually add the QuartzCore.framework to your project and then include the appropriate header:
#import <QuartzCore/QuartzCore.h>
Related
How would one crosshatch (apply a set of parallel lines at 45 degrees) across the fill of a shape in IOS using core graphics? Sample code?
(I'm specially interested in use with an MKPolygon in MKMapKit, however for the moment just trying to see if it's possible in a UIView using drawRect?. So fill the background of a UIView with crosshatch'ing)
for swift 3., using approach from #user3230875
final class CrossHatchView: UIView {
// MARK: - LifeCycle
override func draw(_ rect: CGRect) {
// create rect path with bounds that equal to the
// size of a view, in addition it adds rounded corners, this will
// be used later as a canvas for dash drawing
let path:UIBezierPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5)
// specify the new area where the our drawing will be visible
// check [link][1] for more
path.addClip()
// grab the size of drawing area
let pathBounds = path.bounds
// cleanUp rounded rect, that is drawn above
// just remove roundedRect in the words
path.removeAllPoints()
// get start and end point of the line
let p1 = CGPoint(x:pathBounds.maxX, y:0)
let p2 = CGPoint(x:0, y:pathBounds.maxX)
// draw line
path.move(to: p1)
path.addLine(to: p2)
// set line width equal to double width of view
// because we later will draw this line using dash pattern
path.lineWidth = bounds.width * 2
// set dash pattern with some interval
let dashes:[CGFloat] = [0.5, 7.0]
path.setLineDash(dashes, count: 2, phase: 0.0)
// set color for line
UIColor.lightGray.withAlphaComponent(0.5).set()
// actually draw a line using specific
// color and dash pattern
path.stroke()
}
}
result:
Create a UIImage containing your crosshatch pattern in whatever way you want (e.g. by drawing it with Core Graphics or by loading it from a PNG file).
Then use +[UIColor colorWithPatternImage:] (Swift UIColor(patternImage:)) to create a “color” that draws the crosshatch image.
Finally, set the pattern color as your fill color, and fill the shape (presumably by filling a path that outlines the shape, or by using UIRectFill).
If you need more control over the pattern (to change how it's tiled or aligned), you can drop down to the Core Graphics level and use CGPatternCreate and CGColorCreateWithPattern.
Here's what I was talking about over in the Apple Developer Forum:
#import "CrossHatchView.h"
#implementation CrossHatchView
static CGFloat sides = 5.0;
- (void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat xCentre = CGRectGetMidX(bounds);
CGFloat yCentre = CGRectGetMidY(bounds);
CGFloat radius = 0.0;
if (CGRectGetWidth(bounds) > CGRectGetHeight(bounds)) {
radius = CGRectGetHeight(bounds) / 2.0;
} else {
radius = CGRectGetWidth(bounds) / 2.0;
}
CGFloat angleIncrement = 2.0 * M_PI / sides;
CGFloat initialAngle = ( M_PI + (2.0 * M_PI / sides) ) / 2.0;
for (NSUInteger i = 0; i < sides; i++) {
CGFloat angle = initialAngle + i * angleIncrement;
CGFloat x = xCentre + radius * cos(angle);
CGFloat y = yCentre + radius * sin(angle);
CGPoint point = CGPointMake(x, y);
if (i == 0) {
[path moveToPoint:point];
} else {
[path addLineToPoint:point];
}
}
[path closePath];
[[UIColor cyanColor] set];
[path addClip];
CGRect pathBounds = [path bounds];
[path removeAllPoints];
CGPoint p1 = pathBounds.origin;
CGPoint p2 = CGPointMake(CGRectGetMaxX(pathBounds), CGRectGetMaxY(pathBounds));
[path moveToPoint:p1];
[path addLineToPoint:p2];
path.lineWidth = 400.0;
CGFloat dashes[] = { 2.0, 2.0 };
[path setLineDash:dashes count:2 phase:0.0];
[[UIColor blackColor] set];
[path stroke];
}
#end
hey try this sample code which i tried on a 300x300 UIView
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1.5);
CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
int backward=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, backward, 0);
CGContextAddLineToPoint(context, 300, 300-backward);
backward=backward+20;
}
int backwardNegitive=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, 0,backwardNegitive);
CGContextAddLineToPoint(context, 300-backwardNegitive,300);
backwardNegitive=backwardNegitive+20;
}
int forward=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, 300-forward, 0);
CGContextAddLineToPoint(context, 0, 300-forward);
forward=forward+20;
}
int forwardNegative=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, 0,300+forwardNegative);
CGContextAddLineToPoint(context,300+forwardNegative,0);
forwardNegative=forwardNegative+20;
}
CGContextStrokePath(context);
}
Hope this help you.
I am experimenting with UIBezierPath. I want to make a grid view.
NSInteger yAxisIncremental = 0;
NSInteger xAxisIncremental = 0;
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineWidth = 1;
for (int x = 0; x <5; x++) {
[path moveToPoint:CGPointMake(xAxisIncremental, 0)];
[path addLineToPoint:CGPointMake(xAxisIncremental, self.frame.size.height)];
xAxisIncremental = xAxisIncremental + kWidthOfSquare;
}
for (int x = 0; x <=5; x++) {
[path moveToPoint:CGPointMake(0, yAxisIncremental)];
[path addLineToPoint:CGPointMake(self.frame.size.width, yAxisIncremental)];
yAxisIncremental = yAxisIncremental + kHeightOfSquare;
}
[[UIColor blackColor] setStroke];
[path stroke];
Notice the first line is thin and corresponding lines are a little thick.Have I done something wrong in the code or is it the expected behavior?
The first line you are drawing begins at 0. This means that half of the width of the line will probably be offscreen or outside of your view. You can solve this by starting at pixel 0.5 instead of 0.
Also if you choose to have a line width of 1.0 this means that on high-dpi devices the lines will be thicker than 1 actual pixel on the screen. To draw a line which is 1px in width you can use a lineWidth of 0 as stated in the UIBezierPath documentation:
A width of 0 is interpreted as the thinnest line that can be rendered on a particular device.
Here is some sample code for a rect points generation with safe area for the stroke.
objective-c
UIBezierPath *aPath = [UIBezierPath bezierPath];
CGFloat safeArea = 1.5;
CGFloat minX = rect.origin.x+safeArea;
CGFloat minY = rect.origin.y+safeArea;
CGFloat maxX = rect.origin.x+rect.size.width-safeArea;
CGFloat maxY = rect.origin.y+rect.size.height-safeArea;
//draw your rectangle with stroke safe area*2
...
.
I have coded as below for half circle in iOS using UIBezierPath and CAShapeLayer.
clockWiseLayer = [[CAShapeLayer alloc] init];
CGFloat startAngle = -M_PI_2;
CGFloat endAngle = M_PI + M_PI_2;
CGFloat width = CGRectGetWidth(imageView.frame)/2.0f + 30;
CGFloat height = CGRectGetHeight(imageView.frame)/2.0f +50;
CGPoint centerPoint = CGPointMake(width, height);
float radius = CGRectGetWidth(imageView.frame)/2+10;
clockWiseLayer.path = [UIBezierPath bezierPathWithArcCenter:centerPoint
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES].CGPath;
clockWiseLayer.fillColor = [UIColor clearColor].CGColor;
clockWiseLayer.strokeColor = [UIColor blueColor].CGColor;
clockWiseLayer.borderColor = [UIColor greenColor].CGColor;
clockWiseLayer.backgroundColor = [UIColor redColor].CGColor;
clockWiseLayer.strokeStart = 0.0f;
clockWiseLayer.strokeEnd = 0.5f;
clockWiseLayer.lineWidth = 2.0f;
clockWiseLayer.borderWidth = 5.0f;
clockWiseLayer.shouldRasterize = NO;
[self.layer addSublayer:clockWiseLayer];
This results as below.
I want this blue half circle on the opposite side of the World Globe.
It is half Circle, but I want it on the other side, also CounterClockWise.
I am unable to set start and end angle for that, while clockwise:NO.
Thanks.
Check documentation for coordinates:
http://i.stack.imgur.com/1yJo6.png
Y-Axis is reversed compared to standard coordinate system in mathematics.
You should draw from 1/2*PI (bottom anchor) to 3/2*PI (top anchor) and then you set strokeStart to 0.0f and strokeEnd to 1.0f (so it fills whole path).
Working code using iOS constants:
CGFloat startAngle = M_PI_2;
CGFloat endAngle = startAngle + M_PI;
CGFloat width = CGRectGetWidth(imageView.frame)/2.0f;
CGFloat height = CGRectGetHeight(imageView.frame)/2.0f;
CGPoint centerPoint = CGPointMake(width, height);
float radius = CGRectGetWidth(imageView.frame)/2+10;
CAShapeLayer* clockWiseLayer = [[CAShapeLayer alloc] init];
clockWiseLayer.path = [UIBezierPath bezierPathWithArcCenter:centerPoint
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES].CGPath;
clockWiseLayer.fillColor = [UIColor clearColor].CGColor;
clockWiseLayer.strokeColor = [UIColor blueColor].CGColor;
clockWiseLayer.borderColor = [UIColor greenColor].CGColor;
clockWiseLayer.backgroundColor = [UIColor redColor].CGColor;
clockWiseLayer.strokeStart = 0.0f;
clockWiseLayer.strokeEnd = 1.0f;
clockWiseLayer.lineWidth = 2.0f;
clockWiseLayer.borderWidth = 5.0f;
[self.layer addSublayer:clockWiseLayer];
You start from top of the circle with -M_PI_2 and end at M_PI + M_PI_2 (if you want to have full circle and limit it using strokeEnd, strokeStart). Then set the circle path to draw from half of the end of the path (left side of image) instead from beginning to half (right side of the image)
CGFloat startAngle = -M_PI_2;
CGFloat endAngle = M_PI + M_PI_2;
clockWiseLayer.strokeStart = .5f;
clockWiseLayer.strokeEnd = 1.0f;
I have played with bezierPathWithArcCenter and it works quite strange if clockwise = NO. But if you want to create an circle bezier path with conterclockwise direction you can create a clockwise path and then revert it with bezierPathByReversingPath
A new path object with the same path shape but for which the path has been created in the reverse direction.
I'm trying to build a design app and one of the features allows people to preview different types of custom borders on various assets.
I am trying to get these borders to draw using UIBezierPath.
Right now the system will support:
various border radii (anything from 0 -> a ceiling calculated elsewhere )
different colors for the entire border
I'm trying to implement:
The ability to have different border line widths.
I think I should be able to achieve this by drawing each path with a different width, but I can't figure out how to change it from path to path. Can anyone help me out?
Thanks.
Click top see the border script in action
+(void)setBorderOnView :(id)object withWidth:(float)width andBorders:(NSArray*)borders ofColor:(UIColor*)color andRadius:(float)radius andRadii:(NSArray*)radii
{
UIView *objectView = object;
// Add half a pixel to compensate for border stroke, which puts .5 pixels outside of view.
float borderWidth = width+0.5;
float topLeftRadius = radius;
float topRightRadius = radius;
float bottomRightRadius = radius;
float bottomLeftRadius = radius;
CAShapeLayer *roundedCornerLayer = [CAShapeLayer layer];
CAShapeLayer *cornerMaskLayer = [CAShapeLayer layer];
UIColor * borderColor = color;
roundedCornerLayer.strokeColor = borderColor.CGColor;
// Begin corner path
UIBezierPath *roundedCorners = [UIBezierPath bezierPath];
[roundedCorners moveToPoint:CGPointMake(0, objectView.frame.size.height - bottomLeftRadius)];
[roundedCorners addLineToPoint:CGPointMake(0, 0 + topLeftRadius)];
if(topLeftRadius != 0){
[roundedCorners addArcWithCenter:CGPointMake(topLeftRadius, topLeftRadius)
radius:radius
startAngle:DEGREES_TO_RADIANS(180)
endAngle:DEGREES_TO_RADIANS(270)
clockwise:YES];
}
[roundedCorners addLineToPoint:CGPointMake(objectView.frame.size.width - topRightRadius, 0)];
if(topRightRadius != 0){
[roundedCorners addArcWithCenter:CGPointMake(objectView.frame.size.width - topRightRadius, 0 + topRightRadius)
radius:radius
startAngle:DEGREES_TO_RADIANS(270)
endAngle:DEGREES_TO_RADIANS(360)
clockwise:YES];
}
[roundedCorners addLineToPoint:CGPointMake(objectView.frame.size.width, objectView.frame.size.height - bottomRightRadius)];
if(bottomRightRadius != 0){
[roundedCorners addArcWithCenter:CGPointMake(objectView.frame.size.width - bottomRightRadius, objectView.frame.size.height - bottomRightRadius)
radius:radius
startAngle:DEGREES_TO_RADIANS(0)
endAngle:DEGREES_TO_RADIANS(90)
clockwise:YES];
}
[roundedCorners addLineToPoint:CGPointMake(0 + bottomLeftRadius, objectView.frame.size.height)];
if(bottomLeftRadius != 0){
[roundedCorners addArcWithCenter:CGPointMake(0 + bottomLeftRadius, objectView.frame.size.height - bottomLeftRadius)
radius:radius
startAngle:DEGREES_TO_RADIANS(90)
endAngle:DEGREES_TO_RADIANS(180)
clockwise:YES];
}
roundedCornerLayer.path = roundedCorners.CGPath;
roundedCornerLayer.fillColor = [UIColor clearColor].CGColor;
roundedCornerLayer.lineWidth = borderWidth;
[cornerMaskLayer setPath:roundedCorners.CGPath];
objectView.layer.mask = cornerMaskLayer;
[objectView.layer addSublayer:roundedCornerLayer];
}
roundedCornerLayer.lineWidth = borderWidth; sets the width of the path. I'm not sure if you want the same path to have different widths or different paths with different widths.
If you want different widths in one path then you must break it into separate subpaths and each will have a different lineWidth.
If you want to have different paths then just change the value of width when you call the method.
Did I miss anything?
Hiiii
I'm trying to draw in a view an arc with dynamic width.
I have built an ARRAY with several UIBezierPath with arcs then I draw one after the other. this is the code:
- (void)drawRect:(CGRect)rect
{
// Drawing code
CGFloat radious;
if (self.bounds.size.width < self.bounds.size.height) {
radious = self.bounds.size.width / 2;
}
else{
radious = self.bounds.size.height / 2;
}
float oneGradeInRadians = (float)(2 * M_PI) / 360.f;
float radiandToDraw = (float)self.finalAngleRadians - self.initialAngleRadians;
float splitMeasure = oneGradeInRadians;
int numberOfArcs = radiandToDraw / splitMeasure;
NSMutableArray *arrayOfBeziersPaths = [NSMutableArray arrayWithCapacity:numberOfArcs];
for (int i = 0; i < numberOfArcs; i++) {
float startAngle = self.initialAngleRadians + (i * splitMeasure);
float endAngle = self.initialAngleRadians +((i + 1) * splitMeasure);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/2) radius:radious - self.widthLine.floatValue startAngle:startAngle endAngle:endAngle clockwise:YES];
bezierPath.lineWidth = self.widthLine.floatValue + i/20;
float hue = (float)(i / 3.f);
UIColor *color = [UIColor colorWithHue:hue/360.0 saturation:1 brightness:1 alpha:1];
[color setStroke];
[arrayOfBeziersPaths addObject:bezierPath];
}
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineJoin(context, kCGLineJoinMiter);
//We saved the context
CGContextSaveGState(context);
for (int i = 0; i < numberOfArcs; i++) {
[(UIBezierPath *)[arrayOfBeziersPaths objectAtIndex:i] stroke];
[(UIBezierPath *)[arrayOfBeziersPaths objectAtIndex:i] fill];
}
//Restore the contex
CGContextRestoreGState(context);
}
In this way I get the right shape but, also I get an annoying lines between the arcs:
I think the problem may be a property to change between arcs, but I'm totally lost, or maybe there is another better way to build this.
I tried creating several UIBezierPath paths and I added them to an unique UIBezierPath, then I stoke and fill that path. I didn't get the annoying lines, but the problem is I can not modify the line width, so I can not get the same effect.
Any idea? thanks
dont create many such paths. just create two arcs (inner circle, exterior circle) and two straight lines joining the ends. then fill the path.
Add below code in a viewDidLoad of an empty viewController and check.
- (void)viewDidLoad
{
[super viewDidLoad];
CGFloat k =0.5522847498;
UIBezierPath *path =[UIBezierPath bezierPath];
CGFloat radius=130;
[path addArcWithCenter:CGPointMake(0, 0) radius:radius startAngle:M_PI_2 endAngle:M_PI clockwise:NO];
CGFloat start=10;
CGFloat increment=5;
[path addLineToPoint:CGPointMake(-radius-start, 0)];
[path addCurveToPoint:CGPointMake(0, -radius-start-increment) controlPoint1:CGPointMake(-radius-start, -(radius +start)*k) controlPoint2:CGPointMake(-(radius+start)*k, -radius-start-increment)];
[path addCurveToPoint:CGPointMake(radius+start+2*increment, 0) controlPoint1:CGPointMake((radius+start+increment)*k, -radius-start-increment) controlPoint2:CGPointMake(radius+start+2*increment, (-radius-start-increment)*k)];
[path addCurveToPoint:CGPointMake(0, radius+start+3*increment) controlPoint1:CGPointMake(radius+start+2*increment, (radius+start+2*increment)*k) controlPoint2:CGPointMake((radius+start+2*increment)*k,radius+start+3*increment)];
[path addLineToPoint:CGPointMake(0,radius)];
CAShapeLayer *layer =[CAShapeLayer layer];
[layer setFrame:CGRectMake(150, 200, 300, 300)];
[layer setFillColor:[UIColor greenColor].CGColor];
[layer setStrokeColor:[UIColor blackColor].CGColor];
[layer setPath:path.CGPath];
[self.view.layer addSublayer:layer];
}
It produced result like,
The problem here is that you’ve chosen a crazy approach to drawing the shape you’re after. If you take a look at it, you’ll see that it’s actually a filled region bounded on the outside by a circular arc, and on the inner edge by a spiral.
What you should do, therefore, is create a single NSBezierPath, add the outer circular arc to it, then add a line to the start of your spiral, append a spiral (you’ll want to approximate it with Bézier segments) and finally call -closePath. After that, you can -fill the path and you’ll get a good looking result rather than the mess you have above.