Draw iOS 7-style squircle programmatically - ios

I'm trying to find a way to draw a iOS 7-style icon 'squircle' shape programmatically, using core graphics. I'm not asking how to draw a rounded rectangle. A squircle is a superellipse:
which is slightly different than a regular rounded rectangle:
It's exact formula is readily available. However, I can't figure out how to draw this using, for example, a CGPath, let alone fill it, and be able to resize it rather easily. All this while being entirely exact with the formula.

in iOS 13/ Xcode 11 you can now use CALayerCornerCurve
Example
yourLayer.cornerCurve = CALayerCornerCurve.continuous
source: https://developer.apple.com/documentation/quartzcore/calayercornercurve

Quote from Wikipedia: Superellipse
For n = 1/2, in particular, each of the four arcs is a Quadratic Bézier curve defined by the two axes; as a result, each arc is a segment of a parabola.
So why not try to approximate Squircle using Bezier curves? Both curves (Bezier and Squircle) are defined by the parametric equations.
UIBezierPath Class have method: addCurveToPoint:controlPoint1:controlPoint2:
Appends a cubic Bézier curve to the receiver’s path.
NOTE: Use of the addQuadCurveToPoint:controlPoint: method gives worse results - tested.
I used this method and that's what happened as a result:
red line - rounded rectangle, blue line - rectangle from fours Bezier curves
If this result is interested - drawing code below.
NOTE: To achieve a more exact match Bezier curve can be required to change the coordinates of the four corner points (now they correspond to the angles of the rectangle in which is inscribed the figure).
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
//set rect size for draw
float rectSize = 275.;
CGRect rectangle = CGRectMake(CGRectGetMidX(rect) - rectSize/2, CGRectGetMidY(rect) - rectSize/2, rectSize, rectSize);
//Rounded rectangle
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
UIBezierPath* roundedPath = [UIBezierPath bezierPathWithRoundedRect:rectangle cornerRadius:rectSize/4.7];
[roundedPath stroke];
//Rectangle from Fours Bezier Curves
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
UIBezierPath *bezierCurvePath = [UIBezierPath bezierPath];
//set coner points
CGPoint topLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMinY(rectangle));
CGPoint topRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMinY(rectangle));
CGPoint botLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMaxY(rectangle));
CGPoint botRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMaxY(rectangle));
//set start-end points
CGPoint midRPoint = CGPointMake(CGRectGetMaxX(rectangle), CGRectGetMidY(rectangle));
CGPoint botMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMaxY(rectangle));
CGPoint topMPoint = CGPointMake(CGRectGetMidX(rectangle), CGRectGetMinY(rectangle));
CGPoint midLPoint = CGPointMake(CGRectGetMinX(rectangle), CGRectGetMidY(rectangle));
//Four Bezier Curve
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topLPoint controlPoint2:topLPoint];
[bezierCurvePath moveToPoint:midLPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botLPoint controlPoint2:botLPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:topMPoint controlPoint1:topRPoint controlPoint2:topRPoint];
[bezierCurvePath moveToPoint:midRPoint];
[bezierCurvePath addCurveToPoint:botMPoint controlPoint1:botRPoint controlPoint2:botRPoint];
[bezierCurvePath stroke];
CGContextRestoreGState(context);

