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)
}
}
}
Related
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()
}
I'am trying draw a circular crown with UIBezierPath. I can draw two circle with two diferent radius:
let bezierCrown = UIBezierPath()
bezierCrown.addArc(withCenter: center,
radius: raidus1,
startAngle: 0,
endAngle: CGFloat.pi*2,
clockwise: true)
bezierCrown.move(to: CGPoint(x: center.x+distancia2, y: center.y))
bezierCrown.addArc(withCenter: center,
radius: raidus2,
startAngle: 0,
endAngle: CGFloat.pi*2,
clockwise: true)
But when I fill:
UIColor.green.setFill()
bezierCrown.fill()
Everything is filled: image.
I want only the crown to be filled.
You need to set the fillRule to evenOdd
let center = CGPoint(x: 200, y: 400)
let raidus1:CGFloat = 100
let raidus2:CGFloat = 80
let bezierCrown = UIBezierPath()
bezierCrown.addArc(withCenter: center,
radius: raidus1,
startAngle: 0,
endAngle: CGFloat.pi*2,
clockwise: true)
bezierCrown.move(to: CGPoint(x: center.x+raidus2, y: center.y))
bezierCrown.addArc(withCenter: center,
radius: raidus2,
startAngle: 0,
endAngle: CGFloat.pi*2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierCrown.cgPath
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillRule = .evenOdd
view.layer.addSublayer(shapeLayer)
TicketViewTableViewCell.swift
#IBOutlet weak var ticketView: TestView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
ticketView.layer.masksToBounds = true
ticketView.layer.cornerRadius = 8.0
ticketView.circleRadius = 10
ticketView.circleY = ticketView.frame.height * 0.6
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
enter image description here
I want to get the UIView like the above image in TableViewCell.
You can use this custom view as a background view:
#IBDesignable class TicketBackground: UIView {
#IBInspectable var cornerRadius : CGFloat = 4
#IBInspectable var indentRadius : CGFloat = 10
#IBInspectable var color : UIColor = UIColor.lightGray
override func draw(_ rect: CGRect) {
drawBackground()
}
private func drawBackground()
{
self.backgroundColor = UIColor.clear
let width = self.frame.width
let height = self.frame.height
let path = UIBezierPath()
path.move(to: CGPoint(x: cornerRadius, y: 0))
path.addLine(to: CGPoint(x: width-cornerRadius, y: 0))
path.addArc(withCenter: CGPoint(x: width-cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: 3*CGFloat.pi/2, endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: width, y: height/2-indentRadius))
path.addArc(withCenter: CGPoint(x: width, y: height/2), radius: indentRadius, startAngle: 3*CGFloat.pi/2, endAngle: CGFloat.pi/2, clockwise: false)
path.addLine(to: CGPoint(x: width, y: height-cornerRadius))
path.addArc(withCenter: CGPoint(x: width-cornerRadius, y: height-cornerRadius), radius: cornerRadius, startAngle: 0, endAngle: CGFloat.pi/2, clockwise: true)
path.addLine(to: CGPoint(x: cornerRadius, y: height))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: height-cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi/2, endAngle: CGFloat.pi, clockwise: true)
path.addLine(to: CGPoint(x: 0, y: height/2+indentRadius))
path.addArc(withCenter: CGPoint(x: 0, y: height/2), radius: indentRadius, startAngle: CGFloat.pi/2, endAngle: 3*CGFloat.pi/2, clockwise: false)
path.addLine(to: CGPoint(x: 0, y: cornerRadius))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: 3*CGFloat.pi/2, clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = self.color.cgColor
self.layer.addSublayer(shapeLayer)
}
}
I get this error when I try to preview PriceLabel, however it builds successfully. The strangest part is that my code doesn't contain any typealias.
I have restarted Xcode, but without any change.
I get the error in the top left of the canvas.
See code below:
import SwiftUI
struct PriceLabel: View {
let price: Int
static let currencyFormatter: NumberFormatter = {
let currencyFormatter = NumberFormatter()
currencyFormatter.decimalSeparator = ","
currencyFormatter.groupingSeparator = " "
currencyFormatter.maximumFractionDigits = 0
return currencyFormatter
}()
var body: some View {
ZStack {
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.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)
Text("\(type(of: self).currencyFormatter.string(from: NSNumber(value: price))!):-")
.foregroundColor(.white)
}
}
}
struct PriceLabel_Previews: PreviewProvider {
static var previews: some View {
PriceLabel(price: 25)
}
}
Thanks in advance!
Make sure that whatever type is causing the problem, doesn't have the same name as your target module.
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.