Custom Progress View with Step progress - ios

I'm trying to create following progress view.
I have write following code to make this view,
class SeggyProgressView: UIView {
//--------------------------------------------------
// MARK:- Variables
//--------------------------------------------------
var arrPointsRange = [0, 500, 1000, 1500, 2000, 2500, 3000]
var currenPoint = 1586
//--------------------------------------------------
// MARK:- Init Methods
//--------------------------------------------------
init() {
super.init(frame: .zero)
self.setupView()
}
//--------------------------------------------------
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
//--------------------------------------------------
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
//--------------------------------------------------
// MARK:- Custom Methods
//--------------------------------------------------
func setupView() {
let path = UIBezierPath()
// Add first point to x=10 and y=1/4 of view
let x: CGFloat = 50
var lineYPoint = self.bounds.height / 4
// Add starting point
path.move(to: CGPoint(x: x, y: lineYPoint))
// Calculate line width
let lineWidth = (self.bounds.width - (x * 2) - CGFloat(10 * (self.arrPointsRange.count - 2))) / CGFloat(self.arrPointsRange.count - 2)
// Keep it to store x value of
var lineXPoint = lineWidth
// Enumerated on point range to draw above lines and haf circle
for (index, _) in arrPointsRange.enumerated() {
if index < (self.arrPointsRange.count - 2) {
lineXPoint += lineWidth
path.addLine(to: CGPoint(x: lineXPoint, y: lineYPoint))
let arcCenterX = lineXPoint + 10
path.addArc(withCenter: CGPoint(x: arcCenterX, y: lineYPoint), radius: 8, startAngle: CGFloat(180.0).toRadians(), endAngle: CGFloat(0.0).toRadians(), clockwise: true)
}
}
lineXPoint += lineWidth
// Draw line line for above
path.addLine(to: CGPoint(x: lineXPoint, y: lineYPoint))
var arcCenterX = lineXPoint + 10
// draw full circle
path.addArc(withCenter: CGPoint(x: arcCenterX, y: lineYPoint), radius: 10, startAngle: CGFloat(210.0).toRadians(), endAngle: CGFloat(150.0).toRadians(), clockwise: true)
lineYPoint += 10
// Enumerate to draw below line and half circle
for (index, _) in arrPointsRange.enumerated() {
if index < (self.arrPointsRange.count - 2) {
lineXPoint -= lineWidth
path.addLine(to: CGPoint(x: lineXPoint, y: lineYPoint))
let arcCenterX = lineXPoint - 10
path.addArc(withCenter: CGPoint(x: arcCenterX, y: lineYPoint), radius: 8, startAngle: CGFloat(0.0).toRadians(), endAngle: CGFloat(180.0).toRadians(), clockwise: true)
}
}
lineXPoint -= lineWidth
// Draw first below line
path.addLine(to: CGPoint(x: lineXPoint, y: lineYPoint))
arcCenterX = lineXPoint - 10
let y = lineYPoint - 5
// Draw first full circle
path.addArc(withCenter: CGPoint(x: arcCenterX, y: y), radius: 10, startAngle: CGFloat(30).toRadians(), endAngle: CGFloat(300).toRadians(), clockwise: true)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.darkGray.cgColor
shapeLayer.lineWidth = 3
shapeLayer.fillColor = UIColor.orange.cgColor
self.layer.addSublayer(shapeLayer)
}
}
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
But I can't make exactly as above, I have following output.
Anyone know how to fix this ?

Related

How to create custom curved iOS UITabBar?