A filled version of the accepted answer, also ported to Swift:
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.saveGState()
let rect = self.bounds
let rectSize: CGFloat = rect.width
let rectangle = CGRect(x: rect.midX - rectSize / 2, y: rect.midY - rectSize / 2, width: rectSize, height: rectSize)
let topLPoint = CGPoint(x: rectangle.minX, y: rectangle.minY)
let topRPoint = CGPoint(x: rectangle.maxX, y: rectangle.minY)
let botLPoint = CGPoint(x: rectangle.minX, y: rectangle.maxY)
let botRPoint = CGPoint(x: rectangle.maxX, y: rectangle.maxY)
let midRPoint = CGPoint(x: rectangle.maxX, y: rectangle.midY)
let botMPoint = CGPoint(x: rectangle.midX, y: rectangle.maxY)
let topMPoint = CGPoint(x: rectangle.midX, y: rectangle.minY)
let midLPoint = CGPoint(x: rectangle.minX, y: rectangle.midY)
let bezierCurvePath = UIBezierPath()
bezierCurvePath.move(to: midLPoint)
bezierCurvePath.addCurve(to: topMPoint, controlPoint1: topLPoint, controlPoint2: topLPoint)
bezierCurvePath.addCurve(to: midRPoint, controlPoint1: topRPoint, controlPoint2: topRPoint)
bezierCurvePath.addCurve(to: botMPoint, controlPoint1: botRPoint, controlPoint2: botRPoint)
bezierCurvePath.addCurve(to: midLPoint, controlPoint1: botLPoint, controlPoint2: botLPoint)
context.setFillColor(UIColor.lightGray.cgColor)
bezierCurvePath.fill()
context.restoreGState()
}
perfect for use in a UIView subclass.

Building on top of Ruslan's and Sunkas' answers above, I created a path that joins superelliptic "corners" with straight line segments; i.e. a superelliptic analog of the regular, rounded rectangle (like the mask seen around the edges of the iPhone X Simulator):
extension UIBezierPath {
static func superellipse(in rect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
// (Corner radius can't exceed half of the shorter side; correct if
// necessary:)
let minSide = min(rect.width, rect.height)
let radius = min(cornerRadius, minSide/2)
let topLeft = CGPoint(x: rect.minX, y: rect.minY)
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
// The two points of the segment along the top side (clockwise):
let p0 = CGPoint(x: rect.minX + radius, y: rect.minY)
let p1 = CGPoint(x: rect.maxX - radius, y: rect.minY)
// The two points of the segment along the right side (clockwise):
let p2 = CGPoint(x: rect.maxX, y: rect.minY + radius)
let p3 = CGPoint(x: rect.maxX, y: rect.maxY - radius)
// The two points of the segment along the bottom side (clockwise):
let p4 = CGPoint(x: rect.maxX - radius, y: rect.maxY)
let p5 = CGPoint(x: rect.minX + radius, y: rect.maxY)
// The two points of the segment along the left side (clockwise):
let p6 = CGPoint(x: rect.minX, y: rect.maxY - radius)
let p7 = CGPoint(x: rect.minX, y: rect.minY + radius)
let path = UIBezierPath()
path.move(to: p0)
path.addLine(to: p1)
path.addCurve(to: p2, controlPoint1: topRight, controlPoint2: topRight)
path.addLine(to: p3)
path.addCurve(to: p4, controlPoint1: bottomRight, controlPoint2: bottomRight)
path.addLine(to: p5)
path.addCurve(to: p6, controlPoint1: bottomLeft, controlPoint2: bottomLeft)
path.addLine(to: p7)
path.addCurve(to: p0, controlPoint1: topLeft, controlPoint2: topLeft)
return path
}
}
The points p0 through p7 in the code can be visualized in the following diagram:
If you pass a rectangle that is actually a square, and the corner radius is equal to or greater than half the side length, the straight line segments collapse (p0 "merges" with p1, p2 with p3, etc.) and you get the standard superellipse.

