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.
Usage
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:
ProgressDisplayLayer
progressNumbers
: An array of integers representing the progress numbers.
currentProgressIndex
: An index that represents the current progress position.
numberLayers
: An array to hold instances ofProgressNumberLayer
.
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.
ProgressNumberLayer
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.
ProgressLineLayer
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 inProgressNumberLayer
.
- Adjust the font size and appearance of progress number text by modifying the attributes in
layoutSublayers()
ofProgressNumberLayer
.
- Customize the appearance of progress lines by adjusting the
lineColor
property inProgressLineLayer
.
- Modify the layout and spacing of progress segments in
layoutLayers()
ofProgressDisplayLayer
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.