So this is the navigation my designer made for our project. Height of the TabBar is 70.
What I have tried so far.
My attempt was based on tutorial from Philipp Weiss.
https://betterprogramming.pub/draw-a-custom-ios-tabbar-shape-27d298a7f4fa
Its based on idea of creating custom IBDesignable UITabBar class and overriding draw method.
#IBDesignable
class CustomizedTabBar: UITabBar {
private var shapeLayer: CALayer?
override func draw(_ rect: CGRect) {
self.addShape()
}
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.blueMenu2.cgColor
shapeLayer.fillColor = UIColor.blueMenu2.cgColor
shapeLayer.lineWidth = 1.0
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
func createPath() -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
I was trying to edit bezier path to reach my goal but with no success.
I am not sure if this approach can work for this specific TabBar design.
Setting height of navigation to 70 was without problem.
#IBInspectable var height: CGFloat = 70
override open func sizeThatFits(_ size: CGSize) -> CGSize {
guard let window = UIApplication.shared.keyWindow else {
return super.sizeThatFits(size)
}
var sizeThatFits = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
sizeThatFits.height = height + window.safeAreaInsets.bottom
} else {
sizeThatFits.height = height
}
return sizeThatFits
}
How can I create this curved TabBar?
Do u know how to make similar shape just by using bezier curves?
To create a UIBezierPath for your desired shape...
move to 1
add 90° clockwise arc with center c1
add line to 2
add 90° clockwise arc with center c2
add 180° counter-clockwise arc with center c3
add 90° clockwise arc with center c4
add line to 3
add 90° clockwise arc with center c5
add line to 4
add 90° clockwise arc with center c6
add line to 5
add 90° clockwise arc with center c7
close path
Here is some sample code - it's a UIView subclass, with all the path elements in layoutSubviews():
class TabBarShapeView: UIView {
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.gray.cgColor
shapeLayer.lineWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
let middleRad: CGFloat = bounds.height - 10.0
let cornerRad: CGFloat = 12.0
let pth = UIBezierPath()
let topLeftC: CGPoint = CGPoint(x: bounds.minX + cornerRad, y: bounds.minY + cornerRad)
let topRightC: CGPoint = CGPoint(x: bounds.maxX - cornerRad, y: bounds.minY + cornerRad)
let botRightC: CGPoint = CGPoint(x: bounds.maxX - cornerRad, y: bounds.maxY - cornerRad)
let botLeftC: CGPoint = CGPoint(x: bounds.minX + cornerRad, y: bounds.maxY - cornerRad)
var pt: CGPoint!
// 1
pt = CGPoint(x: bounds.minX, y: bounds.minY + cornerRad)
pth.move(to: pt)
// c1
pth.addArc(withCenter: topLeftC, radius: cornerRad, startAngle: .pi * 1.0, endAngle: .pi * 1.5, clockwise: true)
// 2
pt = CGPoint(x: bounds.midX - middleRad, y: bounds.minY)
pth.addLine(to: pt)
// c2
pt.y += middleRad * 0.5
pth.addArc(withCenter: pt, radius: middleRad * 0.5, startAngle: -.pi * 0.5, endAngle: 0.0, clockwise: true)
// c3
pt.x += middleRad * 1.0
pth.addArc(withCenter: pt, radius: middleRad * 0.5, startAngle: .pi * 1.0, endAngle: 0.0, clockwise: false)
// c4
pt.x += middleRad * 1.0
pth.addArc(withCenter: pt, radius: middleRad * 0.5, startAngle: .pi * 1.0, endAngle: .pi * 1.5, clockwise: true)
// 3
pt = CGPoint(x: bounds.maxX - cornerRad, y: bounds.minY)
pth.addLine(to: pt)
// c5
pth.addArc(withCenter: topRightC, radius: cornerRad, startAngle: -.pi * 0.5, endAngle: 0.0, clockwise: true)
// 4
pt = CGPoint(x: bounds.maxX, y: bounds.maxY - cornerRad)
pth.addLine(to: pt)
// c6
pth.addArc(withCenter: botRightC, radius: cornerRad, startAngle: 0.0, endAngle: .pi * 0.5, clockwise: true)
// 5
pt = CGPoint(x: bounds.minX + cornerRad, y: bounds.maxY)
pth.addLine(to: pt)
// c7
pth.addArc(withCenter: botLeftC, radius: cornerRad, startAngle: .pi * 0.5, endAngle: .pi * 1.0, clockwise: true)
pth.close()
shapeLayer.path = pth.cgPath
}
}
Your subclass likely isn't working because UITabBar doesn't draw the tab bar itself in drawRect(). But makes it from multiple internal sub views.
I'd recommend using a UITabBarController, but hiding the UITabBar itself.
self.tabBarController.tabBar.hidden = true
Then putting your own custom tab bar look alike view at the button of the screen.
Adding additionalSafeAreaInsets to make the content move up out of the way of your new view, like they would the real tab bar.
Then just change the tab index yourself on button presses.
self.tabBarController.selectedIndex = 1

