iOS How to achieve my drawing using a bezier path? - ios

I need to have a border and corner radius on the section of my collectionView. So if it's the first row I need to draw a bottomless rect layer. And if it's the last row I will draw a topless layer.
How to achieve my drawing using a bezier path?
Edited
I have used this code, it works. But I could not create a method for topless rect.
extension CGMutablePath {
static func bottomlessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
let path = CGMutablePath()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.maxY), radius: radius)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
}
let layer = CAShapeLayer()
layer.lineWidth = 1
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = nil
layer.path = CGMutablePath.bottomlessRoundedRect(in: testView.bounds.insetBy(dx: 1, dy: 1), radius: 18)
view.layer.insertSublayer(layer, at: 0)
view.layoutIfNeeded()
Here is method for topless rect, it is not working correctly.
static func toplessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
let path = CGMutablePath()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.minX, y: rect.maxY), radius: radius)
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}
This is the result.
Please help me to have a correct topless rect. (I have not used drawing before)
The answer
static func toplessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
let path = CGMutablePath()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX, y: rect.maxY), radius: radius)
path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}

Diagram it out. Rounded rectangles are a series of line segments connected to quarter-circle arcs of a given radius. (the corner radius.)
iOS uses different coordinate systems depending on how you're doing your drawing (Core Graphics uses LLO, or lower left orgin, and UIKit/Core Animation uses ULO, or upper left origin.)
It looks like the CGPaths used in CAShapeLayers use ULO (upper-left-origin) coordinates, where 0,0 is in the upper left corner and Y increases as you go down.
Your bottomlessRoundedRect() function starts with this line of code:
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
That moves to the lower left (max Y) position of the rectangle to start.
Annotating the whole function, here's what it does:
static func bottomlessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
let path = CGMutablePath()
//Move to the lower left corner of the rect (starting point)
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
//Draw a line from the starting point to the beginning of the arc in the
//top left corner, and draw the top left rounded corner
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY),
tangent2End: CGPoint(x: rect.maxX, y: rect.minY),
radius: radius)
//Draw a line from the top left corner to the begnning of the top right
//arc, and the top right corner arc
path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY),
tangent2End: CGPoint(x: rect.maxX, y: rect.maxY),
radius: radius)
//Draw a final line from the end of the top right corner arc to
//the bottom right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
Draw your full rectangle. Pick your corner radius. Draw an inner rectangle who's corners are inset by the corner radius. Draw circles at the corners of your inner rectangle, and note how they meet the sides of your outer rectangles.
Now put it together.
Open a bezier path. Move to your desired starting point. Create a line to the next side, minus your corner radius. Draw a 1/4 circle arc starting and ending at the angles needed to complete that corner. Draw the next line segment. Draw another arc. Draw your final line segment.
An annotated version of a toplessRoundedRect() function looks like this:
static func toplessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
let path = CGMutablePath()
//Move to the top left corner.
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
//Draw a line from the top left corner to the begnning of the bottom left
//rounded corner, plus the bottom left rounded corner
path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY),
tangent2End: CGPoint(x: rect.maxX, y: rect.maxY),
radius: radius)
//Draw a line from the end of the bottom left rounded corner to the beginning
//of the bottom right rounded corner, plus the bottom right rounded corner
path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY),
tangent2End: CGPoint(x: rect.maxX, y: rect.minY),
radius: radius)
//Draw a line from the end of the bottom right rounded corner
//to the top right corner.
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}

Here's a tip: work through your code one line at a time.
Change your CGMutablePath extension to this:
extension CGMutablePath {
static func bottomlessRoundedRect(in rect: CGRect, radius: CGFloat) -> CGMutablePath {
let path = CGMutablePath()
// 1
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
// 2
//path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
// 3
//path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.maxY), radius: radius)
// 4
//path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
return path
}
}
When you run your app, only the 1 part of the path will be executed. You won't see anything, because all you've done so far is move to a point.
Now, uncomment the 2 part. Run your code, and see what the result is.
Uncomment the 3 part. Run your code, and see what the result is.
Uncomment the 4 part. Run your code, and see what the result is.
By now, you should have a pretty good idea of what the code is doing, and creating your toplessRoundedRect func should be a piece of cake.

Related

SwiftUI: Custom shape material fill does not work

