SantiOch
SantiOch

Reputation: 47

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)

Upvotes: 1

Views: 119

Answers (2)

Rob
Rob

Reputation: 438122

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

Upvotes: 1

Duncan C
Duncan C

Reputation: 131481

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

Upvotes: 0

Related Questions