This isn't a great answer since it doesn't get to the heart of what you're asking which is how to draw a superellipse** programmatically, but you can:
Download the SVG for the iOS7 icon shape here: http://dribbble.com/shots/1127699-iOS-7-icon-shape-PSD
Import it to your Xcode project
Add PocketSVG to your project: https://github.com/arielelkin/PocketSVG
Load the SVG, and convert it to a UIBezierPath, from there you can scale and transform however you like:
PocketSVG *myVectorDrawing = [[PocketSVG alloc] initFromSVGFileNamed:#"iOS_7_icon_shape"];
UIBezierPath *myBezierPath = myVectorDrawing.bezier;
// Apply your transforms here:
[myBezierPath applyTransform:CGAffineTransformMakeScale(2.5, 2.5)];
[myBezierPath applyTransform:CGAffineTransformMakeTranslation(10, 50)];
CAShapeLayer *myShapeLayer = [CAShapeLayer layer];
myShapeLayer.path = myBezierPath.CGPath;
myShapeLayer.strokeColor = [[UIColor redColor] CGColor];
myShapeLayer.lineWidth = 2;
myShapeLayer.fillColor = [[UIColor clearColor] CGColor];
[self.view.layer addSublayer:myShapeLayer];
** It's maybe worth noting that the shape might not be an exact superellipse anyway: http://i.imgur.com/l0ljVRo.png

The other answers here don't look like the real thing. If you want something that actually matches the shape of an iOS icon then there's a detailed method here with a lot of very magic numbers. Kudos to PaintCode for working it out.

This would be pretty easy to do in OpenGL ES with a shader. Just draw a quad and pass in the x and y as vertex attributes. In the fragment shader, plug x and y into the equation. If the result <= 1, then the fragment is inside the shape. If I can get some free time I might try this and post it here.
If you want to use CGPath, I think the key is to parameterize x and y in terms of t, which goes from 0 to 2π. Then evaluate x and y at regular intervals. I'll try to figure this out in my free time, too, but my math is kind of rusty.
BTW, I'm certain that Apple is not using this formula. See the link that #millimoose posted: http://blog.mikeswanson.com/post/62341902567/unleashing-genetic-algorithms-on-the-ios-7-icon

Thank you Mark for the link and to PaintCode for figuring it out. It does indeed produce the same result as CALayerCornerCurve.continuous added in iOS 13. Here's Swift version of PaintCode's objc extension:
extension UIBezierPath {
/// Source: [PaintCode](https://www.paintcodeapp.com/news/code-for-ios-7-rounded-rectangles)
static func iOS7RoundedRect(in rect: CGRect, cornerRadius radius: CGFloat) -> UIBezierPath {
let limit: CGFloat = min(rect.size.width, rect.size.height) / 2 / 1.52866483
let limitedRadius: CGFloat = min(radius, limit)
func topLeft(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
CGPoint(x: rect.origin.x + x * limitedRadius, y: rect.origin.y + y * limitedRadius)
}
func topRight(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
CGPoint(x: rect.origin.x + rect.size.width - x * limitedRadius, y: rect.origin.y + y * limitedRadius)
}
func bottomRight(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
CGPoint(x: rect.origin.x + rect.size.width - x * limitedRadius, y: rect.origin.y + rect.size.height - y * limitedRadius)
}
func bottomLeft(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
CGPoint(x: rect.origin.x + x * limitedRadius, y: rect.origin.y + rect.size.height - y * limitedRadius)
}
let path = UIBezierPath()
path.move(to: topLeft(1.52866483, 0.00000000))
path.addLine(to: topRight(1.52866471, 0.00000000))
path.addCurve(to: topRight(0.66993427, 0.06549600),
controlPoint1: topRight(1.08849323, 0.00000000),
controlPoint2: topRight(0.86840689, 0.00000000))
path.addLine(to: topRight(0.63149399, 0.07491100))
path.addCurve(to: topRight(0.07491176, 0.63149399),
controlPoint1: topRight(0.37282392, 0.16905899),
controlPoint2: topRight(0.16906013, 0.37282401))
path.addCurve(to: topRight(0.00000000, 1.52866483),
controlPoint1: topRight(0.00000000, 0.86840701),
controlPoint2: topRight(0.00000000, 1.08849299))
path.addLine(to: bottomRight(0.00000000, 1.52866471))
path.addCurve(to: bottomRight(0.06549569, 0.66993493),
controlPoint1: bottomRight(0.00000000, 1.08849323),
controlPoint2: bottomRight(0.00000000, 0.86840689))
path.addLine(to: bottomRight(0.07491111, 0.63149399))
path.addCurve(to: bottomRight(0.63149399, 0.07491111),
controlPoint1: bottomRight(0.16905883, 0.37282392),
controlPoint2: bottomRight(0.37282392, 0.16905883))
path.addCurve(to: bottomRight(1.52866471, 0.00000000),
controlPoint1: bottomRight(0.86840689, 0.00000000),
controlPoint2: bottomRight(1.08849323, 0.00000000))
path.addLine(to: bottomLeft(1.52866483, 0.00000000))
path.addCurve(to: bottomLeft(0.66993397, 0.06549569),
controlPoint1: bottomLeft(1.08849299, 0.00000000),
controlPoint2: bottomLeft(0.86840701, 0.00000000))
path.addLine(to: bottomLeft(0.63149399, 0.07491111))
path.addCurve(to: bottomLeft(0.07491100, 0.63149399),
controlPoint1: bottomLeft(0.37282401, 0.16905883),
controlPoint2: bottomLeft(0.16906001, 0.37282392))
path.addCurve(to: bottomLeft(0.00000000, 1.52866471),
controlPoint1: bottomLeft(0.00000000, 0.86840689),
controlPoint2: bottomLeft(0.00000000, 1.08849323))
path.addLine(to: topLeft(0.00000000, 1.52866483))
path.addCurve(to: topLeft(0.06549600, 0.66993397),
controlPoint1: topLeft(0.00000000, 1.08849299),
controlPoint2: topLeft(0.00000000, 0.86840701))
path.addLine(to: topLeft(0.07491100, 0.63149399))
path.addCurve(to: topLeft(0.63149399, 0.07491100),
controlPoint1: topLeft(0.16906001, 0.37282401),
controlPoint2: topLeft(0.37282401, 0.16906001))
path.addCurve(to: topLeft(1.52866483, 0.00000000),
controlPoint1: topLeft(0.86840701, 0.00000000),
controlPoint2: topLeft(1.08849299, 0.00000000))
path.close()
return path
}
}

Related

Want to draw explanatory sign using UIBezierPath

I need to draw explanatory sign using UIBezierPath.
what i am trying to draw is
what i am getting using below code
private func linePath()-> CGPath {
let viewCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = bounds.midX/7.5
let miniRadius = bounds.midX/15
let path = UIBezierPath()
///path.move(to: viewCenter)
path.addArc(withCenter: viewCenter, radius: miniRadius, startAngle: 0, endAngle: 180.degreesToRadians, clockwise: true)
path.addQuadCurve(to: CGPoint(x: path.currentPoint.x - 5, y: path.currentPoint.y - 30), controlPoint: CGPoint(x: path.currentPoint.x - 10, y: path.currentPoint.y - 30))
// path.addLine(to: CGPoint(x: path.currentPoint.x - 5, y: path.currentPoint.y - 30))
path.addArc(withCenter: CGPoint(x: path.currentPoint.x + radius, y: path.currentPoint.y + radius/2), radius: radius, startAngle: 180, endAngle: 0.degreesToRadians, clockwise: true)
path.close()
return path.cgPath
}
Any help or pointers are appreciated.
When drawing smooth shapes with Bézier curves, we want to make sure that there are no points of discontinuity of the slope in the overall path where one curve abuts another. I.e., we want to ensure there aren’t any points where the slopes before and after a start/end point are not equal.
In short, make sure that the slope of the first control point for each Bézier curve matches the slope of the previous curve (and of course, that the second control point matches the slope of the following curve).
Consider, the following, stroking the three paths, one arc and two cubic Bézier (plus stroking the control points for the Bézier):
I have done this with three curves, and I'm showing the control points for the green and yellow curves with the dark lines. Going clockwise, note how the yellow curve’s first control point is collinear with the tangent of the blue arc. And the yellow’s second control point is collinear with the green curve’s first control point. And, of course, the green curve’s second control point is collinear with the starting tangent of the blue curve.
Getting rid of the distracting colors that yields:
FYI, this generated the above:
func updatePath() {
let verticalControlOffset = radius
let horizontalControllOffset = radius / 2
let height = radius * 5
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.minY + radius),
radius: radius,
startAngle: .pi,
endAngle: 2 * .pi,
clockwise: true)
var startPoint = path.currentPoint
var endPoint = CGPoint(x: bounds.midX, y: bounds.minY + height)
var cp1 = CGPoint(x: startPoint.x, y: startPoint.y + verticalControlOffset)
var cp2 = CGPoint(x: endPoint.x + horizontalControllOffset, y: endPoint.y)
path.addCurve(to: endPoint,
controlPoint1: cp1,
controlPoint2: cp2)
startPoint = path.currentPoint
endPoint = CGPoint(x: bounds.midX - radius, y: bounds.minY + radius)
cp1 = CGPoint(x: startPoint.x - horizontalControllOffset, y: startPoint.y)
cp2 = CGPoint(x: endPoint.x, y: endPoint.y + verticalControlOffset)
path.addCurve(to: endPoint,
controlPoint1: cp1,
controlPoint2: cp2)
shapeLayer.path = path.cgPath
}
Now, this is not the exact same as your target shape. But it illustrates the issue, namely when joining a series of Bézier curves together, make sure the control points for adjoining curves line with each other. This is easy when your control points are all vertical and horizontal, but if not, you will want to use a little algebra and/or trigonometry to make sure they line up properly. And sometimes, when rendering complicated shapes, it is helpful to actually stroke lines to the control points, so you can easily visualize what is going on.