Draw animated rounded rect line with gradient color

So I'm trying to create a loading indicator on an element, basically it's a path that's expanding and contracting towards the end.
The hard part is that the path is a gradient color which I have no idea to while also animating the paths strokeStart and strokeEnd
Here is what I have so far without the gradient.
//
// Created by Tommy Sadiq Hinrichsen on 07/07/2021.
// Copyright (c) 2021 Dagrofa. All rights reserved.
//
import Foundation
import UIKit
#IBDesignable
class LoadingBorderView: UIView {
private let strokeLayer = CAShapeLayer()
var startPosition: UIRectEdge = .right {
didSet { self.setNeedsDisplay() }
}
private var cornerRadius: CGFloat { return self.frame.height / 2 }
override init(frame: CGRect) {
super.init(frame: frame)
self.configureView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.configureView()
}
private func configureView() {
self.strokeLayer.fillColor = nil
self.strokeLayer.strokeColor = UIColor.black.cgColor
self.strokeLayer.strokeEnd = 0
self.strokeLayer.lineCap = .round
self.layer.addSublayer(self.strokeLayer)
}
private func getRoundedPath() -> UIBezierPath {
let path = UIBezierPath()
let drawTop: (UIBezierPath) -> Void = { (path: UIBezierPath) in
path.move(to: CGPoint(x: 0, y: self.cornerRadius))
path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 180.cgfloat.toRadians(), endAngle: 270.cgfloat.toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: self.bounds.width - self.cornerRadius, y: 0))
path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: -90.cgfloat.toRadians(), endAngle: 0.cgfloat.toRadians(), clockwise: true)
}
let drawBottom: (UIBezierPath) -> Void = { (path: UIBezierPath) in
path.move(to: CGPoint(x: self.frame.width, y: self.cornerRadius))
path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 0.cgfloat.toRadians(), endAngle: 90.cgfloat.toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: self.cornerRadius, y: self.frame.height))
path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 90.cgfloat.toRadians(), endAngle: 180.cgfloat.toRadians(), clockwise: true)
}
switch self.startPosition {
case .right:
drawBottom(path)
drawTop(path)
case .left:
drawTop(path)
drawBottom(path)
default:
fatalError("LoadingBorderView must start from left or right")
}
return path
}
func startAnimation(duration: TimeInterval = 2.0, delay: TimeInterval = 0.3) {
let strokeStartAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart))
strokeStartAnimation.beginTime = delay
strokeStartAnimation.duration = duration
strokeStartAnimation.fromValue = 0
strokeStartAnimation.toValue = 1
let strokeEndAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
strokeEndAnimation.duration = duration
strokeEndAnimation.fromValue = 0
strokeEndAnimation.toValue = 1
strokeEndAnimation.fillMode = .forwards
let group = CAAnimationGroup()
group.beginTime = CACurrentMediaTime()
group.timingFunction = .easeInOutCubic
group.duration = duration + delay
group.repeatCount = .infinity
group.animations = [strokeStartAnimation, strokeEndAnimation]
self.strokeLayer.add(group, forKey: "strokeLayer")
}
func stopAnimation() {
self.strokeLayer.removeAllAnimations()
}
override func draw(_ rect: CGRect) {
self.strokeLayer.frame = self.bounds
self.strokeLayer.path = self.getRoundedPath().cgPath
}
}
public extension CAMediaTimingFunction {
///https://easings.net/#easeInOutCubic
static var easeInOutCubic = CAMediaTimingFunction(controlPoints: 0.65, 0, 0.35, 1)
}

