Wrong shadowPath for some cornerRadius - swift

Found that in some edge cases the shadowPath seems to render the shadow too rounded.
I'm applying the same shadow settings to all the subviews, I expected that the UIBezierPath with the same corner radius as the view, would behave the same. But seems that when the height starts to decrease they behave different.
Any idea why or how to fix it?

Try something like that
var cornerRadius: CGFloat = 32.0
let layerHalfHeight = subView.bounds.height / 2.0
if layerHalfHeight < cornerRadius {
cornerRadius = layerHalfHeight
}
subView.layer.cornerRadius = cornerRadius
let path = UIBezierPath()
path.move(to: CGPoint(x: cornerRadius, y: 0.0))
path.addLine(to: CGPoint(x: subView.bounds.width - cornerRadius, y: 0.0))
path.addArc(withCenter: CGPoint(x: subView.bounds.width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: 3.0 * .pi / 2.0, endAngle: 2 * .pi, clockwise: true)
path.addLine(to: CGPoint(x: subView.bounds.width, y: subView.bounds.height - cornerRadius))
path.addArc(withCenter: CGPoint(x: subView.bounds.width - cornerRadius, y: subView.bounds.height - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: true)
path.addLine(to: CGPoint(x: cornerRadius, y: subView.bounds.height))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: subView.bounds.height - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: true)
path.addLine(to: CGPoint(x: 0.0, y: cornerRadius))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: 3.0 * .pi / 2.0, clockwise: true)
path.close()
subView.layer.shadowPath = path.cgPath
So main idea is when the height of a layer become less then default corner radius * 2 then you need to change corner radius to half of the height.

For now, I added a corrector for the UIBezierPath cornerRadius, it kind of works but feels like is not the proper solution.
var cornerRadiusFix: CGFloat = cornerRadius
if cornerRadiusFix > subView.bounds.height / 3 {
cornerRadiusFix = cornerRadiusFix * 0.8
}
subView.layer.shadowPath = UIBezierPath(roundedRect: subView.bounds, cornerRadius: cornerRadiusFix).cgPath

For shadow path to work, you would have to change the bounds in UIView.layoutSubviews of the view with the shadow.
In your case, there is no need to use shadow path. Just remove the line setting the shadow path and everything will work correctly.

Related

Need to create custom shape for UIView Swift

I need to create this custom UIView for my project.I need to achieve this
I was doing trial and error by creating, My own custom UIView and then drawing on it.
I'm working on this
My Question is, if I'm able to draw my custom shape, How will I clip-off the remaining white space of the UIView
func drawCustomShape() {
let viewSize = self.frame.size
let buttonSlotRadius = CGFloat(30)
let effectiveViewHeight = viewSize.height - buttonSlotRadius
let path = UIBezierPath()
path.move(to: CGPoint(x: 30, y: 0))
path.addLine(to: CGPoint(x: viewSize.width - 30, y: 0))
path.addArc(withCenter: CGPoint(x: viewSize.width - 30,
y: 30),
radius: 30,
startAngle: .pi * 3 / 2,
endAngle: 0,
clockwise: true)
path.addLine(to: CGPoint(x: viewSize.width, y: effectiveViewHeight))
path.addArc(withCenter: CGPoint(x: viewSize.width - 30,
y: effectiveViewHeight - 30),
radius: 30,
startAngle: 0,
endAngle: .pi / 2,
clockwise: true)
path.addLine(to: CGPoint(x: viewSize.width / 4, y: effectiveViewHeight))
// close path join to origin
path.close()
UIColor.secondarySystemFill.setFill()
path.fill()
path.lineWidth = 6.0
path.stroke()
}
You can use UIView with clear background color, and use CAShapeLayer with desired path like this:
extension UIView {
func drawCustomShape() {
let cornerRadius: CGFloat = 18.0
let shapeOffset = self.frame.size.height * 0.2
//create shape layer
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.lineWidth = 1.0
shapeLayer.fillColor = UIColor.white.cgColor
self.layer.addSublayer(shapeLayer)
//create path
let path = UIBezierPath()
//top left point
path.move(to: CGPoint(x: 0, y: cornerRadius))
//top left corner
path.addQuadCurve(to: CGPoint(x: cornerRadius, y: 0),
controlPoint: CGPoint(x: 0, y: 0))
//top right point
path.addLine(to: CGPoint(x: self.frame.size.width - cornerRadius, y: 0))
//top right corner
path.addQuadCurve(to: CGPoint(x: self.frame.size.width, y: cornerRadius),
controlPoint: CGPoint(x: self.frame.size.width, y: 0))
//bottom right point
path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height - shapeOffset - cornerRadius))
//bottom right corner
path.addQuadCurve(to: CGPoint(x: self.frame.size.width - cornerRadius, y: self.frame.size.height - shapeOffset),
controlPoint: CGPoint(x: self.frame.size.width, y: self.frame.size.height - shapeOffset))
//bottom left
path.addLine(to: CGPoint(x: cornerRadius, y: self.frame.size.height))
//bottom left corner
path.addQuadCurve(to: CGPoint(x: 0, y: self.frame.size.height - cornerRadius),
controlPoint: CGPoint(x: 0, y: self.frame.size.height))
path.close()
shapeLayer.path = path.cgPath
}
Usage:
myView.drawCustomShape()
Result:

