I want to draw a round rect in an CALayer using [UIBezierPath bezierPathWithRoundedRect: cornerRadius:], when setting up the rect's cornerRadius, I found that it is not consistent from 0 to maximum radius(about half of the layer's bound). The value jumped at about 1/3 of maximum radius which is quite confusing. After some research, I found probably it's a bug of iOS7 style of UIBezierPath drawing a round rect. PaintCode's research on that. So my question is how to draw an old style of perfect round rect with consistent cornerRadius value change?
Took a look at the PaintCode link you posted (although, it's much easier to answer questions here that explain the issue without having to go read an article somewhere else)...
Yes, it appears UIBezierPath(roundedRect: ...) is still buggy when the radius is greater than - as you noted - roughly 1/3 of 1/2 of the rect dimension.
To create a rounded rect path "manually," we can do this (using the rect dimensions and corner radius from that link):
let r: CGRect = CGRect(x: 0, y: 0, width: 150.0, height: 153.0)
let cornerRad: CGFloat = 50.0
// center points for the corner arcs
let ptCTR: CGPoint = CGPoint(x: r.maxX - cornerRad, y: r.minY + cornerRad)
let ptCBR: CGPoint = CGPoint(x: r.maxX - cornerRad, y: r.maxY - cornerRad)
let ptCBL: CGPoint = CGPoint(x: r.minX + cornerRad, y: r.maxY - cornerRad)
let ptCTL: CGPoint = CGPoint(x: r.minX + cornerRad, y: r.minY + cornerRad)
let bez: UIBezierPath = UIBezierPath()
// Top-Right corner
bez.addArc(withCenter: ptCTR, radius: cornerRad, startAngle: .pi * 1.5, endAngle: .pi * 0.0, clockwise: true)
// Bottom-Right corner
bez.addArc(withCenter: ptCBR, radius: cornerRad, startAngle: .pi * 0.0, endAngle: .pi * 0.5, clockwise: true)
// Bottom-Left corner
bez.addArc(withCenter: ptCBL, radius: cornerRad, startAngle: .pi * 0.5, endAngle: .pi * 1.0, clockwise: true)
// Top-Left corner
bez.addArc(withCenter: ptCTL, radius: cornerRad, startAngle: .pi * 1.0, endAngle: .pi * 1.5, clockwise: true)
// close the path
bez.close()
I want to draw a semi circle between 2 points on a circle. The main represents a clock and i want to draw another line to represent a progress from one hour to another so the points position may vary. First of all i know the X and Y of the 2 points i am interested in. This is how i try to add angles in UIBezierPath. My problem is that the new circle starts correctly but ends at a totally random location
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x) ```
let circlePath1 = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true) ```
Wherever i set the redPoint, the circle starts at a correct location but the circle never ands at bluePoint.
I have tried your code and it works for me if points are actually on that circumference
let redPoint = CGPoint(x: 100.0, y: 200.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x)
let path = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true)
//Path in layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer)
or for example:
let redPoint = CGPoint(x: 200.0, y: 100.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
You get:
But if you use coordinates that are not actually on your circumference, you get wrong results. My suggestion is to check your inputs and eventually if points are belonging to your desired circumference or not.
You're doing this backwards. Don't try to get the angle by starting with the location of the little red and blue filled circles. Use the angle to place the little red and blue filled circles.
In that example, my angles are -1 and 2.8 (radians). First I draw the arc (just as you did); then I superimpose the circles, which is trivial because I know where their centers are (the endpoints of the arc) by converting polar to cartesian coordinates.
I want to change the radius of two corners of a SKShapeNode (rect) but I didn't find a working solution.
I've tried to use a path, and it didn't work.
Swift 4.2, iOS 12.1.1, Xcode 10.1
let shape = SKShapeNode()
shape.path = UIBezierPath(roundedRect: CGRect(x: -128, y: -128, width: 256, height: 256), cornerRadius: 64).cgPath
shape.position = CGPoint(x: frame.midX, y: frame.midY)
shape.fillColor = UIColor.red
shape.strokeColor = UIColor.blue
shape.lineWidth = 10
addChild(shape)
I made a function that will allow you to customize each corner radius to whatever size you'd like. You can have 1,2,3 or 4 corners with a radius. If you always just want two corners then I would suggest making a wrapper function so you don't have so many parameters to fill in each time you call it.
func CustomRoundRectPath(_ rect:CGRect, _ TLR:CGFloat,_ TRR:CGFloat,_ BLR:CGFloat,_ BRR:CGFloat) -> CGPath {
let w = rect.width
let h = rect.height
//TLP:(TLP)
let TLP = CGPoint(x: TLR, y: h - TLR)
let TRP = CGPoint(x: w - TRR, y: h - TRR)
let BLP = CGPoint(x: BLR, y: BLR)
let BRP = CGPoint(x: w - BRR, y: BRR)
//Create path and addComponents
let path = CGMutablePath()
path.addArc(center: TLP, radius: TLR, startAngle: CGFloat.pi, endAngle: CGFloat.pi/2, clockwise: true)
path.addLine(to: CGPoint(x: TRP.x, y: h))
path.addArc(center: TRP, radius: TRR, startAngle: CGFloat.pi/2, endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: w, y: BRP.y))
path.addArc(center: BRP, radius: BRR, startAngle: 0, endAngle: -CGFloat.pi/2, clockwise: true)
path.addLine(to: CGPoint(x: BLP.x, y: 0))
path.addArc(center: BLP, radius: BLR, startAngle: -CGFloat.pi/2, endAngle: -CGFloat.pi, clockwise: true)
path.addLine(to: CGPoint(x: 0, y: TLP.y))
return path
}
I'm using SpriteKit and trying to achieve the following effect using arcs
I created three SKShapeNodes and assigned UIBezierPath's CGPath property to the SKShapeNode's path property..
Here is my code:
func createCircle(){
//container contains the SKShapeNodes
self.container = SKShapeNode()
self.container.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
//creating UIBezierPaths
let arc = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 100, startAngle: CGFloat(M_PI), endAngle: CGFloat(M_PI/2) , clockwise: false)
arc.flatness = 100
self.red = SKShapeNode()
self.red.lineWidth = 20
self.red.strokeColor = SKColor.redColor()
self.red.path = arc.CGPath
let arc1 = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 100, startAngle: CGFloat(M_PI/2), endAngle: CGFloat(0.0), clockwise: false)
self.blue = SKShapeNode()
self.blue.strokeColor = SKColor.whiteColor()
self.blue.lineWidth = 20
self.blue.path = arc1.CGPath
let arc2 = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 100, startAngle: 0.1, endAngle: CGFloat(3*M_PI/2), clockwise: false)
self.yellow = SKShapeNode()
self.yellow.strokeColor = SKColor.yellowColor()
self.yellow.lineWidth = 20
self.yellow.path = arc2.CGPath
//adding arcs to the container
self.container.addChild(red)
self.container.addChild(yellow)
self.container.addChild(blue)
//adding container to the GameScene
self.addChild(container)
}
After experimenting with the startAngle and endAngle I was able to make this:
The code output shape is not identical to the circle I want to create. How can I get the gaps that are in the desired effect image?
Using this image as a reference I was able to make changes to startAngle and endAngleof UIBezierPath arc method
Result Image
I managed to create the rounded corners, but I'm having trouble with the first rounded corner (lower right )
Question :
Can I add an (addArcWithCenter) method before the ( moveToPoint ) method ?
How can i get rid of the straight line at the beginning of the rectangle (lower right) ?
here is my code for the custom rectangle and a screenshot :
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 300, y: 0))
path.addArcWithCenter(CGPoint(x: 300-10, y: 50), radius: 10 , startAngle: 0 , endAngle: CGFloat(M_PI/2) , clockwise: true) //1st rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 50), radius:10, startAngle: CGFloat(2 * M_PI / 3), endAngle:CGFloat(M_PI) , clockwise: true)// 2rd rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 10), radius:10, startAngle: CGFloat(M_PI), endAngle:CGFloat(3 * M_PI / 2), clockwise: true)// 3rd rounded corner
// little triangle at the bottom
path.addLineToPoint(CGPoint(x:240 , y:0))
path.addLineToPoint(CGPoint(x: 245, y: -10))
path.addLineToPoint(CGPoint(x:250, y: 0))
path.addArcWithCenter(CGPoint(x: 290, y: 10), radius: 10, startAngle: CGFloat(3 * M_PI / 2), endAngle: CGFloat(2 * M_PI ), clockwise: true)
path.closePath()
I think what you're doing is overly complicated. UIBezierPath gives you UIBezierPath(roundedRect:) so why not use it? Stroke the rounded rectangle; erase the spot where you're going to put the little triangle; add the triangle; fill the compound path; and stroke the missing two sides of the triangle. Like this (this is just some code I happened to have lying around - you should change the numbers to fit your shape, of course):
let con = UIGraphicsGetCurrentContext()
CGContextTranslateCTM(con, 10, 10)
UIColor.blueColor().setStroke()
UIColor.blueColor().colorWithAlphaComponent(0.4).setFill()
let p = UIBezierPath(roundedRect: CGRectMake(0,0,250,180), cornerRadius: 10)
p.stroke()
CGContextClearRect(con, CGRectMake(20,170,10,11))
let pts = [
CGPointMake(20,180), CGPointMake(20,200),
CGPointMake(20,200), CGPointMake(30,180)
]
p.moveToPoint(pts[0])
p.addLineToPoint(pts[1])
p.addLineToPoint(pts[3])
p.fill()
CGContextStrokeLineSegments(con, pts, 4)
A couple of observations:
Make sure that you take the view bounds and inset it by half of the line width. That ensures that the entire stroked border falls within the bounds of the view. If your line width is 1, this might not be so obvious, but with larger line widths, the problem becomes more pronounced.
If using draw(_:) method, don’t use the rect that is passed to this method, but rather refer to the bounds (inset, as described above). The CGRect passed to draw(_:) is the rectangle being drawn, not necessarily the full bounds. (It generally is, but not always, so always refer to the bounds of the view, not the rect passed to this method.)
As the documentation says (emphasis added):
The portion of the view’s bounds that needs to be updated. The first time your view is drawn, this rectangle is typically the entire visible bounds of your view. However, during subsequent drawing operations, the rectangle may specify only part of your view.
I’d give all of the the various properties of the view a didSet observer that will trigger the view to be redrawn. That way, any IB overrides or programmatically set values will be reflected in the resulting view automatically.
If you want, you can make the whole thing #IBDesignable and make the properties #IBInspectable, so you can see this rendered in Interface Builder. It’s not necessary, but can be useful if you want to see this rendered in storyboards or NIBs.
While you can round corners using a circular arc, using a quad curve is easier, IMHO. You just specify where the arc ends and the corner of the rectangle, and the quadratic bezier will produce a nicely rounded corner. Using this technique, no calculation of angles or the center of the arc is necessary.
Thus:
#IBDesignable
public class BubbleView: UIView {
#IBInspectable public var lineWidth: CGFloat = 1 { didSet { setNeedsDisplay() } }
#IBInspectable public var cornerRadius: CGFloat = 10 { didSet { setNeedsDisplay() } }
#IBInspectable public var calloutSize: CGSize = CGSize(width: 10, height: 5) { didSet { setNeedsDisplay() } }
#IBInspectable public var fillColor: UIColor = .yellow { didSet { setNeedsDisplay() } }
#IBInspectable public var strokeColor: UIColor = .black { didSet { setNeedsDisplay() } }
override public func draw(_ rect: CGRect) {
let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let path = UIBezierPath()
// lower left corner
path.move(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - calloutSize.height))
path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - calloutSize.height - cornerRadius),
controlPoint: CGPoint(x: rect.minX, y: rect.maxY - calloutSize.height))
// left
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
// upper left corner
path.addQuadCurve(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY),
controlPoint: CGPoint(x: rect.minX, y: rect.minY))
// top
path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
// upper right corner
path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.minY + cornerRadius),
controlPoint: CGPoint(x: rect.maxX, y: rect.minY))
// right
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - calloutSize.height - cornerRadius))
// lower right corner
path.addQuadCurve(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - calloutSize.height),
controlPoint: CGPoint(x: rect.maxX, y: rect.maxY - calloutSize.height))
// bottom (including callout)
path.addLine(to: CGPoint(x: rect.midX + calloutSize.width / 2, y: rect.maxY - calloutSize.height))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX - calloutSize.width / 2, y: rect.maxY - calloutSize.height))
path.close()
fillColor.setFill()
path.fill()
strokeColor.setStroke()
path.lineWidth = lineWidth
path.stroke()
}
}
That yields:
Instead of starting the code with a straight line :
path.moveToPoint(CGPoint(x: 300, y: 0))
I instead start with an arc (upper right):
path.addArcWithCenter(CGPoint(x: 300-10, y: 50), radius: 10 , startAngle: 0 , endAngle: CGFloat(M_PI/2) , clockwise: true) //1st rounded corner
and by doing this, I have four rounded corners and I just need to add a straight line at the end of the code right before:
path.closePath()
Here is the code and a screenshot:
let path = UIBezierPath()
path.addArcWithCenter(CGPoint(x: 300-10, y: 50), radius: 10 , startAngle: 0 , endAngle: CGFloat(M_PI/2) , clockwise: true) //1st rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 50), radius:10, startAngle: CGFloat(2 * M_PI / 3), endAngle:CGFloat(M_PI) , clockwise: true)// 2rd rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 10), radius:10, startAngle: CGFloat(M_PI), endAngle:CGFloat(3 * M_PI / 2), clockwise: true)// 3rd rounded corner
// little triangle
path.addLineToPoint(CGPoint(x:240 , y:0))
path.addLineToPoint(CGPoint(x: 245, y: -10))
path.addLineToPoint(CGPoint(x:250, y: 0))
path.addArcWithCenter(CGPoint(x: 290, y: 10), radius: 10, startAngle: CGFloat(3 * M_PI / 2), endAngle: CGFloat(2 * M_PI ), clockwise: true)
path.addLineToPoint(CGPoint(x:300 , y:50))
path.closePath()
Swift 5 with configuration variables:
override func draw(_ rect: CGRect) {
let arrowXOffset: CGFloat = 13
let cornerRadius: CGFloat = 6
let arrowHeight: CGFloat = 6
let mainRect = CGRect(origin: rect.origin, size: CGSize(width: rect.width, height: rect.height - arrowHeight))
let leftTopPoint = mainRect.origin
let rightTopPoint = CGPoint(x: mainRect.maxX, y: mainRect.minY)
let rightBottomPoint = CGPoint(x: mainRect.maxX, y: mainRect.maxY)
let leftBottomPoint = CGPoint(x: mainRect.minX, y: mainRect.maxY)
let leftArrowPoint = CGPoint(x: leftBottomPoint.x + arrowXOffset, y: leftBottomPoint.y)
let centerArrowPoint = CGPoint(x: leftArrowPoint.x + arrowHeight, y: leftArrowPoint.y + arrowHeight)
let rightArrowPoint = CGPoint(x: leftArrowPoint.x + 2 * arrowHeight, y: leftArrowPoint.y)
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: rightTopPoint.x - cornerRadius, y: rightTopPoint.y + cornerRadius), radius: cornerRadius,
startAngle: CGFloat(3 * Double.pi / 2), endAngle: CGFloat(2 * Double.pi), clockwise: true)
path.addArc(withCenter: CGPoint(x: rightBottomPoint.x - cornerRadius, y: rightBottomPoint.y - cornerRadius), radius: cornerRadius,
startAngle: 0, endAngle: CGFloat(Double.pi / 2), clockwise: true)
path.addLine(to: rightArrowPoint)
path.addLine(to: centerArrowPoint)
path.addLine(to: leftArrowPoint)
path.addArc(withCenter: CGPoint(x: leftBottomPoint.x + cornerRadius, y: leftBottomPoint.y - cornerRadius), radius: cornerRadius,
startAngle: CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi), clockwise: true)
path.addArc(withCenter: CGPoint(x: leftTopPoint.x + cornerRadius, y: leftTopPoint.y + cornerRadius), radius: cornerRadius,
startAngle: CGFloat(Double.pi), endAngle: CGFloat(3 * Double.pi / 2), clockwise: true)
path.addLine(to: rightTopPoint)
path.close()
}
You can't do this automatically. You have to make the lines shorter and then use arcs of the radius that you want the corner radius to be.
So. Instead of adding a line to x,y you add the line to x-radius, y.
Then add the arc. Then the next line starts at x, y+radius.