Drawing Shape layer using Coregraphics - iOS

I am trying to achieve following shape using coregraphics.
I am able to create a rounded rect
func createRoundedRect() {
let path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 15.0)
// Specify the point that the path should start get drawn.
path.move(to: CGPoint(x: 0.0, y: 0.0))
// Create a line between the starting point and the bottom-left side of the view.
path.addLine(to: CGPoint(x: 0.0, y: self.frame.size.height))
// Create the bottom line (bottom-left to bottom-right).
path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height))
// Create the vertical line from the bottom-right to the top-right side.
path.addLine(to: CGPoint(x: self.frame.size.width, y: 0.0))
// Close the path. This will create the last line automatically.
path.close()
}
But I am not sure how to make a view of above shape. Any help or idea is appreciated.
You render this with just two arcs, one for the top and one for the bottom. Just use a fat lineWidth and set the strokeColor to be the same as the fillColor to achieve the desired corner radius.
For example:
#IBDesignable
class TvView: UIView {
override class var layerClass: AnyClass { CAShapeLayer.self }
var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer}
#IBInspectable var curveHeight: CGFloat = 10 { didSet { setNeedsLayout() } }
#IBInspectable var cornerRadius: CGFloat = 10 { didSet { setNeedsLayout() } }
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.path = path()?.cgPath
shapeLayer.lineWidth = cornerRadius * 2
shapeLayer.lineJoin = .round
}
func path() -> UIBezierPath? {
let rect = bounds.insetBy(dx: cornerRadius, dy: cornerRadius)
guard
rect.height > 2 * curveHeight,
rect.width > 0,
curveHeight > 0
else {
return nil
}
let angle: CGFloat = 2 * (atan2(curveHeight, rect.width / 2))
let radius = rect.width / 2 / sin(angle)
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.minY + radius), radius: radius, startAngle: .pi * 3 / 2 - angle, endAngle: .pi * 3 / 2 + angle, clockwise: true)
path.addArc(withCenter: CGPoint(x: rect.midX, y: rect.maxY - radius), radius: radius, startAngle: .pi / 2 - angle, endAngle: .pi / 2 + angle, clockwise: true)
path.close()
return path
}
}
Using the same color for stroke and fill, that yields:
Or, so you can see what’s going on, here it is with the stroke rendered in a different color:

Trim UIView with 2 arcs

I have a UIView and I want to trim it with two circles, like I've drawn(sorry for the quality).
My code:
final class TrimmedView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let size = CGSize(width: 70, height: 70)
let innerRadius: CGFloat = 366.53658283002471
let innerBottomRadius: CGFloat = 297.88543112651564
let path = UIBezierPath()
path.move(to: CGPoint(x: -innerRadius + (size.width / 2), y: innerRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerRadius), radius: innerRadius, startAngle: CGFloat.pi, endAngle: 0, clockwise: true)
path.move(to: CGPoint(x: -innerBottomRadius + (size.width / 2), y: innerBottomRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerBottomRadius), radius: innerBottomRadius, startAngle: 0, endAngle: CGFloat.pi, clockwise: true)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.shadowPath = path.cgPath
layer.mask = shapeLayer
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let view = UIView(frame: CGRect(origin: CGPoint(x: (self.view.bounds.width - 70) / 2, y: (self.view.bounds.height - 70) / 2), size: CGSize(width: 70, height: 70)))
view.backgroundColor = .red
self.view.addSubview(view)
let view1 = TrimmedView(frame: view.frame)
view1.backgroundColor = .yellow
self.view.addSubview(view1)
}
I got this result. It seems for me that top trimming works but the bottom doesn't and I don't know why. Any help would be appreciated. Thanks.
Here is a custom view that should give you what you want.
The UIBezierPath uses QuadCurves for the top "convex" arc and the bottom "concave" arc.
It is marked #IBDesignable so you can see it at design-time in IB / Storyboard. The "height" of the arc and the fill color are each set as #IBInspectable so you can adjust those values at design-time as well.
To use it in Storyboard:
Add a normal UIView
change the Class to BohdanShapeView
in the Attributes Inspector pane, set the Arc Offset and the Fill Color
set the background color as with a normal view (you'll probably use clear)
Result:
To use it via code:
let view1 = BohdanShapeView(frame: view.frame)
view1.fillColor = .systemTeal
view1.arcOffset = 10
self.view.addSubview(view1)
Here is the class:
#IBDesignable
class BohdanShapeView: UIView {
#IBInspectable var arcOffset: CGFloat = 0.0
#IBInspectable var fillColor: UIColor = UIColor.white
let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the shape layer
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// fill color for the shape
shapeLayer.fillColor = self.fillColor.cgColor
let width = bounds.size.width
let height = bounds.size.height
let bezierPath = UIBezierPath()
// start at arcOffset below top-left
bezierPath.move(to: CGPoint(x: 0.0, y: 0.0 + arcOffset))
// add curve to arcOffset below top-right
bezierPath.addQuadCurve(to: CGPoint(x: width, y: 0.0 + arcOffset), controlPoint: CGPoint(x: width * 0.5, y: 0.0 - arcOffset))
// add line to bottom-right
bezierPath.addLine(to: CGPoint(x: width, y: height))
// add curve to bottom-left
bezierPath.addQuadCurve(to: CGPoint(x: 0.0, y: height), controlPoint: CGPoint(x: width * 0.5, y: height - arcOffset * 2.0))
// close the path
bezierPath.close()
shapeLayer.path = bezierPath.cgPath
}
}