Need to achieve UI Like this

I have been working on creating this UI.But not able to achieve that upper slant, I tried tweaking around my Code,
By Doing that, UI messes up.
Any Help would be appreciated.
let buttonSlotRadius = CGFloat(50)
let path = UIBezierPath()
path.move(to: CGPoint(x: self.layer.frame.width - buttonSlotRadius , y: 0))
path.addArc(withCenter: CGPoint(x: self.layer.frame.width - buttonSlotRadius, y: buttonSlotRadius), radius: buttonSlotRadius, startAngle: 3 * .pi / 2, endAngle: 2 * .pi, clockwise: true)
path.addLine(to: CGPoint(x: buttonSlotRadius, y: 2 * buttonSlotRadius))
path.addArc(withCenter: CGPoint(x: buttonSlotRadius, y: 2 * buttonSlotRadius), radius: buttonSlotRadius, startAngle: .pi, endAngle: 3 * .pi / 2, clockwise: true)
path.addLine(to: CGPoint(x: self.layer.frame.width, y: 50))
path.addLine(to: CGPoint(x: self.layer.frame.width, y: self.layer.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.layer.frame.height))
path.addLine(to: CGPoint(x: 0, y: 2 * buttonSlotRadius))
path.addLine(to: CGPoint(x: self.layer.frame.width - buttonSlotRadius, y: 0))
path.close()
UIColor.white.setFill()
path.fill()
I figured It Out.
I added one more layer to Fix that
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 100))
path.addLine(to: CGPoint(x: 0.0, y: self.bounds.height))
path.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
path.addLine(to: CGPoint(x: self.bounds.width, y: 50))
path.close()
UIColor.white.setFill()
path.fill()
And Now its Working.Working UI

How to draw custom shape with arc in centre using UIBezierPath

