Search code examples
iosswiftuibezierpath

Set specific value to round corners of UIBezierPath


I know there are some similar questions like this, but in my case, I want to set cornerRadius = 8 for my layer.

enter image description here

If I set:

shapeLayer.lineCap = CAShapeLayerLineCap.round

it shows like this:

enter image description here

-> Not match as the design.

My code:

import Foundation
import UIKit

final class OnboardingCell: UICollectionViewCell {
  @IBOutlet weak var boxView: UIView!
  @IBOutlet weak var continueButton: UIButton!
  @IBOutlet weak var progressView: UIView!

  var onTap: (() -> Void)?
  var levelProgress: CGFloat = 0.1 {
    didSet {
      fgLayer.strokeEnd = levelProgress
    }
  }

  let bgLayer = CAShapeLayer()
  let fgLayer = CAShapeLayer()

  override func awakeFromNib() {
    super.awakeFromNib()
    setup()
    configure()
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    setupShapeLayer(shapeLayer: bgLayer)
    setupShapeLayer(shapeLayer: fgLayer)
  }

  private func setup() {
    bgLayer.lineWidth = 50
    bgLayer.fillColor = nil
    bgLayer.strokeStart = 470 / 1590 + 0.008
    bgLayer.strokeEnd = 1
    bgLayer.cornerRadius = 8
    progressView.layer.addSublayer(bgLayer)
    fgLayer.lineWidth = 50
    fgLayer.fillColor = nil
    fgLayer.strokeStart = 0
    fgLayer.strokeEnd = 470 / 1590 - 0.008
    progressView.layer.addSublayer(fgLayer)
  }

  private func configure() {
    bgLayer.strokeColor = UIColor(rgb: 0xE3EDF7).cgColor
    fgLayer.strokeColor = UIColor(rgb: 0x18D4F4).cgColor
  }

  private func setupShapeLayer(shapeLayer: CAShapeLayer) {
    let linePath = UIBezierPath(arcCenter: CGPoint(x: progressView.bounds.midX, y: progressView.bounds.midY), radius: progressView.frame.height / 2, startAngle: 9/11 * CGFloat.pi, endAngle: 2/11 * CGFloat.pi, clockwise: true)
    linePath.lineWidth = 10
    shapeLayer.path = linePath.cgPath
  }

  override func draw(_ rect: CGRect) {
    super.draw(rect)
    boxView.addFloatEffect()
    continueButton.addFloatEffectForButton()
    bgLayer.addSankEffect()
    fgLayer.addFloatEffect()
  }

  @IBAction func continueButtonTapped(_ sender: UIButton) {
    onTap?()
  }
}

