SwiftUI: Custom shape material fill does not work - ios

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

Related

Curved bottom bar

I want to make curved bottom bar it is not looking perfect round by below code
struct customShape: Shape {
var xAxis : CGFloat
var animatableData: CGFloat{
get { return xAxis}
set { xAxis = newValue}
}
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
let center = xAxis
path.move(to: CGPoint(x: center - 43, y: 0))
let to1 = CGPoint(x: center, y: 50)
let center1 = CGPoint(x: center - 30, y: 0)
let center2 = CGPoint(x: center - 46, y: 42)
let to2 = CGPoint(x: center + 52, y: 0)
let center3 = CGPoint(x: center + 57, y: 47)
let center4 = CGPoint(x: center + 35, y: 0)
path.addCurve(to: to1, control1: center1, control2: center2)
path.addCurve(to: to2, control1: center3, control2: center4)
}
}
}
You are trying to construct a circle with Bezier handles. And it seems you are not calculating the values but guessing and trying. This will be at least very hard if not impossible to solve. Try to add an arc instead of a curve.
Possible implementation:
struct customShape: Shape {
var xAxis : CGFloat
var radius: CGFloat = 40
var animatableData: CGFloat{
get { return xAxis}
set { xAxis = newValue}
}
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: 0, y: 0))
// add a line to the starting point of the circle
path.addLine(to: CGPoint(x: xAxis - radius, y: 0))
// add the arc with the centerpoint of (xAxis,0) and 180°
path.addArc(center: .init(x: xAxis, y: 0), radius: radius, startAngle: .init(degrees: 180), endAngle: .init(degrees: 0), clockwise: true)
// complete the rectangle
path.addLine(to: .init(x: rect.size.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.closeSubpath()
}
}
}

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:

Split a Rectangle View Using the Diagonal in Swift UI

I'm new to SwiftUI and I'm trying to achieve something that is quite easy using UIKit. Basically, I want to draw something like this in a View:
I figured that I could use GeometryScanner but I didn't manage to make it work. Any help would be appreciated.
You can create your own Triangle:
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(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))
return path
}
}
and use it like this:
Rectangle()
.fill(Color.white)
.frame(width: 300, height: 200)
.overlay(
Triangle()
.fill(Color.red)
)

iOS How to achieve my drawing using a bezier path?

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.

Clipping the bezier path in curve shape

I using Swift to creating a custom button in a phone UI and need to clip the bottom of the button in the convex like shape.
The problem was it should be perfect and the bottom curve should match the bigger button
I tried different approaches but it's not getting the perfect result.
Using rounded corners
if buttonDirection == 0 {
path = UIBezierPath(roundedRect: rect, byRoundingCorners:[UIRectCorner.topLeft, .topRight], cornerRadii: CGSize(width: self.bounds.width/2, height: 0))
path.close()
}
Create whole path without rounded curve using QuardCurve // still bottom part not inverse curve plus corner are also not smooth as 1
if buttonDirection == 0 {
//path = UIBezierPath(roundedRect: rect, byRoundingCorners:[UIRectCorner.topLeft, .topRight], cornerRadii: CGSize(width: self.bounds.width/2, height: 0))
//path.close()
let curve = UIBezierPath()
curve.move(to: CGPoint(x: 0, y: self.bounds.height))
curve.addLine(to:CGPoint(x: 0, y: self.bounds.width/3))
curve.addQuadCurve(to: CGPoint(x: self.bounds.width, y: self.bounds.width/3), controlPoint: CGPoint(x: self.bounds.width/2, y: -5))
curve.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
curve.addQuadCurve(to: CGPoint(x: 0, y: self.bounds.height), controlPoint: CGPoint(x: self.bounds.width/2, y: self.bounds.width - 10))
UIColor.black.setFill()
curve.fill()
}
Create the path from 1 and then just Clip QuardCurve
if buttonDirection == 0 {
path = UIBezierPath(roundedRect: rect, byRoundingCorners:[UIRectCorner.topLeft, .topRight], cornerRadii: CGSize(width: self.bounds.width/2, height: 0))
path.close()
path.move(to: CGPoint(x: 0, y: self.bounds.height))
path.addQuadCurve(to: CGPoint(x: self.bounds.width, y: self.bounds.height), controlPoint: CGPoint(x: self.bounds.width/2, y: self.bounds.height/2))
path.addClip()
UIColor.black.setFill()
path.fill()
}
I tried many other things also but not getting the result, What I wanted is simply clip the bottom curve from the 1st path, Like how to cut one path from another (After reading apples doc I come to know I can use path.reverse() also )
here are all buttons in one pic and when you click the button say vol + its changes the colour
PS Edited the original question, as images were confusing, and I found one solution but still want to know if there is a better solution with one path and smooth edges.
Below method worked but if someone has better solution do post.
Basically what I am doing here is first clipping the whole button with the bottom part as convex, using addLines and Quardcurve in the bottom and after that creating new path using bezier roundedRect initialization with top-right and top-left corner.
if buttonDirection == 0 {
let curve = UIBezierPath()
curve.move(to: CGPoint(x: 0, y: self.bounds.height))
curve.addLine(to: CGPoint(x: 0, y: 0))
curve.addLine(to: CGPoint(x: self.bounds.width, y: 0))
curve.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
curve.addQuadCurve(to: CGPoint(x: 0, y: self.bounds.height), controlPoint: CGPoint(x: self.bounds.width/2, y: self.bounds.height - self.bounds.height/4))
curve.close()
curve.addClip()
path = UIBezierPath(roundedRect: rect, byRoundingCorners:[UIRectCorner.topLeft, .topRight], cornerRadii: CGSize(width: self.bounds.width/2, height: 0))
path.close()
}

Resources