iosswiftswiftuidynamicdynamic-island

How do I only draw the top part of a circumference?


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)

Solution

  • It takes a little trigonometry to calculate the angles and the circle’s center which would be appropriate for an arc to be inscribed/rendered within some rectangle bounds.

    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 an inscribed within a rectangle that is shorter than it is wide, the center of the circle associated with this arc 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

    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 z = sqrt(x * x + y * y)
            let phi = atan2(x, y)
    
            let radius = z / 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()
        }
    }
    

    It is not terribly important, but if you want to see how I calculated 𝜃 (theta) and r (radius) for my arc/circle, I first introduced a chord from the lower-left of the arc to the top-center, I then calculated the chord’s length, z, from the values x and y. I also calculated the angle between the chord and the center of the arc’s circle, 𝜙 (phi). Given z and 𝜙, I then calculated r and 𝜃. So, with apologies for the hand-drawn diagram, this is a visualization of the variables within my Arc code:

    trigonometry visualization