PIXEL
DOCK

I like the smell of Swift in the morning…

A shortcut to drawing an arc with SwiftUI

Posted: | Author: | Filed under: Swift, SwiftUI | Tags: , | No Comments »

You have to draw an arc or a semicircle in SwiftUI. The most obvious way to do that is to use a Path. In this blog post you can see how to do that, but it is really not rocket science.

Turns out there is an even quicker and easier way to draw a fraction of a circle. SwiftUI’s Shapes have a method called trim(from:to:) that allows you the specify what fraction of a Shape’s path you want to draw. If you want to draw a semi circle you could call that method like this:

Circle()
    .trim(from: 0.0, to: 0.5)
    .stroke(Color("pdCyan"), lineWidth: 10)
    .frame(width: 100, height: 100)

Which would give you this:

By varying the parameters for from and to you can get almost all variations of an arc:

ZStack {
    Circle()
        .trim(from: 0.0, to: 0.25)
        .stroke(Color.blue, lineWidth: 10)
        .frame(width: 70, height: 70)
    Circle()
       .trim(from: 0.20, to: 0.75)
       .stroke(Color.green, lineWidth: 10)
       .frame(width: 100, height: 100)
    Circle()
       .trim(from: 0.65, to: 1.0)
       .stroke(Color.red, lineWidth: 10)
       .frame(width: 130, height: 130)
}

Which results in this:

As you can see the path of a Circle shape starts at the right center (or 03:00 hours on a clock). Because the value that you pass as from parameter has to be smaller than the value for the to parameter, you cannot draw a semicircle from top to bottom like this because that would mean that you would have to set from to 0.75 and to to 0.25. That is not allowed.

With a little trick you can still achieve that. You simply draw a semicircle from 0.0 to 0.5 and then rotate the shape by -90 degrees. IMHO still simpler that using a Path for this.

Circle()
    .trim(from: 0.0, to: 0.5)
    .stroke(Color.orange, lineWidth: 10)
    .rotationEffect(.degrees(-90))
    .frame(width: 100, height: 100)

You can use this trim method on all SwiftUI’s built-in Shapes. For example if you want to draw a right triangle with SwiftUI you can do this:

Rectangle()
    .trim(from: 0.0, to: 0.5)
    .fill(Color("pdMagenta"))
    .frame(width: 120, height: 80)

Of course you could easily draw a Path for this, but I think the trim method is a pretty cool tool that you can use for drawing semicircles, quarter-circles or triangles.

Clocks run backwards in a SwiftUI world (aka how to draw an arc with SwiftUI)

Posted: | Author: | Filed under: iOS, Swift, SwiftUI | Tags: , | No Comments »

Sometimes you have to draw something on the screen that is not a full circle but only a part of the circle. Something like this:

The obvious way to do this would be to create a custom Arc Shape like this:

struct Arc: Shape {
    var startAngle: Angle
    var endAngle: Angle
    var clockwise: Bool

    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.addArc(center: CGPoint(x: rect.midX, y: rect.midY),
                    radius: rect.width / 2,
                    startAngle: startAngle,
                    endAngle: endAngle,
                    clockwise: clockwise)
        return path
    }
}

Now we need to let SwiftUI know what kind of arc we want to draw. As a human I would describe the arc as starting at 90 degrees and ending at 180 degrees, because we are usually assuming that 0 degrees is at the top of a circle.

SwiftUI begs to differ. In SwiftUI 0 degrees means straight to the right. In other words:

Human 90 degrees == SwiftUI 0 degrees

Because of that we need to draw and arc from 0 degrees to 90 degrees. (I add a gray background, so we can see the frame of the arc)

struct ContentView: View {
    var body: some View {
        Arc(startAngle: .degrees(0),
            endAngle: .degrees(90),
            clockwise: true
        )
        .stroke(Color("pdMagenta"), lineWidth: 12)
        .frame(width: 200, height: 200)
    }
}

Seems pretty straightforward, but the result is a not what I would expect:

This is where things get wild. According to Apple’s docs SwiftUI (and UIKit) use a flipped coordinate system.

To explain what that means let’s have look at these two illustrations:

In a Cartesian coordinate system (that we humans are used to) increasing values on the Y-axis go up to the top. Resulting in the “clockwise” that we are used to.

In SwiftUI’s flipped coordinate system increasing values on the Y-axis are going down to the bottom of the canvas. And that results in a reversed “clockwise” direction. IMHO that is really counterintuitive, but that’s the way it is 🤷‍♂️
This is an excerpt from the Apple docs:
“In a flipped coordinate system … specifying a clockwise arc results in a counterclockwise arc”

In other words: Clocks run backwards in a SwiftUI context.

To get the arc we want we have to toggle the clockwise parameter in our custom ArcShape:

struct Arc: Shape {
    var startAngle: Angle
    var endAngle: Angle
    var clockwise: Bool

    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.addArc(center: CGPoint(x: rect.midX, y: rect.midY),
                    radius: rect.width / 2,
                    startAngle: startAngle,
                    endAngle: endAngle,
                    clockwise: !clockwise)
        return path
    }
}

And now we get want we wanted in the first place: