I know there are some similar questions like this, but in my case, I want to set cornerRadius = 8 for my layer.
If I set:
shapeLayer.lineCap = CAShapeLayerLineCap.round
it shows like this:
-> 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?()
}
}
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:
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:
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.
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
}
}