I am try create custom shape of view with arc in centre, but I am not working with this tools in past.
I create shape what I want but not elegant like need.
What I do wrong?
I am feel where I do mistakes but can't fix it.
How work with this tools, I use this tutorial - https://ayusinghi96.medium.com/draw-custom-shapes-and-views-with-uiberzierpath-ios-1737f5cb975
Result which need:
My result:
My code:
import Foundation
import UIKit
/// Custom card view class
///
class CardView : UIView
{
// init the view with a rectangular frame
override init(frame: CGRect)
{
super.init(frame: frame)
backgroundColor = UIColor.clear
}
// init the view by deserialisation
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
backgroundColor = UIColor.clear
}
/// override the draw(_:) to draw your own view
///
/// Default implementation - `rectangular view`
///
override func draw(_ rect: CGRect)
{
let padding: CGFloat = 5.0
let centerButtonHeight: CGFloat = 50
let f = CGFloat(centerButtonHeight / 2.0) + padding
let halfW = frame.width/2.0
let r = CGFloat(11)
// Card view corner radius
let cardRadius = CGFloat(7)
// Button slot arc radius
let buttonSlotRadius = CGFloat(30)
// Card view frame dimensions
let viewSize = self.bounds.size
// Effective height of the view
let effectiveViewHeight = viewSize.height - buttonSlotRadius
// Get a path to define and traverse
let path = UIBezierPath()
// Shift origin to left corner of top straight line
path.move(to: CGPoint(x: cardRadius, y: 0))
// top line
path.addLine(to: CGPoint(x: viewSize.width - cardRadius, y: 0))
path.addLine(to: CGPoint(x: halfW-f-(r/2.0), y: 0))
//
path.addQuadCurve(to: CGPoint(x: halfW-f, y: (r/2.0)), controlPoint: CGPoint(x: halfW-f, y: 0))
//
path.addArc(withCenter: CGPoint(x: halfW, y: (r/7.0)), radius: f, startAngle: .pi, endAngle: 0, clockwise: false)
//
path.addQuadCurve(to: CGPoint(x: halfW+f+(r/5.5), y: 0), controlPoint: CGPoint(x: halfW+f, y: 0))
// top-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: cardRadius
),
radius: cardRadius,
startAngle: CGFloat(Double.pi * 3 / 2),
endAngle: CGFloat(0),
clockwise: true
)
// right line
path.addLine(
to: CGPoint(x: viewSize.width, y: effectiveViewHeight)
)
// bottom-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius,
startAngle: CGFloat(0),
endAngle: CGFloat(Double.pi / 2),
clockwise: true
)
// right half of bottom line
path.addLine(
to: CGPoint(x: viewSize.width / 4 * 3, y: effectiveViewHeight)
)
// left half of bottom line
path.addLine(
to: CGPoint(x: cardRadius, y: effectiveViewHeight)
)
// bottom-left corner arc
path.addArc(
withCenter: CGPoint(
x: cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius,
startAngle: CGFloat(Double.pi / 2),
endAngle: CGFloat(Double.pi),
clockwise: true
)
// left line
path.addLine(to: CGPoint(x: 0, y: cardRadius))
// top-left corner arc
path.addArc(
withCenter: CGPoint(x: cardRadius, y: cardRadius),
radius: cardRadius,
startAngle: CGFloat(Double.pi),
endAngle: CGFloat(Double.pi / 2 * 3),
clockwise: true
)
// close path join to origin
path.close()
// Set the background color of the view
UIColor.init(red: 0.118, green: 0.118, blue: 0.15, alpha: 1).set()
path.fill()
}
}
You need to change these two lines:
path.addArc(withCenter: CGPoint(x: halfW, y: (r/7.0)), radius: f, startAngle: .pi, endAngle: 0, clockwise: false)
//
path.addQuadCurve(to: CGPoint(x: halfW+f+(r/5.5), y: 0), controlPoint: CGPoint(x: halfW+f, y: 0))
to:
path.addArc(withCenter: CGPoint(x: halfW, y: (r/2.0)), radius: f, startAngle: .pi, endAngle: 0, clockwise: false)
//
path.addQuadCurve(to: CGPoint(x: halfW+f+(r/2), y: 0), controlPoint: CGPoint(x: halfW+f, y: 0))
And here is the fixed and slightly refactored code inside the func draw(_ rect: CGRect)
override func draw(_ rect: CGRect) {
let padding: CGFloat = 5
let centerButtonHeight: CGFloat = 50
let f: CGFloat = centerButtonHeight / 2 + padding
let halfW = frame.width / 2
let r: CGFloat = 11
// Card view corner radius
let cardRadius: CGFloat = 7
// Button slot arc radius
let buttonSlotRadius: CGFloat = 30
// Card view frame dimensions
let viewSize = self.bounds.size
// Effective height of the view
let effectiveViewHeight = viewSize.height - buttonSlotRadius
// Get a path to define and traverse
let path = UIBezierPath()
// Shift origin to left corner of top straight line
path.move(to: CGPoint(x: cardRadius, y: 0))
// top line
path.addLine(to: CGPoint(x: viewSize.width - cardRadius, y: 0))
path.addLine(to: CGPoint(x: halfW - f - (r / 2), y: 0))
path.addQuadCurve(
to: CGPoint(x: halfW - f, y: (r / 2)),
controlPoint: CGPoint(x: halfW - f, y: 0)
)
path.addArc(
withCenter: CGPoint(x: halfW, y: (r / 2)),
radius: f, startAngle: .pi, endAngle: 0, clockwise: false
)
//
path.addQuadCurve(
to: CGPoint(x: halfW + f + (r / 2), y: 0),
controlPoint: CGPoint(x: halfW + f, y: 0)
)
// top-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: cardRadius
),
radius: cardRadius, startAngle: .pi * 3 / 2, endAngle: 0, clockwise: true
)
// right line
path.addLine(
to: CGPoint(x: viewSize.width, y: effectiveViewHeight)
)
// bottom-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius, startAngle: 0, endAngle: .pi / 2, clockwise: true
)
// right half of bottom line
path.addLine(
to: CGPoint(x: viewSize.width / 4 * 3, y: effectiveViewHeight)
)
// left half of bottom line
path.addLine(
to: CGPoint(x: cardRadius, y: effectiveViewHeight)
)
// bottom-left corner arc
path.addArc(
withCenter: CGPoint(
x: cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius, startAngle: .pi / 2, endAngle: .pi, clockwise: true
)
// left line
path.addLine(to: CGPoint(x: 0, y: cardRadius))
// top-left corner arc
path.addArc(
withCenter: CGPoint(x: cardRadius, y: cardRadius),
radius: cardRadius, startAngle: .pi, endAngle: .pi / 2 * 3, clockwise: true
)
// close path join to origin
path.close()
// Set the background color of the view
UIColor(red: 0.118, green: 0.118, blue: 0.15, alpha: 1).set()
path.fill()
}

