skip to Main Content

Im trying to draw an arc in SwiftUI, im practicing and I want to make this view (see in the picture) from the apple website where it shows how to implement Dynamic Island live activities.

This is the view im trying to replicate

I have tried using path but im not sure how to only draw an arc and not a half circle like my code does.

Here is the code using Path:

  struct ArcShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

        let center = CGPoint(x: rect.midX, y: rect.midY)
        let radius: CGFloat = 100
        let startAngle = Angle(degrees: 180)
        let endAngle = Angle(degrees: 0) 
        path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
        
        return path
    }
}

And here is a closer approach usingCircle and trimming it, but I don’t know how to "mush it" and make it flatter, and also round the corners, I’ve tried using .cornerRadius and making the frame wide and not tall but I didn’t see any result, only the circle adjusting to the smaller size on the frame:

Circle()
   .trim(from: 0.55, to: 0.95)
   .stroke(.linearGradient(colors: [.blue, .cyan],
                          startPoint: .leading,
                          endPoint: .trailing), lineWidth: 5)

2

Answers


  1. Paths use angles that start at zero on the right (east in compass directions) and increase as you go clockwise.

    You created an arc that started at 180 degrees (due west) and went to 0 degrees (due east) drawing counter-clockwise. That drew a half circle.

    If you want to draw less of the circle, add some offset the starting angle and subtract the same amount from the ending angle. So, as Paulw11 suggested, try 180+40 = 220 degrees for the left (west) side of your arc, and 0-40 = -40, or 360-40 = 320 degrees for the ending, right side of your arc.

    This code:

    struct ArcShape: Shape {
        var radius: CGFloat // The circle radius to use. Bigger numbers make a flatter but bigger arc
        var arcOffset: CGFloat // The number of degrees above the center to start the left and right of the arc.
        
      func path(in rect: CGRect) -> Path {
          var path = Path()
          let center = CGPoint(x: rect.midX, y: rect.midY)
          let startAngle = Angle(degrees: 180 + arcOffset)
          let endAngle = Angle(degrees: 0 - arcOffset)
          path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
          
          return path
      }
    }
    
    struct ContentView: View {
        var body: some View {
            // GeometryReader lets us learn things like the size of the screen
            GeometryReader { proxy in
                VStack(alignment: .center) {
                    Spacer()
                        .frame(height: 100)
                    let inset: CGFloat = 10
                    let frameSize = proxy.size.width - inset * 2
                    // Draw 2 arcs on top of each other.
                    ZStack {
                        // The first blue arc will be from
                        // 180-30 = 210° to
                        // 360-30 = 330°
                        ArcShape(radius: frameSize / 2 - 20, arcOffset: 30)
                            .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
                            .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.7))
                            .frame(width: frameSize, height: frameSize)
                        // The second cyan arc will be from
                        // 180-70 = 110° to
                        // 360-70 = 190°
                        ArcShape(radius: frameSize / 2 - 20, arcOffset: 70)
                            .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
                            .foregroundColor(.cyan)
                            .frame(width: frameSize, height: frameSize)
                    }
                    .border(Color.green, width: 1) //Remove if you don't want a border
                    .padding(.leading, inset)
    
                }
            }
        }
    }
    
    #Preview {
        ContentView()
    }
    

    Yields an image that looks like this:

    enter image description here

    Login or Signup to reply.
  2. It takes a little trigonometry to calculate the angles and the circle’s center which would be appropriate for an arc to be rendered within some rectangle.

    For example, let us imagine that you want to render an arc within this blue rectangle. So we need to figure out where the center of the circle associated with the arc that will be inside the rectangle. As a result, because the arc is a little “mushed”, the center of the associated circle will actually fall outside of the rectangle. So, in the following diagram, the blue rectangle is where I want the arc to be, the dotted black line illustrates the center and radius of the arc, and, obviously, the red line is the actual arc we end up stroking. (You obviously will not stroke the rectangle or the dotted black lines: Those are there for illustrative purposes only.)

    illustration of what is going on

    Or, in you example (omitting the rectangle and dotted line that were merely in the above diagram to illustrate what was going on):

    OP’s arc

    If you need to see the trigonometry encapsulated by the above, let me know, but I do not really want to lose anyone with the math.

    Anyway, the second image, above, was generated with:

    struct Arc: Shape {
        /// Percent
        ///
        /// How much of the Arc should we draw? `1` means 100%. `0.5` means half. Etc.
        let percent: CGFloat
    
        /// Line width
        ///
        /// How wide is the line going to be stroked. This is used to offset the arc within the `CGRect`.
        let lineWidth: CGFloat
    
        init(percent: CGFloat = 1, lineWidth: CGFloat = 1) {
            self.percent = percent
            self.lineWidth = lineWidth
        }
    
        func path(in rect: CGRect) -> Path {
            let rect = rect.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
            var path = Path()
    
            let x = rect.width / 2
            let y = rect.height
            let w = sqrt(x * x + y * y)
            let phi = atan2(x, y)
    
            let radius = w / 2 / cos(phi)
            let center = CGPoint(x: rect.minX + x, y: rect.minY + radius)
    
            let theta = 2 * (.pi / 2 - phi) * percent
    
            let startAngle = Angle(radians: 3 * .pi / 2 - theta)
            let endAngle = Angle(radians: 3 * .pi / 2 + theta)
            path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
    
            return path
        }
    }
    

    And I just used a rectangle that was ¼ as tall as it was wide:

    struct ContentView: View {
        var body: some View {
            GeometryReader { geometry in
                ZStack {
                    Arc(lineWidth: 20)
                        .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
                        .foregroundStyle(Color.blue)
                        .frame(width: geometry.size.width, height: geometry.size.width / 4, alignment: .center)
                    Arc(percent: 0.2, lineWidth: 20)
                        .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
                        .foregroundStyle(Color.cyan)
                        .frame(width: geometry.size.width, height: geometry.size.width / 4, alignment: .center)
    //                Rectangle()
    //                    .stroke(style: StrokeStyle(lineWidth: 1, lineCap: .round))
    //                    .foregroundStyle(Color.blue)
    //                    .frame(width: geometry.size.width, height: geometry.size.width / 4, alignment: .center)
                }
            }
            .padding()
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search