Last updated at 2023-09-09

Easy SwiftUI Progress Display using CALayer

AI Status
Last Edit By
Last edited time
Sep 9, 2023 05:05 PM
Sep 9, 2023
When you need a progress view in Swift you may want to use UIView to layout them.
Even though UIView can handle them, I suggest you to use CALayer to draw the progress.


LayerDrawer<ProgressDisplayLayer> { layer in layer.progressNumbers = [1,2,3] layer.currentProgressIndex = 1 } .frame(height: 50)
Get the layer drawer source code at here.

Full Code with Explanation Below

You can jump to Customization Point Note to adjust variable in your project
class ProgressDisplayLayer: CALayer { var progressNumbers: [Int] = [] { didSet { setNeedsDisplay() } } var currentProgressIndex = 0 { didSet { setNeedsDisplay() } } private var numberLayers: [ProgressNumberLayer] = [] override func layoutSublayers() { super.layoutSublayers() layoutLayers() } private func insertNumber(number: Int, xOffset: CGFloat, isActive: Bool) { let numberLayer = ProgressNumberLayer() numberLayer.bgColor = isActive ? UIColor.blue.cgColor : UIColor.red.cgColor numberLayer.number = number numberLayer.frame = CGRect(x: xOffset, y: 0, width: bounds.height, height: bounds.height) addSublayer(numberLayer) } private func insertLine(xOffset: CGFloat, width: CGFloat, isActive: Bool) { let line = ProgressLineLayer() let height: CGFloat = 2 line.frame = CGRect(x: xOffset, y: bounds.height / 2 - height / 2, width: width, height: height) addSublayer(line) line.lineColor = isActive ? .blue : .red } private func layoutLayers() { sublayers?.forEach({ $0.removeFromSuperlayer() }) let lineWidth: CGFloat = 60.0 let numberOfProgress = CGFloat(progressNumbers.count) let numberOfLine = numberOfProgress - 1 let drawingWidth = numberOfProgress * bounds.height + numberOfLine * lineWidth var xOffset = bounds.width / 2 - drawingWidth / 2 for (index, number) in progressNumbers.enumerated() { let isActive = index <= min(index, currentProgressIndex) if index > 0 { insertLine(xOffset: xOffset - lineWidth, width: lineWidth, isActive: isActive) } insertNumber(number: number, xOffset: xOffset, isActive: isActive) xOffset += bounds.height + lineWidth } } } final class ProgressNumberLayer: CALayer { var bgColor: CGColor = UIColor.blue.cgColor var fontSize: CGFloat = 20 { didSet { setNeedsDisplay() } } var number: Int = 1 { didSet { setNeedsDisplay() } } override func draw(in ctx: CGContext) { let centerX = bounds.midX let centerY = bounds.midY let radius = min(bounds.width, bounds.height) / 2 ctx.setFillColor(bgColor) ctx.addArc(center: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true) ctx.fillPath() } override func layoutSublayers() { super.layoutSublayers() if let existingTextLayer = sublayers?.first(where: { $0 is CATextLayer }) { existingTextLayer.removeFromSuperlayer() } let textLayer = CATextLayer() textLayer.string = "\(number)" textLayer.fontSize = fontSize textLayer.foregroundColor = UIColor.white.cgColor textLayer.alignmentMode = .center let centerX = bounds.midX let centerY = bounds.midY let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: fontSize), .foregroundColor: textLayer.foregroundColor! ] let textSize = (textLayer.string as! NSString) .size(withAttributes: attributes) let textX = centerX - textSize.width / 2 let textY = centerY - textSize.height / 2 textLayer.frame = .init( origin: .init(x: textX, y: textY), size: textSize) addSublayer(textLayer) } } final class ProgressLineLayer: CALayer { var lineColor: UIColor = .black { didSet { setNeedsDisplay() } } override func draw(in ctx: CGContext) { ctx.setFillColor(lineColor.cgColor) ctx.fill(bounds) } }
Let's break down the code and discuss customization points:


  • progressNumbers: An array of integers representing the progress numbers.
  • currentProgressIndex: An index that represents the current progress position.
  • numberLayers: An array to hold instances of ProgressNumberLayer.

Customization Points:

  • insertNumber(number: xOffset: isActive:): This method inserts a numbered segment with a specified number, x-offset, and activation state. You can customize the appearance of the segment here.
  • insertLine(xOffset: width: isActive:): This method inserts a line segment with a specified x-offset, width, and activation state. You can customize the appearance of the line segment here.
  • layoutLayers(): This method calculates the layout of the progress display based on the provided progress numbers and the current progress index. You can adjust the layout logic as needed.


  • bgColor: The background color of the progress number circle.
  • fontSize: The font size of the progress number text.
  • number: The integer value to display.

Customization Points:

  • draw(in ctx: CGContext): This method draws the circular background of the progress number. You can customize the drawing logic or appearance here.
  • layoutSublayers(): This method adds and positions the text layer that displays the progress number. You can adjust the text appearance and position here.


  • lineColor: The color of the progress line.

Customization Points:

  • draw(in ctx: CGContext): This method draws the progress line. You can customize the line appearance here.
Customization suggestions:
  • You can customize the background color of progress number circles by modifying the bgColor property in ProgressNumberLayer.
  • Adjust the font size and appearance of progress number text by modifying the attributes in layoutSublayers() of ProgressNumberLayer.
  • Customize the appearance of progress lines by adjusting the lineColor property in ProgressLineLayer.
  • Modify the layout and spacing of progress segments in layoutLayers() of ProgressDisplayLayer to achieve the desired visual representation of your progress display.
By leveraging these customization points, you can tailor the appearance and behavior of your progress display to match your specific requirements and design preferences.

Discussion (0)

Related Posts