How to draw smooth curve according to given height

I want to drive a smooth curve that curve's 1/3 is outward 2/3 is inward.This is what I want to achieve.
So far , I tried this with adding 1 curve like :
var height: CGFloat = UIScreen.main.bounds.height / 14
let centerWidth = self.frame.width / 2
let screenView = UIScreen.main.bounds
let width = (screenView.width - (2*screenView.width/40)) / 10
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - width * 2.5), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth , y: height),
controlPoint1: CGPoint(x: (centerWidth - width * 1.33), y: 0), controlPoint2: CGPoint(x: centerWidth - width * 1.33 , y: height))
But when I try this outward and inward curve's have same height. So I changed it and tried adding 2 curve like :
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - width * 2.5), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth - width * 1.33 , y: height/3),
controlPoint1: CGPoint(x: (centerWidth - width * 1.33), y: 0), controlPoint2: CGPoint(x: centerWidth - width * 1.33 , y: height/3))
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: centerWidth - width * 1.33 , y: height/3), controlPoint2: CGPoint(x: centerWidth - width , y: height ))
It similar to what I want but junction point doesnt look the curve is a single piece
I have tried so many things but cant draw what I want exacly.So How can I draw a curve that endpoint height's 1/3 gonna be outward and 2/3 inward that looks like a one piece.
I'm open to all kinds of ideas to draw.Regards
NOTE
I'm trying to customize tabbar's center button which height value is UIScreen.main.bounds.height and width is `
let screenView = UIScreen.main.bounds
let width = (screenView.width - (2*screenView.width/40))`
As I look at your designer’s rendering, if you are breaking this into two separate Bézier curves, the key observation is that the top ⅓ curve is symmetrical (with respect to itself) as is the bottom ⅔ curve. And for symmetrical Bézier curves, a quad Bézier is easier to deal with.
And to make the inflection point seamless, you’ll want to ensure that two control points (one for the quad Bézier above the inflection point, the other for the quad Bézier below the inflection point) are collinear with the inflection point itself. E.g.:
It takes a little trigonometry to figure out the placement of these control points, e.g.
let center = CGPoint(x: view.bounds.midX, y: 200)
let radius: CGFloat = 140
let curveOffsetBottom: CGFloat = 30
let curveOffsetInflection: CGFloat = 50
greenCircularShapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
let h = radius + curveOffsetBottom + 50
let curveBottom = CGPoint(x: center.x,
y: center.y + radius + curveOffsetBottom)
let y0 = radius + curveOffsetBottom - h * 2 / 3
let t0 = asin(y0 / (radius + curveOffsetInflection))
let x0 = y0 / tan(t0)
let inflectionPoint = CGPoint(x: center.x - x0, y: center.y + y0)
let t1 = atan((curveBottom.x - inflectionPoint.x) / (curveBottom.y - inflectionPoint.y))
let t2 = 2 * (.pi / 2 - t1)
let x2 = (curveBottom.y - inflectionPoint.y) / tan(t2)
let x1 = x2 / 2
let cp1 = CGPoint(x: inflectionPoint.x - x1, y: curveBottom.y - h)
let cp2 = CGPoint(x: inflectionPoint.x + x2, y: curveBottom.y)
let curveTop = CGPoint(x: cp1.x - cp1.distance(to: inflectionPoint), y: curveBottom.y - h)
let path = UIBezierPath()
path.move(to: curveTop)
path.addQuadCurve(to: inflectionPoint, controlPoint: cp1)
path.addQuadCurve(to: curveBottom, controlPoint: cp2)
path.addLine(to: CGPoint(x: view.bounds.maxX, y: path.currentPoint.y))
path.addLine(to: CGPoint(x: view.bounds.maxX, y: view.bounds.maxY))
path.addLine(to: CGPoint(x: view.bounds.minX, y: view.bounds.maxY))
path.addLine(to: CGPoint(x: view.bounds.minX, y: curveTop.y))
path.close()
blackCurveShapeLayer.path = path.cgPath
Where
extension CGPoint {
func distance(to point: CGPoint) -> CGFloat {
hypot(point.x - x, point.y - y)
}
}

