How to draw smooth curve according to given height - ios

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

Related

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

Waved Curved Tabbar Design in swift 4.2 İOS

Wave curved (upside) tabbar behind the center raised button. İ have created a customised class for tabbar by using reference of "https://medium.com/#philipp307/draw-a-custom-ios-tabbar-shape-27d298a7f4fa" But actually İ need to create a Wave Curved in the center of tab bar. İ have tried to play around with Bézier curve but not getting an exact behind the central button. I don't need a dip of it. İ need it behind (upside wave) my custom code and images attached.
func createPath() -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
tabbar design

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.

Some visual bug with CALayer

I have a UITableView with the following cells layout:
• UIView
•• UIImageView
••• Semi-transparent overlay UIView as subview of UIImageView
There are also icon view imitating the "play" button and UILabel, but they seem to be not related to this bug.
And I have the following code:
override func layoutSubviews() {
super.layoutSubviews()
createPath()
}
private func createPath() {
let padding = CGFloat(16)
let bottomLineLength = CGFloat(64)
let arcPadding = CGFloat(8)
let width = self.contentView.frame.width
let height = self.contentView.frame.height
if pathLayer != nil {
pathLayer?.removeFromSuperlayer()
}
pathLayer = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: padding + arcPadding, y: padding))
path.addLine(to: CGPoint(x: width - padding - arcPadding, y: padding))
// Top-Right arc
path.addArc(withCenter: CGPoint(x: width - padding - arcPadding, y: padding + arcPadding),
radius: arcPadding,
startAngle: CGFloat(3 * M_PI / 2),
endAngle: CGFloat(M_PI * 2),
clockwise: true)
path.move(to: CGPoint(x: width - padding, y: padding + arcPadding))
path.addLine(to: CGPoint(x: width - padding, y: height - padding - arcPadding))
// Bottom-Right arc
path.addArc(withCenter: CGPoint(x: width - padding - arcPadding, y: height - padding - arcPadding),
radius: arcPadding,
startAngle: CGFloat(M_PI * 2),
endAngle: CGFloat(M_PI / 2),
clockwise: true)
path.move(to: CGPoint(x: width - padding - arcPadding, y: height - padding))
path.addLine(to: CGPoint(x: width - padding - bottomLineLength, y: height - padding))
path.move(to: CGPoint(x: padding + bottomLineLength, y: height - padding))
path.addLine(to: CGPoint(x: padding + arcPadding, y: height - padding))
// Bottom-Left arc
path.addArc(withCenter: CGPoint(x: padding + arcPadding, y: height - padding - arcPadding),
radius: arcPadding,
startAngle: CGFloat(M_PI / 2),
endAngle: CGFloat(M_PI),
clockwise: true)
path.move(to: CGPoint(x: padding, y: height - padding - arcPadding))
path.addLine(to: CGPoint(x: padding, y: padding + arcPadding))
// Top-Left arc
path.addArc(withCenter: CGPoint(x: padding + arcPadding, y: padding + arcPadding),
radius: arcPadding,
startAngle: CGFloat(M_PI),
endAngle: CGFloat(3 * M_PI / 2),
clockwise: true)
pathLayer?.rasterizationScale = 2 * UIScreen.main.scale
pathLayer?.shouldRasterize = true
pathLayer?.strokeColor = UIColor(red: 0xFF/255, green: 0xFF/255, blue: 0xFF/255, alpha: 0.5).cgColor
pathLayer?.lineWidth = 2.0
pathLayer?.path = path.cgPath
contentView.layer.addSublayer(pathLayer!)
}
I'm calling it in layoutSubviews for re-creating the path on device rotation.
And if you look at the screenshot, there are some black and rotated a bit black "holes" near the path. All of them look the same, and they are surely not present on the image in UIImageView.
I think it's related to my CALayer. How to fix it?
Your problem is these black wedges:
The underlying cause is that the default fillColor of a CAShapeLayer is opaque black. Set it to nil to avoid filling:
pathLayer?.fillColor = nil
That done, let me show you a shorter way to construct your path:
let cgPath = CGMutablePath()
let start = CGPoint(x: padding + bottomLineLength, y: height - padding)
let blCorner = CGPoint(x: padding, y: height - padding)
let tlCorner = CGPoint(x: padding, y: padding)
let trCorner = CGPoint(x: width - padding, y: padding)
let brCorner = CGPoint(x: width - padding, y: height - padding)
let end = CGPoint(x: width - padding - bottomLineLength, y: height - padding)
cgPath.move(to: start)
cgPath.addArc(tangent1End: blCorner, tangent2End: tlCorner, radius: arcPadding)
cgPath.addArc(tangent1End: tlCorner, tangent2End: trCorner, radius: arcPadding)
cgPath.addArc(tangent1End: trCorner, tangent2End: brCorner, radius: arcPadding)
cgPath.addArc(tangent1End: brCorner, tangent2End: end, radius: arcPadding)
cgPath.addLine(to: end)
shapeLayer.path = cgPath
Result:

Draw iOS 7-style squircle programmatically

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
}
}

Resources