Search code examples
iosswiftuiviewuibezierpathcashapelayer

iOS Draw Custom Shape using CAShapeLayer


I want to draw a custom shape similar to the image below.

Aim


  1. A line with inverted round corners
  2. A hollow circle
  3. Another line that follows

I achieved this in Android in the following way.

float radiusClear = halfWidth - strokeSize / 2f; // 1
canvas.drawRect(0, 0, width, radiusClear, rootPaint); // 2
canvas.drawCircle(0, radiusClear, radiusClear, clearPaint); // 3
canvas.drawCircle(width, radiusClear, radiusClear, clearPaint); // 4
canvas.drawLine(halfWidth, 0, halfWidth, halfHeight, rootPaint); // 5
canvas.drawLine(halfWidth, halfHeight, halfWidth, height, iconPaint); // 6
canvas.drawCircle(halfWidth, halfHeight, halfWidth, iconPaint); // 7
canvas.drawCircle(halfWidth, halfHeight, thirdWidth, clearPaint); // 8
  • Where (1) calculates the distances.
  • (2) Draw a rect at the top

Top Rect

  • (3) (4) Draws two circles that clears the rect so it looks like two arcs

Arc1 Arc2

  • Then rest of the calls draw the remaining views in a similar way.

What would be the equivalent or better approach on swift?