Solution

  • The cornerRadius only adjusts the corners of the bounds of the layer, not of the path. If you want path with rounded corners, you’ll have to stroke that yourself.

    There are two approaches to rendering an arc with a particular corner radius:

    1. If rendering a simple solid rendition, one very simple approach is to render two arcs, one clockwise, one counter clockwise, using different radii. The line width of these individual arcs should be twice the desired corner rounding of the final shape. Then, if you render these two arcs with a matching stroke color and fill color, you’ll get the desired shape.

      Here it is, animated so you can see what’s going on:

      enter image description here

      This is the code (without the animation):

      class ArcView: UIView {
          var startAngle: CGFloat = .pi * 3 / 4
          var endAngle: CGFloat = .pi * 5 / 4
          var clockwise: Bool = true
      
          /// Radius of center of this arc
          var radius: CGFloat = 100
      
          /// The linewidth of this thick arc
          var lineWidth: CGFloat = 50
      
          /// The corner radius of this thick arc
          var cornerRadius: CGFloat = 10
      
          static override var layerClass: AnyClass { return CAShapeLayer.self }
          var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer }
      
          override func layoutSubviews() {
              super.layoutSubviews()
              updatePath()
          }
      
          func updatePath() {
              let center = CGPoint(x: bounds.midX, y: bounds.midY)
              let innerRadius = radius - lineWidth / 2 + cornerRadius
              let innerAngularDelta = asin(cornerRadius / innerRadius) * (clockwise ? 1 : -1)
              let outerRadius = radius + lineWidth / 2 - cornerRadius
              let outerAngularDelta = asin(cornerRadius / outerRadius) * (clockwise ? 1 : -1)
      
              let path = UIBezierPath(arcCenter: center, radius: innerRadius, startAngle: startAngle + innerAngularDelta, endAngle: endAngle - innerAngularDelta, clockwise: clockwise)
              path.addArc(withCenter: center, radius: outerRadius, startAngle: endAngle - outerAngularDelta, endAngle: startAngle + outerAngularDelta, clockwise: !clockwise)
              path.close()
      
              // configure shapeLayer
      
              shapeLayer.lineWidth = cornerRadius * 2
              shapeLayer.fillColor = UIColor.blue.cgColor
              shapeLayer.strokeColor = UIColor.blue.cgColor
              shapeLayer.lineJoin = .round
              shapeLayer.path = path.cgPath
          }
      
      }
      

      The only trick in the above is how to adjust the starting and ending angles of these inner and outer arcs such that they’ll be perfectly circumscribed by the desired final shape. But a little trigonometry can be used to figure out those angular deltas, as shown above.

    2. The other approach is to define a path for the outline of the desired shape. Calculating the inner arc and outer arcs is similar, but you have to manually calculate the angles for the four rounded corners. It’s just a little trigonometry, but it’s a little hairy:

      class ArcView: UIView {
          var startAngle: CGFloat = .pi * 3 / 4
          var endAngle: CGFloat = .pi * 5 / 4
          var clockwise: Bool = true
      
          /// Radius of center of this arc
          var radius: CGFloat = 100
      
          /// The linewidth of this thick arc
          var lineWidth: CGFloat = 100
      
          /// The corner radius of this thick arc
          var cornerRadius: CGFloat = 10
      
          static override var layerClass: AnyClass { return CAShapeLayer.self }
          var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer }
      
          override func layoutSubviews() {
              super.layoutSubviews()
              updatePath()
          }
      
          func updatePath() {
              let center = CGPoint(x: bounds.midX, y: bounds.midY)
              let innerRadius = radius - lineWidth / 2
              let innerAngularDelta = asin(cornerRadius / (innerRadius + cornerRadius)) * (clockwise ? 1 : -1)
              let outerRadius = radius + lineWidth / 2
              let outerAngularDelta = asin(cornerRadius / (outerRadius - cornerRadius)) * (clockwise ? 1 : -1)
      
              let path = UIBezierPath(arcCenter: center, radius: innerRadius, startAngle: startAngle + innerAngularDelta, endAngle: endAngle - innerAngularDelta, clockwise: clockwise)
      
              var angle = endAngle - innerAngularDelta
              var cornerStartAngle = angle + .pi * (clockwise ? 1 : -1)
              var cornerEndAngle = endAngle + .pi / 2 * (clockwise ? 1 : -1)
              var cornerCenter = CGPoint(x: center.x + (innerRadius + cornerRadius) * cos(angle), y: center.y + (innerRadius + cornerRadius) * sin(angle))
              path.addArc(withCenter: cornerCenter, radius: cornerRadius, startAngle: cornerStartAngle, endAngle: cornerEndAngle, clockwise: !clockwise)
      
              angle = endAngle - outerAngularDelta
              cornerStartAngle = cornerEndAngle
              cornerEndAngle = endAngle - outerAngularDelta
              cornerCenter = CGPoint(x: center.x + (outerRadius - cornerRadius) * cos(angle), y: center.y + (outerRadius - cornerRadius) * sin(angle))
              path.addArc(withCenter: cornerCenter, radius: cornerRadius, startAngle: cornerStartAngle, endAngle: cornerEndAngle, clockwise: !clockwise)
      
              path.addArc(withCenter: center, radius: outerRadius, startAngle: endAngle - outerAngularDelta, endAngle: startAngle + outerAngularDelta, clockwise: !clockwise)
      
              angle = startAngle + outerAngularDelta
              cornerStartAngle = angle
              cornerEndAngle = startAngle - .pi / 2 * (clockwise ? 1 : -1)
              cornerCenter = CGPoint(x: center.x + (outerRadius - cornerRadius) * cos(angle), y: center.y + (outerRadius - cornerRadius) * sin(angle))
              path.addArc(withCenter: cornerCenter, radius: cornerRadius, startAngle: cornerStartAngle, endAngle: cornerEndAngle, clockwise: !clockwise)
      
              angle = startAngle + innerAngularDelta
              cornerStartAngle = cornerEndAngle
              cornerEndAngle = angle + .pi * (clockwise ? 1 : -1)
              cornerCenter = CGPoint(x: center.x + (innerRadius + cornerRadius) * cos(angle), y: center.y + (innerRadius + cornerRadius) * sin(angle))
              path.addArc(withCenter: cornerCenter, radius: cornerRadius, startAngle: cornerStartAngle, endAngle: cornerEndAngle, clockwise: !clockwise)
      
              path.close()
      
              // configure shapeLayer
      
              shapeLayer.fillColor = UIColor.blue.cgColor
              shapeLayer.strokeColor = UIColor.clear.cgColor
              shapeLayer.lineJoin = .round
              shapeLayer.path = path.cgPath
          }
      }