Search code examples
iosswiftswift5uibezierpath

How to make this shape in iOS swift?


I want to create this badge or direction-type view in swift. I cannot use image because text can be long in other languages. I want to make it with UIView. I want to achieve this:

enter image description here

I managed to make it with sharp points with this code

class ArrowView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }

    private func commonInit() -> Void {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0, y: 0)) // Starting point: Top left
        path.addLine(to: CGPoint(x: self.frame.width - self.frame.height/2, y: 0)) // Move to the top right before the arrow point
        path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height/2)) // Move to the tip of the arrow
        path.addLine(to: CGPoint(x: self.frame.width - self.frame.height/2, y: self.frame.height)) // Move to the bottom right before the arrow point
        path.addLine(to: CGPoint(x: 0, y: self.frame.height)) // Move to the bottom left
        path.close()

        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        shapeLayer.fillColor = UIColor.black.cgColor // Choose the color of the arrow

        self.layer.addSublayer(shapeLayer)
    }

    override func layoutSubviews() {
        self.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
        self.commonInit()
    }
}

How to make line joint rounded like the actual design?


Solution

  • Another option...

    Work with CGMutablePath and take advantage of tangent curves:

    @IBDesignable
    class ArrowView: UIView {
        
        @IBInspectable var cornerRadius: CGFloat = 6.0 { didSet { setNeedsLayout() } }
        @IBInspectable var color: UIColor = .black { didSet { setNeedsLayout() } }
        
        override class var layerClass: AnyClass { CAShapeLayer.self }
        var shapeLayer: CAShapeLayer { layer as! CAShapeLayer }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            // may want to include this to make sure self's background is clear
            self.backgroundColor = .clear
            
            // set the shape fill color
            shapeLayer.fillColor = color.cgColor
    
            // use rect as reference to bounds
            //  makes it easy if we want to inset the shape
            let rect: CGRect = bounds
            
            let arrowInset: CGFloat = rect.height / 2.0 //- cornerRadius / 2.0
            
            // top-left corner
            let pt1: CGPoint = .init(x: rect.minX, y: rect.minY)
            // top-right curve
            let pt2: CGPoint = .init(x: rect.maxX - arrowInset, y: rect.minY)
            // arrow point
            let pt3: CGPoint = .init(x: rect.maxX, y: rect.midY)
            // bottom-right curve
            let pt4: CGPoint = .init(x: rect.maxX - arrowInset, y: rect.maxY)
            // bottom-left corner
            let pt5: CGPoint = .init(x: rect.minX, y: rect.maxY)
            
            let cPath: CGMutablePath = CGMutablePath()
            
            cPath.move(to: pt1)
            cPath.addArc(tangent1End: pt2, tangent2End: pt3, radius: cornerRadius)
            cPath.addArc(tangent1End: pt3, tangent2End: pt4, radius: cornerRadius)
            cPath.addArc(tangent1End: pt4, tangent2End: pt5, radius: cornerRadius)
            cPath.addLine(to: pt5)
            cPath.closeSubpath()
            
            shapeLayer.path = cPath
        }
        
    }
    

    enter image description here

    Note: I used the same @IBInspectable properties to make it easy for you to compare with Rob's implementation.