Solution

  • //
    //  ConnectorView.swift
    //
    //  Created by harsh vishwakrama on 5/24/18.
    //
    
    import UIKit
    
    private let grayColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
    private let purpleColor = UIColor(red: 0.387, green: 0.416, blue: 0.718, alpha: 1.000)
    
    
    @IBDesignable
    class ConnectorView: UIView {
    
        var mode: Mode = .end{
            didSet{
                let width = bounds.width
                let height = bounds.height
                let halfWidth = bounds.width / 2
                let halfHeight = bounds.height / 2
                let thirdWidth = bounds.width / 3
                let strokeWidth = width / 5
                let midPoint = CGPoint(x: bounds.midX, y: bounds.midY)
    
                switch mode {
                case .start:
                    drawStart(width, thirdWidth, halfWidth, halfHeight, midPoint,strokeWidth)
                case .node:
                    drawNode(halfWidth, thirdWidth, halfHeight, midPoint,strokeWidth)
                case .end:
                    drawEnd(halfWidth, thirdWidth, halfHeight, midPoint,strokeWidth)
                case .only:
                    drawOnly(width, thirdWidth, halfWidth, halfHeight, strokeWidth, midPoint)
                }
                layoutSubviews()
            }
        }
    
    
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            clipsToBounds = true
        }
    
        enum Mode {
            case start, node, end, only
        }
    }
    extension ConnectorView{
        fileprivate func drawStart(_ width: CGFloat, _ thirdWidth: CGFloat, _ halfWidth: CGFloat, _ halfHeight: CGFloat, _ midPoint: CGPoint, _ strokeWidth: CGFloat) {
            layer.sublayers?.forEach{ layer in
                layer.removeFromSuperlayer()
            }
            let linePathTop = UIBezierPath()
            linePathTop.move(to: CGPoint(x: -width, y: -thirdWidth))
            linePathTop.addCurve(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth), controlPoint1: CGPoint(x: halfWidth, y: -thirdWidth ), controlPoint2: CGPoint(x: halfWidth, y: -thirdWidth))
            linePathTop.move(to: CGPoint(x: 2 * width, y: -thirdWidth))
            linePathTop.addCurve(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth), controlPoint1: CGPoint(x: halfWidth, y: -thirdWidth ), controlPoint2: CGPoint(x: halfWidth, y: -thirdWidth))
            linePathTop.move(to: CGPoint(x: 0, y: -thirdWidth))
            linePathTop.addLine(to: CGPoint(x: width, y: -thirdWidth))
            linePathTop.move(to: CGPoint(x: halfWidth, y: -thirdWidth))
            linePathTop.addLine(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth))
            linePathTop.close()
    
            let shapeLayerTop = CAShapeLayer()
            shapeLayerTop.path = linePathTop.cgPath
            shapeLayerTop.fillColor = UIColor.clear.cgColor
            shapeLayerTop.strokeColor = purpleColor.cgColor
            shapeLayerTop.lineWidth = strokeWidth
            layer.addSublayer(shapeLayerTop)
    
            let shapeLayerBottom = CAShapeLayer()
            let linePathBottom = UIBezierPath()
            linePathBottom.move(to: CGPoint(x: halfWidth, y: halfHeight + thirdWidth))
            linePathBottom.addLine(to: CGPoint(x: halfWidth, y: bounds.height))
            linePathBottom.close()
    
            shapeLayerBottom.path = linePathBottom.cgPath
            shapeLayerBottom.strokeColor = grayColor.cgColor
            shapeLayerBottom.fillColor = UIColor.clear.cgColor
            shapeLayerBottom.lineWidth = strokeWidth
            layer.addSublayer(shapeLayerBottom)
    
            let shapeLayerMid = CAShapeLayer()
            let circlePath = UIBezierPath(arcCenter: midPoint , radius: thirdWidth, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
            shapeLayerMid.path = circlePath.cgPath
            shapeLayerMid.strokeColor = grayColor.cgColor
            shapeLayerMid.fillColor = UIColor.clear.cgColor
            shapeLayerMid.lineWidth = strokeWidth
            layer.addSublayer(shapeLayerMid)
        }
    
        fileprivate func drawEnd(_ halfWidth: CGFloat, _ thirdWidth: CGFloat, _ halfHeight: CGFloat, _ midPoint: CGPoint,_ strokeWidth: CGFloat) {
            layer.sublayers?.forEach{ layer in
                layer.removeFromSuperlayer()
            }
            let linePath = UIBezierPath()
            linePath.move(to: CGPoint(x: halfWidth, y: -thirdWidth))
            linePath.addLine(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth))
            linePath.close()
    
            let shapeLayerLine = CAShapeLayer()
            shapeLayerLine.fillColor = UIColor.clear.cgColor
            shapeLayerLine.strokeColor = grayColor.cgColor
            shapeLayerLine.lineWidth = strokeWidth
            shapeLayerLine.path = linePath.cgPath
            layer.addSublayer(shapeLayerLine)
    
            let shapeLayerMid = CAShapeLayer()
            let circlePath = UIBezierPath(arcCenter: midPoint , radius: thirdWidth, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
            shapeLayerMid.path = circlePath.cgPath
            shapeLayerMid.strokeColor = grayColor.cgColor
            shapeLayerMid.fillColor = UIColor.clear.cgColor
            shapeLayerMid.lineWidth = strokeWidth
            layer.addSublayer(shapeLayerMid)
        }
    
        fileprivate func drawNode(_ halfWidth: CGFloat, _ thirdWidth: CGFloat, _ halfHeight: CGFloat, _ midPoint: CGPoint,_ strokeWidth: CGFloat) {
            layer.sublayers?.forEach{ layer in
                layer.removeFromSuperlayer()
            }
            let linePath = UIBezierPath()
            linePath.move(to: CGPoint(x: halfWidth, y: -thirdWidth))
            linePath.addLine(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth))
    
            linePath.move(to: CGPoint(x: halfWidth, y: halfHeight + thirdWidth))
            linePath.addLine(to: CGPoint(x: halfWidth, y: bounds.height))
            linePath.close()
    
            let shapeLayerLine = CAShapeLayer()
            shapeLayerLine.fillColor = UIColor.clear.cgColor
            shapeLayerLine.strokeColor = grayColor.cgColor
            shapeLayerLine.lineWidth = strokeWidth
            shapeLayerLine.path = linePath.cgPath
            layer.addSublayer(shapeLayerLine)
    
            let shapeLayerMid = CAShapeLayer()
            let circlePath = UIBezierPath(arcCenter: midPoint , radius: thirdWidth, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
            shapeLayerMid.path = circlePath.cgPath
            shapeLayerMid.strokeColor = grayColor.cgColor
            shapeLayerMid.fillColor = UIColor.clear.cgColor
            shapeLayerMid.lineWidth = strokeWidth
            layer.addSublayer(shapeLayerMid)
        }
    
        fileprivate func drawOnly(_ width: CGFloat, _ thirdWidth: CGFloat, _ halfWidth: CGFloat, _ halfHeight: CGFloat, _ strokeWidth: CGFloat, _ midPoint: CGPoint) {
            layer.sublayers?.forEach{ layer in
                layer.removeFromSuperlayer()
            }
            let linePathTop = UIBezierPath()
            linePathTop.move(to: CGPoint(x: -width, y: -thirdWidth))
            linePathTop.addCurve(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth), controlPoint1: CGPoint(x: halfWidth, y: -thirdWidth ), controlPoint2: CGPoint(x: halfWidth, y: -thirdWidth))
            linePathTop.move(to: CGPoint(x: 2 * width, y: -thirdWidth))
            linePathTop.addCurve(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth), controlPoint1: CGPoint(x: halfWidth, y: -thirdWidth ), controlPoint2: CGPoint(x: halfWidth, y: -thirdWidth))
            linePathTop.move(to: CGPoint(x: 0, y: -thirdWidth))
            linePathTop.addLine(to: CGPoint(x: width, y: -thirdWidth))
            linePathTop.move(to: CGPoint(x: halfWidth, y: -thirdWidth))
            linePathTop.addLine(to: CGPoint(x: halfWidth, y: halfHeight - thirdWidth))
            linePathTop.close()
    
            let shapeLayerTop = CAShapeLayer()
            shapeLayerTop.path = linePathTop.cgPath
            shapeLayerTop.fillColor = UIColor.clear.cgColor
            shapeLayerTop.strokeColor = purpleColor.cgColor
            shapeLayerTop.lineWidth = strokeWidth
            layer.addSublayer(shapeLayerTop)
    
            let shapeLayerMid = CAShapeLayer()
            let circlePath = UIBezierPath(arcCenter: midPoint , radius: thirdWidth, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
            shapeLayerMid.path = circlePath.cgPath
            shapeLayerMid.strokeColor = grayColor.cgColor
            shapeLayerMid.fillColor = UIColor.clear.cgColor
            shapeLayerMid.lineWidth = strokeWidth
            layer.addSublayer(shapeLayerMid)
        }
    }
    

    This is the first solution I was able to come up with. May need some tweaks but this works for me. I need 3 stages of a UI to place in UITableViewCell. One for the first cell, one for the last cell and other for the remaining cells.

    The result is like this

    enter image description here