UIBezierPath bezierPathWithRoundedRect: the cornerRadius value is not consistent - ios

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

Related

doughnut chart with rounded

I want to create doughnut chart with overlapping one side with rounded corner. like
requirement is starting end should be bellow previous line and ending end should above next line. I tired to do it but last line layer show both on top. I tried with bellow code
private func drawCircle(){
var startAngle: CGFloat = .pi / -2
var endAngle: CGFloat = 0.0
drawableItems.forEach { (item) in
print(item.title)
print(startAngle)
let center = calculateCenter()
endAngle = startAngle + calculateAngles(item: item)
if endAngle > 2 * CGFloat.pi {
endAngle -= 2 * .pi
}
// Radius of the view
let radius: CGFloat = calculateRadius()
let arcWidth: CGFloat = self.arcWidth
let circlePath = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
circlePath.lineCapStyle = .round
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.strokeColor = item.color.cgColor
shapeLayer.lineWidth = arcWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
layer.addSublayer(shapeLayer)
startAngle = endAngle
}
}
output gives as bellow. in which last layer with green colour show both end top instead of one bellow previous and other on top of next
please help me to do so.
The issue is that your are drawing a simple arc with a thick line and rounded ends. The two rounded ends of the last arc is always going to cover the previous and first arcs.
The solution is to draw arcs that have a concave end that doesn't cover the convex end of the previous arc.
The following UIBezierPath extension will draw such an arc:
extension UIBezierPath {
convenience init(
chartArcCenter center: CGPoint,
radius: CGFloat,
lineWidth: CGFloat,
startAngle: CGFloat,
endAngle: CGFloat,
clockwise: Bool
) {
self.init()
// Draw inner radius arc
addArc(withCenter: center, radius: radius - lineWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
// Draw convex endcap
var x = cos(endAngle) * radius + center.x
var y = sin(endAngle) * radius + center.y
addArc(withCenter: CGPoint(x: x, y: y), radius: lineWidth / 2, startAngle: endAngle - .pi, endAngle: endAngle, clockwise: !clockwise)
// Draw outer radius arc
addArc(withCenter: center, radius: radius + lineWidth/2, startAngle: endAngle, endAngle: startAngle, clockwise: !clockwise)
// Draw concave startcap
x = cos(startAngle) * radius + center.x
y = sin(startAngle) * radius + center.y
addArc(withCenter: CGPoint(x: x, y: y), radius: lineWidth / 2, startAngle: startAngle, endAngle: startAngle + .pi, clockwise: clockwise)
close()
}
}
Use this in your code instead of the current UIBezierPath call. You can also make changes to the drawing settings of the shape layer.
With all changes in place your method becomes:
private func drawCircle(){
var startAngle: CGFloat = .pi / -2
var endAngle: CGFloat = 0.0
// Radius of the view
let radius: CGFloat = calculateRadius()
let arcWidth: CGFloat = self.arcWidth
let center = calculateCenter()
drawableItems.forEach { (item) in
endAngle = startAngle + calculateAngles(item: item)
let circlePath = UIBezierPath(chartArcCenter: center,
radius: radius/2 - arcWidth/2,
lineWidth: arcWidth,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = item.color.cgColor
layer.addSublayer(shapeLayer)
startAngle = endAngle
}
}
Also note that there's no reason for the if endAngle > 2 * CGFloat.pi { block. It's OK if the angle "wraps". And the calculations of radius, width and center only need to be done once so no need for those inside the loop.

Strange problem with UIBezierPath animation

I want to draw the Moon and then animate Moon's shadow. But after launching this code I can see some glitches on animation line:
GIF:
Why is this happening?
Playground code here
Update 1:
Both paths created by this function but with different angles (0 and π/2*0.6):
func calculateMoonPath(for angle: CGFloat) -> UIBezierPath {
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let radius = view.bounds.height/2
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: -.pi/2,
endAngle: .pi/2,
clockwise: true)
path.addArc(withCenter: .init(x: center.x - radius * tan(angle), y: center.y),
radius: radius / CGFloat(cosf(Float(angle))),
startAngle: .pi/2 - angle,
endAngle: angle - .pi/2,
clockwise: false
)
path.close()
return path
}
In my experience, the code that generates arcs creates different numbers of cubic bezier curves under the covers as the arc angle changes.
That changes the number of control points in the two curves, and messes up the animation. (as David Rönnqvist says, animations are undefined if the starting and ending path have a different number of control points.)
From what I've read, a full circle requires 4 cubic bezier curves to complete.
It wouldn't be that hard to create a variant of the addArc method that always built the arc using 4 cubic bezier curves, regardless of the arc angle. That's what I would suggest.
You could probably break your arc into 4 pieces (Using 4 sequential calls to addArc(withCenter:...) with different start and end angles such that they combine to make your desired full arc. Each of those should be short enough arc lengths to be a single Bezier curve, so you should get the same number of control points for the beginning and ending combined curve.
If you rewrite your calculateMoonPath function like this:
func calculateMoonPath(for angle: CGFloat) -> UIBezierPath {
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let radius = view.bounds.height/2
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: -.pi/2,
endAngle: .pi/2,
clockwise: true)
let startAngle = .pi/2 - angle
let endAngle = angle - .pi/2
let delta = (endAngle - startAngle) / 4
for index in 0...3 {
let thisStart = startAngle + delta * CGFloat(index)
let thisEnd = startAngle + delta * CGFloat(index + 1)
path.addArc(withCenter: .init(x: center.x - radius * tan(angle), y: center.y),
radius: radius / CGFloat(cosf(Float(angle))),
startAngle: thisStart,
endAngle: thisEnd,
clockwise: false
)
}
path.close()
return path
}
That yields the following:
The way you do each of the two lines,
is simply, two control points!
One at each end.
That's all there is to it. Don't try using an arc.
Here ...
https://www.desmos.com/calculator/cahqdxeshd

Drawing UIView Quad Circle

How to make UIView like above.
Tried below but its creating a semi circle type view.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: topView.bounds.size.width, y: topView.bounds.size.height / 2), radius: topView.bounds.size.height, startAngle: .pi, endAngle: 0.0, clockwise: false)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
topView.layer.mask = circleShape
Draw it yourself. There is nothing complicated.
With sample:
Start at CenterPoint
Go to BottomPoint (line)
Arc from BottomPoint to LeftPoint, and the angle are Pi/2 to Pi (in clockwise)
Go to leftPoint
Go to CenterPoint (line)
The UIBezierPath:
let bezierPath = UIBezierPath()
let centerPoint = CGPoint(x: view.bounds.width, y: 0)
let bottomPoint = CGPoint(x: view.bounds.width, y: view.bounds.height)
let leftPoint = CGPoint(x: 0, y: 0)
bezierPath.move(to: bottomPoint) //not needed
bezierPath.addArc(withCenter: centerPoint,
radius: view.bounds.height,
startAngle: CGFloat.pi/2.0,
endAngle: CGFloat.pi,
clockwise: true)
bezierPath.addLine(to: leftPoint) //not needed
bezierPath.addLine(to: centerPoint)
There are two "not needed", because they are implicit, but you might want to write them if they are "too much hidden" for you.
Why your self? Because, a circle will only have two points and fill between it. In other words, it won't go to "centerPoint" to fill it.
Example with the same angle I used in my handmade path:
There are 2 * .pi radians in a circle, and you're going from pi to 0, which is a semicircle. Your start and end angles have to be 0.5 pi apart.

