How to draw custom shape with arc in centre using UIBezierPath - swift

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

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 do you trace the border of an iPhone screen (including the notch for an X, 11, or 12 series)?

I'm trying to trace a line around the outside of the main view - trace along the edge of the screen. The following image shows the outline, and the code below shows how it animates.
The problem is, the animation draws the outer edge first, and then it draws the area around the notch.
I need it to start to draw the outer edge, drop down and draw around the notch, and then continue along the outer edge until it finishes.
import UIKit
class ViewController: UIViewController {
var layer: CAShapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
setupBorder()
animateBorder()
}
func setupBorder() {
let bounds = self.view.bounds
if UIDevice.current.hasNotch {
// FIXME: needs tweaks for:
// 12 pro max (corners and notch)
// 12 pro (corners)
// 12 mini (notch)
// the math works for all X and 11 series
// border around the phone screen
let framePath = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 40, height: 40))
// Math courtesy of:
// https://www.paintcodeapp.com/news/iphone-x-screen-demystified
let devicePointWidth = UIScreen.main.bounds.size.width
let w = devicePointWidth * 83 / 375
let n = devicePointWidth * 209 / 375
let notchBounds = CGRect(x: w, y: -10, width: n, height: 40)
let notchPath = UIBezierPath(roundedRect: notchBounds, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 20, height: 20))
// This is the problem. The framePath is drawn first,
// and then the notchPath is drawn. I need these to be
// mathematically merged
framePath.append(notchPath)
framePath.usesEvenOddFillRule = true
layer.path = framePath.cgPath
} else {
// if device is an 8 or lower, the border of the screen
// is a rectangle
layer.path = UIBezierPath(rect: bounds).cgPath
}
layer.strokeColor = UIColor.blue.cgColor
layer.strokeEnd = 0.0
layer.lineWidth = 20.0
layer.fillColor = nil
self.view.layer.addSublayer(layer)
}
func animateBorder() {
CATransaction.begin()
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 6
CATransaction.setCompletionBlock { [weak self] in
self?.layer.strokeColor = UIColor.cyan.cgColor
self?.layer.strokeEnd = 1.0
}
layer.add(animation, forKey: "stroke-screen")
CATransaction.commit()
}
}
extension UIDevice {
var hasNotch: Bool {
// FIXME: Does not work with apps that use SceneDelegate
// Requires the window var in the AppDelegate
if #available(iOS 11.0, *) {
return UIApplication.shared.delegate?.window??.safeAreaInsets.bottom ?? 0 > 20
}
return false
}
}
The code above can be used in a new project, BUT the SceneDelegate.swift file will need to be removed, the Application Scene Manifest entry in the Info.plist will need to be deleted, and var window: UIWindow? will need to be added to AppDelegate.swift.
You're having that issue because you're creating two separate shapes and appending one to the next. So your path is being properly drawn.
You need to create one shape by using the exact coordinates so its properly drawn.
Here's a simple shape without any rounded corners:
let framePath = UIBezierPath()
framePath.move(to: .zero)
framePath.addLine(to: CGPoint(x: w, y: 0))
framePath.addLine(to: CGPoint(x: w, y: 30))
framePath.addLine(to: CGPoint(x: w+n, y: 30))
framePath.addLine(to: CGPoint(x: w+n, y: 0))
framePath.addLine(to: CGPoint(x: devicePointWidth, y: 0))
framePath.addLine(to: CGPoint(x: devicePointWidth, y: self.view.bounds.height))
framePath.addLine(to: CGPoint(x: 0, y: self.view.bounds.height))
framePath.addLine(to: .zero)
Then you can user addArc(withCenter:radius:startAngle:endAngle:clockwise:) to add the curved parts.
Here is a rough draft using some of the values that you had calculated:
let circleTop = CGFloat(3*Double.pi / 2)
let circleRight = CGFloat(0)
let circleBottom = CGFloat(Double.pi / 2)
let circleLeft = CGFloat(Double.pi)
let framePath = UIBezierPath()
framePath.move(to: CGPoint(x: 40, y: 0))
framePath.addLine(to: CGPoint(x: w-6, y: 0))
framePath.addArc(withCenter: CGPoint(x: w-6, y: 6), radius: 6, startAngle: circleTop, endAngle: circleRight, clockwise: true)
framePath.addArc(withCenter: CGPoint(x: w+20, y: 10), radius: 20, startAngle: circleLeft, endAngle: circleBottom, clockwise: false)
framePath.addLine(to: CGPoint(x: w+n-20, y: 30))
framePath.addArc(withCenter: CGPoint(x: w+n-20, y: 10), radius: 20, startAngle: circleBottom, endAngle: circleRight, clockwise: false)
framePath.addArc(withCenter: CGPoint(x: w+n+6, y: 6), radius: 6, startAngle: CGFloat(Double.pi), endAngle: circleTop, clockwise: true)
framePath.addLine(to: CGPoint(x: devicePointWidth-40, y: 0))
framePath.addArc(withCenter: CGPoint(x: devicePointWidth-40, y: 40), radius: 40, startAngle: circleTop, endAngle: circleRight, clockwise: true)
framePath.addLine(to: CGPoint(x: devicePointWidth, y: self.view.bounds.height - 40))
framePath.addArc(withCenter: CGPoint(x: UIScreen.main.bounds.size.width-40, y: UIScreen.main.bounds.size.height-40), radius: 40, startAngle: circleRight, endAngle: circleBottom, clockwise: true)
framePath.addLine(to: CGPoint(x: 40, y: self.view.bounds.height))
framePath.addArc(withCenter: CGPoint(x: 40, y: UIScreen.main.bounds.size.height-40), radius: 40, startAngle: circleBottom, endAngle: circleLeft, clockwise: true)
framePath.addLine(to: CGPoint(x: 0, y: 40))
framePath.addArc(withCenter: CGPoint(x: 40, y: 40), radius: 40, startAngle: circleLeft, endAngle: circleTop, clockwise: true)

How to make the UIView as Like Ticket in UITablevViewCell

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

Wrong shadowPath for some cornerRadius

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.