CAShapeLayer animation issue

I have a CAShapeLayer (below) which is used to draw a bubble. Which all works fine, as per designs but when i start resizing it within a tableView with tableView.beginUpdates() and tableView.endUpdates(). I end up having the old layer in black momentarily. which is looks awful. I am really puzzled on how to solve this. It kinda feels that layers are black, and the fill paints over them like a mask.
I have tried several things as to add/removing the layer in different places. etc. but result is always this.
video to describe it
override func draw(_ rect: CGRect) {
self.layer.backgroundColor = UIColor.clear.cgColor
let fillColor: UIColor = self.shapeColor
let shapeLayer = CAShapeLayer()
shapeLayer.contentsScale = UIScreen.main.scale
let path = UIBezierPath()
shapeLayer.fillColor = fillColor.cgColor
let width = self.bounds.size.width
let height = self.bounds.size.height
path.move(to: CGPoint(x: 3.02, y: height * 0.5))
path.move(to: CGPoint(x: 3.02, y: 24.9))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addCurve(to: CGPoint(x: 17.64, y: -0), controlPoint1: CGPoint(x: 3.02, y: 6.55),controlPoint2: CGPoint(x: 9.57, y: -0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addCurve(to: CGPoint(x: width, y: 14.62), controlPoint1: CGPoint(x: width - 7.77, y: 0), controlPoint2: CGPoint(x: width, y: 6.55))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addCurve(to: CGPoint(x: width - 15.85, y: height), controlPoint1: CGPoint(x: width, y: height - 7.03), controlPoint2: CGPoint(x: width - 7.77, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addCurve(to: CGPoint(x: 8.69, y: height - 3.56), controlPoint1: CGPoint(x: 14.39, y: height),controlPoint2: CGPoint(x: 11.24, y: height - 1.57))
path.addLine(to: CGPoint(x: 8.79, y: height - 3.46))
path.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 7.27, y: height - 1.27), controlPoint2: CGPoint(x: 3.84, y: height + 0.51))
path.addCurve(to: CGPoint(x: 3.02, y: height - 14.1), controlPoint1: CGPoint(x: 4.06, y: height - 4.22), controlPoint2: CGPoint(x: 3.02, y: height - 9.62))
path.close()
if self.reverted {
let mirrorOverXOrigin: CGAffineTransform = CGAffineTransform(scaleX: -1.0, y: 1.0);
let translate: CGAffineTransform = CGAffineTransform(translationX: width, y: 0);
// Apply these transforms to the path
path.apply(mirrorOverXOrigin)
path.apply(translate)
}
path.fill()
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
if let bubbleLayer = self.bubbleLayer {
self.layer.replaceSublayer(bubbleLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.bubbleLayer = shapeLayer
}
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
self.layer.insertSublayer(shapeLayer, at: 0)
self.bubbleLayer?.removeFromSuperlayer()
self.bubbleLayer = shapeLayer
}
your solution cannot work. Draw(_ :) is not called during animation block. It is draw after tableView.endUpdates(), after UITableView ends animation.
Draw(_ :) - I don't think you should use it for your needs at all.
If I can recommend you some solution:
1) In awakeFromNib:
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.backgroundColor = self.shapeColor
self.contentView.layer.cornerRadius = 15
/** make left view and attach it to the left bottom anchors with fixed size and draw into this view the shape of your left arrow***/
self.leftArrowView.isHidden = self.isReverted
/** make right view and attach it to the right bottom anchors with fixed size and draw into this view the shape of your right arrow***/
self.rightArrowView.isHidden = !self.isReverted
}
2) In prepareForReuse:
override func prepareForReuse() {
super.prepareForReuse()
self.leftArrowView.isHidden = self.isReverted
self.rightArrowView.isHidden = !self.isReverted
}
And this should cause your cell will change size smoothly.