Animate CALayer shadow from square to circle (or rounded corners)

I have a UIImageView, which animates from square to circle with this code:
CABasicAnimation *circleShapeAnimation = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
circleShapeAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
circleShapeAnimation.toValue = [NSNumber numberWithFloat:cornerRadius];
I also have a separate UIView (its backgroundColor is clearColor), which I use specifically to achieve an animated shadow. Everything animates fine, except the shape animation:
CABasicAnimation* shadowShapeAnimation = [CABasicAnimation animationWithKeyPath: #"shadowPath"];
shadowShapeAnimation.fromValue = (id)[UIBezierPath bezierPathWithRoundedRect: cell.placeImage.bounds cornerRadius:0.0f].CGPath;
shadowShapeAnimation.toValue = (id)[UIBezierPath bezierPathWithRoundedRect: cell.placeImage.bounds cornerRadius:cornerRadius].CGPath;
which, simply put, looks uglier than me with a hangover in the morning.
I would like to achieve a nice, smooth animation of rounded corners. Any idea how to?
The key was to use those methods:
func circlePathWithCenter(center: CGPoint, radius: CGFloat) -> UIBezierPath {
let circlePath = UIBezierPath()
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI), endAngle: -CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI/2), endAngle: 0, clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: CGFloat(M_PI/2), endAngle: CGFloat(M_PI), clockwise: true)
circlePath.closePath()
return circlePath
}
func squarePathWithCenter(center: CGPoint, side: CGFloat) -> UIBezierPath {
let squarePath = UIBezierPath()
let startX = center.x - side / 2
let startY = center.y - side / 2
squarePath.moveToPoint(CGPoint(x: startX, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.closePath()
return squarePath
}
Found here: iOS animate/morph shape from circle to square

How to add rounded corner to a UIBezierPath custom rectangle?

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.

Resources