I made a custom shape in SwiftUI with the following code:
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addQuadCurve(to: CGPoint(x: 0, y: rect.minY), control: CGPoint(x: rect.midX, y: rect.minY - 25))
return path
}
}
The usage of this shape looks like the following:
CustomShape()
.fill(.ultraThickMaterial)
.frame(height: 200)
The problem is that if I try to fill it with a color it works perfectly. If I try to fill it with a material only the rectangle seems to get filled, the arc portion remains white:
Do you have an idea how to solve this?
It seems that clipping is being applied when you are using a material, but not when you are using a color. The arc of your path is extending outside the frame of the view.
You can fix this by making the path fully contained within the rect parameter of the shape:
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + 25))
path.addQuadCurve(to: CGPoint(x: 0, y: rect.minY + 25), control: CGPoint(x: rect.midX, y: rect.minY))
path.closeSubpath()
return path
}
If you really want the material to extend outside the frame then you'll need to use a container view of some sort.
I'd say that expected effect should be achieved with background and clipShape (assuming that CustomShape is constructed correctly)
Here is a demo for better visibility (tested with Xcode 13.3 / iOS 15.4)
Image("background").overlay(
Rectangle()
.background(.thinMaterial) // << here !! (thin for demo)
.frame(height: 200)
.clipShape(CustomShape()) // << here !!
)

How to create Bézier curve using Path in SwiftUI

I want to achieve a path like this in the picture using Path in SwiftUI, but my understanding of the Bezier curve is minimal, and I cannot reproduce the drawing on the image.
I am talking about the background drawing, which is hard to notice; it's like a mountain or staircase.
Any help will be appreciated!
Image to reproduce:
Here is my code:
struct BezierCurveView: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint.zero)
path.addCurve(to: CGPoint(x: rect.maxX / 3, y: rect.minY), control1: CGPoint(x: rect.minX + 40, y: rect.minY - 90), control2: CGPoint(x: rect.minX + 80, y: rect.minY - 90))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
return path
}
}
Here is the result of my code:

Add two paths together using UIBezierPath in Swift

I have 2 lines drawn using UIBezierPath, how can i combine point to form a shape inform of letter X. I want to join both lines together
Path 1
let path1 = UIBezierPath()
path1.move(to: .zero)
path1.addLine(to: CGPoint(x: 100, y: 100))
path1.close()
path1.lineWidth = 1.0
UIColor.blue.set()
path1.stroke()
path1.fill()
Path 2
let path2 = UIBezierPath()
path2.move(to: .zero)
path2.addLine(to: CGPoint(x: 50, y: 50))
path2.close()
path2.lineWidth = 1.0
UIColor.red.set()
path2.stroke()
path2.fill()
You should use proper CGPoint values
If your view bounds value is (0,0,100,100)
Move to (0,0)
Addline to (100,100)
Mode to (0,100)
Addline to (100,0)
Try this
let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.move(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.close()
path.lineWidth = 1.0
UIColor.blue.set()
path.stroke()
path.fill()

Draw a round edge parallelogram with some shadow offset

I am trying to draw a parallelogram with round edges. I want it to be configurable so that I can one of the vertical edge at 90 degree.
As you can see in the image that topLeft corner is not rounded with code below
func getParallelogram(width: CGFloat, height: CGFloat, radius: CGFloat) -> CGPath {
// Points of the parallelogram
var points = [
CGPoint(x: width * 0.05, y: 0),
CGPoint(x: width , y: 0),
CGPoint(x: width - width * 0.05, y: height),
CGPoint(x: 0, y: height)
]
let point1 = points[0]
let point2 = points[1]
let point3 = points[2]
let point4 = points[3]
let path = CGMutablePath()
path.move(to: point1)
path.addArc(tangent1End: point1, tangent2End: point2, radius: radius)
path.addArc(tangent1End: point2, tangent2End: point3, radius: radius)
path.addArc(tangent1End: point3, tangent2End: point4, radius: radius)
path.addArc(tangent1End: point4, tangent2End: point1, radius: radius)
return path
}
Usage:
let customView = UIView(frame: CGRect(x: 20, y: 50, width: 320, height: 300))
customView.backgroundColor = UIColor.clear
view.addSubview(customView)
let shape = CAShapeLayer()
shape = UIColor.lightGray.cgColor
shape.path = getParallelogram(width: 200, height: 80, radius: 5)
shape.position = CGPoint(x: 10, y: 10)
shape.layer.addSublayer(triangle)
The problem i am facing here is that the top left corner is not round when with above code. I would appreciate any help to achieve this. Also please guide me if there is any other alternative or simpler approach. Thanks!
Note: I am using this code to covert Triangle to Parallelogram UIBezierPath Triangle with rounded edges
Change
path.move(to: point1)
path.addArc(tangent1End: point1, tangent2End: point2, radius: radius)
path.addArc(tangent1End: point2, tangent2End: point3, radius: radius)
path.addArc(tangent1End: point3, tangent2End: point4, radius: radius)
path.addArc(tangent1End: point4, tangent2End: point1, radius: radius)
to
path.move(to: point1)
path.addArc(tangent1End: point2, tangent2End: point3, radius: radius)
path.addArc(tangent1End: point3, tangent2End: point4, radius: radius)
path.addArc(tangent1End: point4, tangent2End: point1, radius: radius)
path.addArc(tangent1End: point1, tangent2End: point2, radius: radius)
Result:

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