harsh_v
harsh_v

Reputation: 3269

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

Top Rect

Arc1 Arc2

What would be the equivalent or better approach on swift?

Upvotes: 1

Views: 2264

Answers (3)

harsh_v
harsh_v

Reputation: 3269

//
//  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

Upvotes: 3

Amrit Trivedi
Amrit Trivedi

Reputation: 1270

I made quick code according to your requirement. I hope it may help you.

//// Color Declarations
let color = UIColor(red: 0.387, green: 0.416, blue: 0.718, alpha: 1.000)
let color2 = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)
let color3 = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)
let color4 = UIColor(red: 0.300, green: 0.586, blue: 0.712, alpha: 1.000)

//// Oval Drawing
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 41, y: 39, width: 20, height: 20))
color.setStroke()
ovalPath.lineWidth = 2.5
ovalPath.stroke()


//// Rectangle 2 Drawing
let rectangle2Path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 100, height: 17))
color4.setFill()
rectangle2Path.fill()


//// Bezier 2 Drawing
let bezier2Path = UIBezierPath()
bezier2Path.move(to: CGPoint(x: -6.5, y: 18.5))
bezier2Path.addCurve(to: CGPoint(x: 41.76, y: 18.5), controlPoint1: CGPoint(x: 40.61, y: 18.5), controlPoint2: CGPoint(x: 41.76, y: 18.5))
bezier2Path.addCurve(to: CGPoint(x: 47.5, y: 22.7), controlPoint1: CGPoint(x: 41.76, y: 18.5), controlPoint2: CGPoint(x: 47.5, y: 18.5))
bezier2Path.addCurve(to: CGPoint(x: 47.5, y: 39.5), controlPoint1: CGPoint(x: 47.5, y: 26.9), controlPoint2: CGPoint(x: 47.5, y: 39.5))
color3.setStroke()
bezier2Path.lineWidth = 1
bezier2Path.stroke()


//// Bezier 3 Drawing
let bezier3Path = UIBezierPath()
bezier3Path.move(to: CGPoint(x: 100.5, y: 17.5))
bezier3Path.addCurve(to: CGPoint(x: 58.5, y: 17.5), controlPoint1: CGPoint(x: 59.5, y: 17.5), controlPoint2: CGPoint(x: 58.5, y: 17.5))
bezier3Path.addCurve(to: CGPoint(x: 55.5, y: 21.5), controlPoint1: CGPoint(x: 58.5, y: 17.5), controlPoint2: CGPoint(x: 55.5, y: 18.5))
bezier3Path.addCurve(to: CGPoint(x: 55.5, y: 39.5), controlPoint1: CGPoint(x: 55.5, y: 24.5), controlPoint2: CGPoint(x: 55.5, y: 39.5))
color2.setStroke()
bezier3Path.lineWidth = 1
bezier3Path.stroke()


//// Rectangle Drawing
let rectanglePath = UIBezierPath(rect: CGRect(x: 47, y: 59, width: 8, height: 41))
color.setFill()
rectanglePath.fill()

Upvotes: 1

Iraniya Naynesh
Iraniya Naynesh

Reputation: 1165

You can user CAShape layer and UIBezierPath to do this if its not customeview // if its custom view its easier just create the path in draw and set the color below are some methods you will need to create the path and other properties like setting color etc. In case it's not the custom view you can use CAShapelayer with path to achieve the same.

//create your path 

let xpos: CGFloat = yourXpos // do your calculation and set the x and y
let ypos:cfFloat =  yourYPos
let path = UIBezierPath() // UIBezierPath is like a pan you draw line arch circle, like pen you can move from one position to another, if you want to close(connected starting and end point) you just call close()

path.move(to: CGPoint(x: xpos, y: yPos)) //move is like the 
path.addLine(to: CGPoint(x: xpos , y: yPos + 25))
path.addArc(withCenter: CGPoint(x: xpos + 1, y: yPos + 25), radius: 2, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
path.close()

path.move()//move to some new place 
You have to calculate x,y like you already done for Android and just set the correct x and y values

//create shape layer and set the path you just created to the shapelayer and shapelayer to your view
let shapeLayer = CAShapeLayer() 
shapeLayer.path = path.cgPath
self.yourView.layer.addSublayer(shapeLayer)
shapeLayer.lineWidth = 0.5 // setting the stoke width// thinkness of drawing 

you can set the stroke colour or can fill it
shapeLayer.fillColor = UIColor.black.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor


PS: I Haven't done the actual calculation but you have already done, so maybe with above help you can easly replicate for iOS.

Upvotes: 0

Related Questions