SwiftUI Path .closeSubpath() not working the same way as UIBezierPath .close()

I'm trying to draw the following shape with SwiftUI:
Before SwiftUI I just had to create a UIBezierPath, add the corners with addArc and then finally call close(), but I don't get the same result when calling closeSubpath on SwiftUI Path.
Here's my code:
Path { path in
let width: CGFloat = 23
let height: CGFloat = 24
let arrowWidth = height / 2.0
let cornerRadius = height / 7.5
path.addArc(center: CGPoint(x: width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: Angle(degrees: -90), endAngle: .zero, clockwise: true)
path.addLine(to: CGPoint(x: width, y: height - cornerRadius))
path.addArc(center: CGPoint(x: width - cornerRadius, y: height - cornerRadius), radius: cornerRadius, startAngle: .zero, endAngle: Angle(degrees: 90), clockwise: true)
path.addArc(center: CGPoint(x: arrowWidth + cornerRadius, y: height - cornerRadius), radius: cornerRadius, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 135), clockwise: true)
path.addArc(center: CGPoint(x: cornerRadius, y: height / 2.0), radius: cornerRadius, startAngle: Angle(degrees: 135), endAngle: Angle(degrees: 225), clockwise: true)
path.addArc(center: CGPoint(x: arrowWidth + cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: Angle(degrees: 225), endAngle: Angle(degrees: -90), clockwise: true)
path.closeSubpath()
}
.foregroundColor(.red)
Thanks in advance!
It should be drawn via Shape, which specifies rect for path, as in demo below
Note: also fixed clockwise direction
struct NewShape_Previews: PreviewProvider {
static var previews: some View {
NewShape()
.frame(width: 100, height: 48)
.foregroundColor(Color.red)
}
}
struct NewShape: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
let width: CGFloat = rect.width
let height: CGFloat = rect.height
let arrowWidth = height / 2.0
let cornerRadius = height / 7.5
path.addArc(center: CGPoint(x: width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: Angle(degrees: -90), endAngle: .zero, clockwise: false)
path.addLine(to: CGPoint(x: width, y: height - cornerRadius))
path.addArc(center: CGPoint(x: width - cornerRadius, y: height - cornerRadius), radius: cornerRadius, startAngle: .zero, endAngle: Angle(degrees: 90), clockwise: false)
path.addArc(center: CGPoint(x: arrowWidth + cornerRadius, y: height - cornerRadius), radius: cornerRadius, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 135), clockwise: false)
path.addArc(center: CGPoint(x: cornerRadius, y: height / 2.0), radius: cornerRadius, startAngle: Angle(degrees: 135), endAngle: Angle(degrees: 225), clockwise: false)
path.addArc(center: CGPoint(x: arrowWidth + cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: Angle(degrees: 225), endAngle: Angle(degrees: -90), clockwise: false)
}
}
}