Drawing a custom view using UIBezierPath results in a non-symmetrical shape

I'm trying to draw an UIView with some 'curvy edges'.
Here's what it's supposed to look like:
here's what I got:
Notice how the top right (TR) corner is not symmetrical to the bottom right (BR) corner ? The BR corner is very similar to what I want to achieve but I can't get the TR corner to align correctly (played around with bunch of different start and end angles).
here's the code:
struct Constants {
static let cornerRadius: CGFloat = 15.0 // used for left-top and left-bottom curvature
static let rightTipWidth: CGFloat = 40.0 // the max. width for the right tip thingy
static let rightCornerRadius: CGFloat = 10.0 // the radius for the right tip
static let rightEdgeRadius: CGFloat = 10.0 // the radius for the top right and bottom right curvature
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// Initialize the path.
let path = UIBezierPath()
// starting point
let startingPoint = CGPoint(x: Constants.cornerRadius, y: 0.0)
path.move(to: startingPoint)
// create a center point for the arc for the top left corner
let leftTopCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: Constants.cornerRadius)
path.addArc(withCenter: leftTopCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 270.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
// move the path to the bottom left corner
path.addLine(to: CGPoint(x: 0.0, y: frame.size.height - Constants.cornerRadius))
// add the arc to bottom left
let leftBottomCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: frame.size.height - Constants.cornerRadius)
path.addArc(withCenter: leftBottomCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 180.degreesToRadians, endAngle: 90.degreesToRadians, clockwise: false)
// move along the bottom to the right edge - rightTipWidth
let maxXRightEdge = frame.size.width - Constants.rightTipWidth
path.addLine(to: CGPoint(x: maxXRightEdge, y: frame.size.height))
// add a curve at the bottom before tipping up at 45 degrees
let bottomRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: frame.size.height - Constants.rightEdgeRadius)
path.addArc(withCenter: bottomRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 90.degreesToRadians, endAngle: 45.degreesToRadians, clockwise: false)
// figure out the center for the right side curvature
let rightMidPointY = frame.size.height / 2.0
let halfRadius = (Constants.rightCornerRadius / 2.0)
// move up till the mid point corner radius
path.addLine(to: CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY + halfRadius)))
// the destination for the curve (end point of the curve)
let rightEndPoint = CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY - halfRadius))
// figure out the right side tip's control point (See: https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve)
let rightControlPoint = CGPoint(x: frame.size.width - halfRadius, y: rightMidPointY)
// add the curve for the right side tip
path.addQuadCurve(to: rightEndPoint, controlPoint: rightControlPoint)
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius))
let topRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: Constants.rightEdgeRadius)
path.addArc(withCenter: topRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 315.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: false) // straight
path.close()
// Specify the fill color and apply it to the path.
UIColor.orange.setFill()
path.fill()
// Specify a border (stroke) color.
UIColor.orange.setStroke()
path.stroke()
}
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
Just a quick summary of my thought process:
Create a bezierPath and move it to the startingPoint
Add the LT (left-top) curve and move the line downards
Move the line along the left edge and add the LB (left-bottom) curve
and the move line along the bottom to the right edge
Move the line till frame.size.width - Constants.rightTipWidth
Add an arc with a center point at x = currentPoint and y = height- rightEdgeRadius
Move the line up until y = (height / 2.0) +
(Constants.rightCornerRadius / 2.0)
Add the QuadCurve with an end point of y = (height / 2.0) -
(Constants.rightCornerRadius / 2.0)
Move the line up till x = maxXRightEdge + Constants.rightEdgeRadius
Add the top right (TR) curve ---> resulting in a non-symmetrical
curvature
Here is another rendition:
#IBDesignable
open class PointerView: UIView {
/// The left-top and left-bottom curvature
#IBInspectable var cornerRadius: CGFloat = 15 { didSet { updatePath() } }
/// The radius for the right tip
#IBInspectable var rightCornerRadius: CGFloat = 10 { didSet { updatePath() } }
/// The radius for the top right and bottom right curvature
#IBInspectable var rightEdgeRadius: CGFloat = 10 { didSet { updatePath() } }
/// The fill color
#IBInspectable var fillColor: UIColor = .blue { didSet { shapeLayer.fillColor = fillColor.cgColor } }
/// The stroke color
#IBInspectable var strokeColor: UIColor = .clear { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
/// The angle of the tip
#IBInspectable var angle: CGFloat = 90 { didSet { updatePath() } }
/// The line width
#IBInspectable var lineWidth: CGFloat = 0 { didSet { updatePath() } }
/// The shape layer for the pointer
private lazy var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
_shapeLayer.fillColor = fillColor.cgColor
_shapeLayer.strokeColor = strokeColor.cgColor
_shapeLayer.lineWidth = lineWidth
return _shapeLayer
}()
public override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
layer.addSublayer(shapeLayer)
}
open override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
private func updatePath() {
let path = UIBezierPath()
let offset = lineWidth / 2
let boundingRect = bounds.insetBy(dx: offset, dy: offset)
let arrowTop = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.minY)
let arrowRight = CGPoint(x: boundingRect.maxX, y: boundingRect.midY)
let arrowBottom = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.maxY)
let start = CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.minY)
// top left
path.move(to: start)
path.addQuadCurve(to: CGPoint(x: boundingRect.minX, y: boundingRect.minY + cornerRadius), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.minY))
// left
path.addLine(to: CGPoint(x: boundingRect.minX, y: boundingRect.maxY - cornerRadius))
// lower left
path.addQuadCurve(to: CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.maxY), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.maxY))
// bottom
path.addLine(to: calculate(from: path.currentPoint, to: arrowBottom, less: rightEdgeRadius))
// bottom right (before tip)
path.addQuadCurve(to: calculate(from: arrowRight, to: arrowBottom, less: rightEdgeRadius), controlPoint: arrowBottom)
// bottom edge of tip
path.addLine(to: calculate(from: path.currentPoint, to: arrowRight, less: rightCornerRadius))
// tip
path.addQuadCurve(to: calculate(from: arrowTop, to: arrowRight, less: rightCornerRadius), controlPoint: arrowRight)
// top edge of tip
path.addLine(to: calculate(from: path.currentPoint, to: arrowTop, less: rightEdgeRadius))
// top right (after tip)
path.addQuadCurve(to: calculate(from: start, to: arrowTop, less: rightEdgeRadius), controlPoint: arrowTop)
path.close()
shapeLayer.lineWidth = lineWidth
shapeLayer.path = path.cgPath
}
/// Calculate some point between `startPoint` and `endPoint`, but `distance` from `endPoint
///
/// - Parameters:
/// - startPoint: The starting point.
/// - endPoint: The ending point.
/// - distance: Distance from the ending point
/// - Returns: Returns the point that is `distance` from the `endPoint` as you travel from `startPoint` to `endPoint`.
private func calculate(from startPoint: CGPoint, to endPoint: CGPoint, less distance: CGFloat) -> CGPoint {
let angle = atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x)
let totalDistance = hypot(endPoint.y - startPoint.y, endPoint.x - startPoint.x) - distance
return CGPoint(x: startPoint.x + totalDistance * cos(angle),
y: startPoint.y + totalDistance * sin(angle))
}
}
And because that is #IBDesignable, I can put it in a separate framework target and then optionally use it (and customize it) right in Interface Builder:
The only change I made in parameters was to not use the width of the tip, but rather the angle of the tip. That way, if the size changes as constraints (or whatever) change, it preserves the desired shape.
I also changed this to use a CAShapeLayer rather that a custom draw(_:) method to enjoy any efficiencies that Apple has built in to shape layers.
I don't know your implementation but I think it will be easy if you implemented it like that , that way you cam achieve symmetric shape perfectly
to draw a triangle , just tweak the positions of triangle points
class TriangleView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.beginPath()
context.move(to: CGPoint(x: rect.minX, y: rect.maxY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
context.addLine(to: CGPoint(x: (rect.maxX / 2.0), y: rect.minY))
context.closePath()
context.setFillColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 0.60)
context.fillPath()
}
}
Here, you forgot halfRadius
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius - halfRadius))
Full code:
override func draw(_ rect: CGRect) {
super.draw(rect)
// Initialize the path.
let path = UIBezierPath()
// starting point
let startingPoint = CGPoint(x: Constants.cornerRadius, y: 0.0)
path.move(to: startingPoint)
// create a center point for the arc for the top left corner
let leftTopCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: Constants.cornerRadius)
path.addArc(withCenter: leftTopCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 270.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
// move the path to the bottom left corner
path.addLine(to: CGPoint(x: 0.0, y: frame.size.height - Constants.cornerRadius))
// add the arc to bottom left
let leftBottomCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: frame.size.height - Constants.cornerRadius)
path.addArc(withCenter: leftBottomCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 180.degreesToRadians, endAngle: 90.degreesToRadians, clockwise: false)
// move along the bottom to the right edge - rightTipWidth
let maxXRightEdge = frame.size.width - Constants.rightTipWidth
path.addLine(to: CGPoint(x: maxXRightEdge, y: frame.size.height))
// add a curve at the bottom before tipping up at 45 degrees
let bottomRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: frame.size.height - Constants.rightEdgeRadius)
path.addArc(withCenter: bottomRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 90.degreesToRadians, endAngle: 45.degreesToRadians, clockwise: false)
// figure out the center for the right side curvature
let rightMidPointY = frame.size.height / 2.0
let halfRadius = (Constants.rightCornerRadius / 2.0)
// move up till the mid point corner radius
path.addLine(to: CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY + halfRadius)))
// the destination for the curve (end point of the curve)
let rightEndPoint = CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY - halfRadius))
// figure out the right side tip's control point (See: https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve)
let rightControlPoint = CGPoint(x: frame.size.width - halfRadius, y: rightMidPointY)
// add the curve for the right side tip
path.addQuadCurve(to: rightEndPoint, controlPoint: rightControlPoint)
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius - halfRadius))
let topRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: Constants.rightEdgeRadius)
path.addArc(withCenter: topRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 315.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: false) // straight
path.close()
// Specify the fill color and apply it to the path.
UIColor.orange.setFill()
path.fill()
// Specify a border (stroke) color.
UIColor.orange.setStroke()
path.stroke()
}

Resources