UIBezierPath with multiple paths not joining lines with bevel type

Probably a simple bug, but I can't figure out what's going on.
I have 3 UIBezierPaths that I am merging into 1. I want either end of the path to have a rounded edge, with the middle path to have a bevel lineJoinStyle. However, the last path seems to have a rounded lineJoinStyle which I can't seem to fix.
Here is an image of what I mean.
Here is the code I'm using:
override func draw(_ rect: CGRect) {
let leftQuarterPoint: CGPoint = CGPoint(x: 15, y: rect.height / 2)
let rightQuarterPoint: CGPoint = CGPoint(x: (rect.width - (rect.width / 4)), y: rect.height / 2)
let leftPath = UIBezierPath()
leftPath.move(to: leftQuarterPoint)
let firstPoint = CGPoint(x: rect.width / 4, y: rightQuarterPoint.y)
Global.Colors.red.setStroke()
leftPath.addCurve(to: firstPoint, controlPoint1: CGPoint(x: leftQuarterPoint.x, y: rightQuarterPoint.y), controlPoint2: CGPoint(x: firstPoint.x, y: firstPoint.y))
leftPath.lineWidth = 20
leftPath.lineCapStyle = .round
leftPath.stroke()
let middlePath = UIBezierPath()
Global.Colors.orange.setStroke()
middlePath.move(to: leftPath.currentPoint)
middlePath.addCurve(to: rightQuarterPoint, controlPoint1: CGPoint(x: rect.width / 2, y: rect.height / 2), controlPoint2: CGPoint(x: rect.width / 2, y: rect.height / 2))
middlePath.lineWidth = 20
middlePath.lineCapStyle = .square
middlePath.lineJoinStyle = .bevel
middlePath.stroke()
let rightPath = UIBezierPath()
Global.Colors.green.setStroke()
rightPath.move(to: middlePath.currentPoint)
rightPath.addCurve(to: CGPoint(x: rect.width - 15, y: (rect.height / 2)), controlPoint1: CGPoint(x: rightQuarterPoint.x, y: rightQuarterPoint.y), controlPoint2: CGPoint(x: rect.width-15, y: rect.height / 2))
rightPath.lineWidth = 20
rightPath.lineCapStyle = .round
rightPath.lineJoinStyle = .bevel
rightPath.stroke()
let path = UIBezierPath()
path.append(leftPath)
path.append(middlePath)
path.append(rightPath)
path.lineWidth = 20
}
Sorry if I'm missing something simple.
Removing these lines from the code and adding them to the end of the draw func will work for you, the reason is side paths are rounded but they have overlap with the middle one so if you drow sides first and then draw the middle one it will hide the round corners.
override func draw(_ rect: CGRect) {
...
Global.Colors.green.setStroke()
rightPath.stroke()
Global.Colors.red.setStroke()
leftPath.stroke()
Global.Colors.orange.setStroke()
middlePath.stroke()
}
Your right path is overlapping the middle one. just start drawing of the right path after the middle one is over.
Replace rightPath.move with this line.
15 will be the value that you want to start drawing the right path.
rightPath.move(to: CGPoint(x: middlePath.currentPoint.x + 15, y: middlePath.currentPoint.y))

