Search code examples
iosswiftuikituibezierpathcgcontext

UIBezierPath partially stroke


I have this code to draw a rectangle which is rounded rect only on one side.

this one

  override func draw(_ rect: CGRect) {
    // Drawing code

    guard let context = UIGraphicsGetCurrentContext() else { return }

    let lineWidth = CGFloat(4)


    let pathRect = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
    let path = UIBezierPath(roundedRect: pathRect.inset(by: UIEdgeInsets(top: lineWidth, left: lineWidth, bottom: lineWidth, right: 0)), byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(width: 7, height: 7))

    context.setFillColor(UIColor.black.cgColor)
    path.fill()

    context.setLineWidth(lineWidth)
}

I want to stroke it with red color on all but the right edge (no stroke on the right edge). How do I do it?


Solution

  • You’ll have to create your own path.

    A couple of observations:

    • Don’t use the rect parameter. The rect is what is being asked to being drawn at this point in time, which may not be the entire view. Use bounds when figuring out what the overall path should be.

    • I might inset the path so that the stroke stays within the bounds of the view.

    • You can make this @IBDesignable if you want to also be able to see it rendered in IB.

    • You don’t really need UIGraphicsGetCurrentContext(). The UIKit methods fill(), stroke(), setFill(), and setStroke() methods automatically use the current context.

    Thus:

    @IBDesignable
    class OpenRightView: UIView {
        @IBInspectable var lineWidth: CGFloat = 4      { didSet { setNeedsDisplay() } }
        @IBInspectable var radius: CGFloat = 7         { didSet { setNeedsDisplay() } }
        @IBInspectable var fillColor: UIColor = .black { didSet { setNeedsDisplay() } }
        @IBInspectable var strokeColor: UIColor = .red { didSet { setNeedsDisplay() } }
    
        override func draw(_ rect: CGRect) {
            let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
    
            let path = UIBezierPath()
            path.lineWidth = lineWidth
            path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
            path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
            path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
            path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
            path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
            path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
    
            fillColor.setFill()
            path.fill()
    
            strokeColor.setStroke()
            path.stroke()
        }
    }
    

    That yields:

    enter image description here


    Theoretically, it might be more efficient to use CAShapeLayer and let Apple take care of the draw(_:) for us. E.g., they may have optimized the rendering to handle partial view updates, etc.

    That might look like the following:

    @IBDesignable
    class OpenRightView: UIView {
        @IBInspectable var lineWidth: CGFloat = 4      { didSet { updatePath() } }
        @IBInspectable var radius: CGFloat = 7         { didSet { updatePath() } }
        @IBInspectable var fillColor: UIColor = .black { didSet { shapeLayer.fillColor = fillColor.cgColor } }
        @IBInspectable var strokeColor: UIColor = .red { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
    
        lazy var shapeLayer: CAShapeLayer = {
            let shapeLayer = CAShapeLayer()
            shapeLayer.fillColor = fillColor.cgColor
            shapeLayer.strokeColor = strokeColor.cgColor
            shapeLayer.lineWidth = lineWidth
            return shapeLayer
        }()
    
        override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            configure()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            configure()
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            updatePath()
        }
    }
    
    private extension OpenRightView {
        func configure() {
            layer.addSublayer(shapeLayer)
        }
    
        func updatePath() {
            let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
    
            let path = UIBezierPath()
            path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
            path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
            path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
            path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
            path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
            path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
            shapeLayer.path = path.cgPath
            shapeLayer.lineWidth = lineWidth
        }
    }