skip to Main Content

I’m trying to implement a circular progress view that looks like so:

enter image description here

struct SpeedometerView: View {
    var body: some View {
        ZStack {
            Color.black.edgesIgnoringSafeArea(.all)
            
            speedProgressView(width: 300)
        }
    }
    
    private func speedProgressView(width: CGFloat) -> some View {
        ZStack {
            Circle()
                .trim(from: 0, to: 0.2)
                .stroke(.red, style: StrokeStyle(lineWidth: 4, lineCap: .round))
                .shadow(color: .green, radius: 10, x: 0, y: 0)
                .shadow(color: .green, radius: 2, x: 0, y: 0)
        }
        .frame(width: width)
        .rotationEffect(.degrees(100))
    }
}

#Preview {
    SpeedometerView()
}

Both ends are rounded as of now due to the stroke style. How do I trim/clip only the circle end that progresses clockwise unless progress is 100% ?
Any help is appreciated.

2

Answers


  1. If you want only one end of the line to be .round and the other to be .butt. So you need Double Circle to do it. I hope it helps

    struct SpeedometerView: View {
        var body: some View {
            ZStack {
                Color.black.edgesIgnoringSafeArea(.all)
                speedProgressView(width: 100)
            }
        }
        
        private func speedProgressView(width: CGFloat) -> some View {
            ZStack {
                
                Circle()
                    .trim(from: 0, to: 0.1)
                    .stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .round))
                    .shadow(color: .green, radius: 10, x: 0, y: 0)
                    .shadow(color: .green, radius: 2, x: 0, y: 0)
                
                
                Circle()
                    .trim(from: 0.1, to: 0.2)
                    .stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .butt))
                    .shadow(color: .green, radius: 10, x: 0, y: 0)
                    .shadow(color: .green, radius: 2, x: 0, y: 0)
            }
            .frame(width: width)
            .rotationEffect(.degrees(100))
        }
    }
    
    Login or Signup to reply.
  2. I would write this as a Shape that is filled, instead of a Path that is stroked.

    The path of the Shape is the union of:

    • a path created by the strokedPath of a (trimmed) circular path, with the .butt line cap.
    • a circular path with the radius of the line width, at where the trimmed path ends.
    struct OneEndRoundedCircle: Shape {
        var trim: CGFloat
        let lineWidth: CGFloat
        
        func path(in rect: CGRect) -> Path {
            let radius = min(rect.width, rect.height) / 2
            let center = CGPoint(x: rect.midX, y: rect.midY)
            let circlePath = Path(
                ellipseIn: CGRect(origin: center, size: .zero)
                    .insetBy(dx: -radius, dy: -radius)
            ).trimmedPath(from: 0, to: trim)
            let stroked = circlePath.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .butt))
    
            // this is where we want the rounded end to be. 
            // If your path starts somewhere else, adjust the angle accordingly
            let roundedPoint = center.applying(.init(
                translationX: radius * sin(.pi / 2),
                y: radius * cos(.pi / 2)
            ))
            let littleCircle = Path(
                ellipseIn: .init(origin: roundedPoint, size: .zero)
                    .insetBy(dx: -lineWidth / 2, dy: -lineWidth / 2)
            )
            return stroked.union(littleCircle)
        }
    }
    
    // Animatable conformance in case you want to animate the amount trimmed
    extension OneEndRoundedCircle: Animatable {
        var animatableData: CGFloat {
            get { trim }
            set { trim = newValue }
        }
    }
    
    private func speedProgressView(width: CGFloat) -> some View {
        ZStack {
            // intentionally increased lineWidth to make the rounded end more visible
            OneEndRoundedCircle(trim: 0.2, lineWidth: 10)
                .fill(.red)
                .shadow(color: .green, radius: 10, x: 0, y: 0)
                .shadow(color: .green, radius: 2, x: 0, y: 0)
        }
        .frame(width: width)
        .rotationEffect(.degrees(100))
    }
    

    Example output:

    Example output

    For iOS 17 or earlier, union is not available, but you can just work with CGPaths instead, and use its union method instead.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search