Draw a diagonal line with side curves UIBezierPath

I want to draw diagonal line with side curves like the following
Code:
func drawCanvas1(frame: CGRect = CGRect(x: 0, y: 0, width: 240, height: 120)) {
//// Bezier Drawing
let bezierPath = UIBezierPath()
UIColor.black.setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
//// Bezier 2 Drawing
let bezier2Path = UIBezierPath()
bezier2Path.move(to: CGPoint(x: frame.minX + 0.5, y: frame.minY + 103.42))
bezier2Path.addCurve(to: CGPoint(x: frame.minX + 12.17, y: frame.minY + 119.5), controlPoint1: CGPoint(x: frame.minX + 0.5, y: frame.minY + 103.42), controlPoint2: CGPoint(x: frame.minX + 0.5, y: frame.minY + 119.5))
bezier2Path.addCurve(to: CGPoint(x: frame.minX + 232.03, y: frame.minY + 26.23), controlPoint1: CGPoint(x: frame.minX + 23.85, y: frame.minY + 119.5), controlPoint2: CGPoint(x: frame.minX + 232.03, y: frame.minY + 26.23))
bezier2Path.addCurve(to: CGPoint(x: frame.minX + 237.87, y: frame.minY + 0.5), controlPoint1: CGPoint(x: frame.minX + 232.03, y: frame.minY + 26.23), controlPoint2: CGPoint(x: frame.minX + 243.7, y: frame.minY + 16.58))
UIColor.black.setStroke()
bezier2Path.lineWidth = 1
bezier2Path.lineCapStyle = .square
bezier2Path.stroke()
let layer1 = CAShapeLayer()
layer1.path = bezierPath.cgPath
let layer2 = CAShapeLayer()
layer2.path = bezier2Path.cgPath
imageCollectionView.layer.addSublayer(layer1)
imageCollectionView.layer.addSublayer(layer2)
}
output:
As you can see in output it is filling with black color whereas I've only given stroke. I'm not really good at bezier path. can anyone help me in getting the desired effect
Maybe using addArc is easier?
func drawCanvas1(frame: CGRect = CGRect(x: 0, y: 0, width: 240, height: 120)) {
let rhsY:CGFloat = frame.minY + 60 // Please adjust this value depending on your need
let cornerRadius:CGFloat = 25
let angle = atan2(frame.maxY - rhsY, frame.width)
// upper left corner
let p1 = CGPoint(x: frame.minX, y: frame.minY)
// left point touching bottom left corner
let p2 = CGPoint(x: frame.minX, y: frame.maxY - cornerRadius)
let a = cornerRadius * cos(angle)
let o = cornerRadius * sin(angle)
// Center of bottom left round corner
let c1 = CGPoint(x: p2.x + a, y: p2.y - o)
let c1Start = CGFloat.pi - angle
let c1End = c1Start - (CGFloat.pi / 2)
// Center of bottom right round corner
let c2 = CGPoint(x: frame.maxX - cornerRadius, y: rhsY)
// bottom point touching bottom left corner
// let p3 = CGPoint(x: c1.x + o, y: c1.y + a)
// bottom point touching bottom right corner
let p4 = CGPoint(x: c2.x + o, y: c2.y + a)
// right point touching bottom right corner
// let p5 = CGPoint(x: frame.maxX, y: rhsY)
// upper right corner
let p6 = CGPoint(x: frame.maxX, y: frame.minY)
let path = UIBezierPath()
path.move(to: p1)
path.addLine(to: p2)
path.addArc(withCenter: c1, radius: cornerRadius, startAngle: c1Start, endAngle: c1End, clockwise: false)
path.addLine(to: p4)
path.addArc(withCenter: c2, radius: cornerRadius, startAngle: c1End, endAngle: 0, clockwise: false)
path.addLine(to: p6)
path.addLine(to: p1)
// I am guessing you are trying to use a mask to the top level image?
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
imageCollectionView.layer.mask = maskLayer
}
Unless you provide your fill color, black is used by default to fill the path. If the path is not closed, fill color tries to fill using start and end points.

Resources