How to get a border on UIBezierPath

I have this
CGRect container = CGRectMake(conX, conY, 220, 50);
UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:container cornerRadius:5.0];
[[UIColor blueColor] setFill];
[path fillWithBlendMode:kCGBlendModeNormal alpha:0.7];
I want to get a gray borer around it, have looked at a lot of stuff online but can't see a way to do it easily, is it possible?
If you're not opposed to using layers, something like this could work for you:
//create triangle path
UIBezierPath* path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 30)];
[path addLineToPoint:CGPointMake(100, 30)];
[path addLineToPoint:CGPointMake(100, 0)];
[path addLineToPoint:CGPointMake(0, 30)];
//apply path to shapelayer
CAShapeLayer* greenPath = [CAShapeLayer layer];
greenPath.path = path.CGPath;
[greenPath setFillColor:[UIColor greenColor].CGColor];
[greenPath setStrokeColor:[UIColor blueColor].CGColor];
greenPath.frame=CGRectMake(0, 0,100,30);
//add shape layer to view's layer
[[self layer] addSublayer:greenPath];
You can get bezierPath using this extension.
let path = UIBezierPath(yourView.bounds, cornerRadius: 12)
let layer = CAShapeLayer()
layer.masksToBounds = false
layer.path = path.cgPath
layer.strokeColor = UIColor.red.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 5
layer.addSublayer(topLayer)
extension UIBezierPath {
static func borderPathInRect(_ drawRect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: drawRect.origin.x, y: cornerRadius))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: CGFloat.pi,
endAngle: 3/2*CGFloat.pi ,
clockwise: true)
path.addLine(to: CGPoint(x: drawRect.size.width - cornerRadius, y: 0))
path.addArc(withCenter: CGPoint(x: drawRect.size.width - cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: 3/2*CGFloat.pi,
endAngle: 2*CGFloat.pi,
clockwise: true)
path.addLine(to: CGPoint(x: drawRect.size.width, y: drawRect.size.height - cornerRadius))
path.addArc(withCenter: CGPoint(x: drawRect.size.width - cornerRadius, y: drawRect.size.height - cornerRadius),
radius: cornerRadius,
startAngle: 0,
endAngle: CGFloat.pi/2,
clockwise: true)
path.addLine(to: CGPoint(x: cornerRadius, y: drawRect.size.height))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: drawRect.size.height - cornerRadius),
radius: cornerRadius,
startAngle: CGFloat.pi/2,
endAngle: CGFloat.pi,
clockwise: true)
path.addLine(to: CGPoint(x: 0, y: cornerRadius))
path.lineJoinStyle = .round
return path
}
}