[SC]()

iOS. Apple. Indies. Plus Things.

Modifier Monday: .trim(from:to:)

// Written by Jordan Morgan // Feb 14th, 2022 // Read it in about 4 minutes // RE: SwiftUI

This post is brought to you by Emerge Tools, the best way to build on mobile.

Welcome to Modifier Monday! Today’s modifier is .trim(from:to:), and its docs are here:

/// Trims this shape by a fractional amount based on its representation as a
/// path.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)

@inlinable public func trim(from startFraction: CGFloat = 0, to endFraction: CGFloat = 1) -> some Shape

Unlike last week’s modifier, this one is an extension off of Shape instead of the catch-all View type. As such, it’ll result in a Shape type as well. If that doesn’t really mean much to you, here’s a quick example of its implications:

// Valid
var body: some View {
    Circle()
        .trim()
        .frame(width:10, height:10)
}

// Invalid
var body: some View {
    Circle()
        .frame(width:10, height:10)
        .trim() // trim() isn't wrapping a `Shape` type here.
}

Let’s see it in action.

Examples

So, trim() will take a shape, and using its two parameters (from and to, respectively) it will draw a portion of the shape. To learn how this works in practice, I just started with a plain Circle and started tacking trim() on it, trying to anticipate what it would do.

For example:

var body: some View {
    Circle()
        .trim(from: 0, to: 1)
        .fill(Color.blue)
}

Any ideas on what that would produce?

If you guessed absolutely nothing, you guessed right! It’s an anticlimactic example to start with, but it’s this very code sample that led me to understand trim() better than anything. Because when you realize that from is basically theStartOfTheShape and to is essentially theEndOfTheShape, you can conclude why this just draws the whole Circle().

You can read it like this:

// Don't cut off anything from the start. Then draw the whole thing to the end.
.trim(from: 0, to: 1)

So, by the same token — if we wanted to draw half of a shape, we could do it using 0.5 for the first argument and stick with our 1 for the latter one:

var body: some View {
    Circle()
        .trim(from: 0.5, to: 1)
        .fill(Color.blue)
}

And sure enough:

Circle clipped using the trim modifier.

If we wanted to flip it the other way, we could easily by flipping around where we start and end:

var body: some View {
    Circle()
        .trim(from: 0.0, to: 0.5)
        .fill(Color.blue)
}

The same as before, but now it’s just flippy flipped:

Circle clipped, but this time flipped horizontally on its axis, using the trim modifier.

Another thing we can infer from this is that SwiftUI starts a Circle type’s path around the middle-right point of the shape. You can see this for yourself by copying any of these aforementioned examples and tweaking around the from value a bit.

That’s important, because you need to have an idea of where the path begins to know how to leverage trim() to get the results you want.

Speaking of - how about using our newfound trimming abilities to make a compass?

Let’s tweak what we’ve got a little bit to get a sharper angle:

Circle()
    .trim(from: 0.6, to: 1)
    .fill(Color.red)

Netting us this:

A Circle that's clipped to a wedge shape.

Now, we can toss a few more of those same shapes, albeit a little offset from the last one, in a ZStack to make a compass looking thingy. I suck at math, so color me not surprised if there’s a better way to do this — but this approach works:

struct Compass: View {
    @Binding var rotation: Double  
    private let delta = 0.78
    
    var body: some View {
        ZStack {
            Circle()
                .trim(from: 0.6, to: 1)
                .fill(Color.white)
            Circle()
                .trim(from: 0.6, to: 1)
                .rotationEffect(.radians(rotation))
            Circle()
                .trim(from: 0.6, to: 1)
                .rotationEffect(.radians(rotation + (delta * 1)))
            Circle()
                .trim(from: 0.6, to: 1)
                .rotationEffect(.radians(rotation + (delta * 2)))
            Circle()
                .trim(from: 0.6, to: 1)
                .rotationEffect(.radians(rotation + (delta * 3)))
            Circle()
                .trim(from: 0.6, to: 1)
                .rotationEffect(.radians(rotation + (delta * 4)))
            Circle()
                .trim(from: 0.6, to: 1)
                .rotationEffect(.radians(rotation + (delta * 5)))
        }
        .scaleEffect(0.6)
    }
}

And then pop a little direction Text views in there:

struct ContentView: View {
    @State private var bottomDegrees = 0.78
    var body: some View {
        VStack {
            ZStack {
                Compass(rotation: $bottomDegrees)
                Text("N")
                    .offset(y: -140)
                Text("E")
                    .offset(x: 140)
                Text("S")
                    .offset(y: 140)
                Text("W")
                    .offset(x: -140)
                Text("")
            }
            .font(.title)
            Spacer()
            Text("\(bottomDegrees)")
            Slider(value: $bottomDegrees, in: 0.0...180.0)
        }
    }
}

And BAM:

A bunch of clipped Circles stacked together making a compass.

Very cool. Before we sign off, may I remind you that some of Cupertino & Friends™️ best work is often found in header files. It’s no different with trim, check out this in-depth example they have:

Path { path in
    path.addLines([
        .init(x: 2, y: 1),
        .init(x: 1, y: 0),
        .init(x: 0, y: 1),
        .init(x: 1, y: 2),
        .init(x: 3, y: 0),
        .init(x: 4, y: 1),
        .init(x: 3, y: 2),
        .init(x: 2, y: 1)
    ])
}
.trim(from: 0.25, to: 1.0)
.scale(50, anchor: .topLeading)
.stroke(Color.black, lineWidth: 3)

That yields an infinity symbol (that sideways “8”) shape, and it too shows off how trim() works in a nice, demonstrable1 way, especially if you tweak the values in its arguments to see how it changes. In the case of the code above, they’ve indicated they want the first quarter of the shape trimmed off, and that’s exactly what we get:

A bunch of clipped Circles stacked together making a compass.

So that’s trim(). A great modifier with an apt-name (unlike the somewhat inscrutable implications from last week’s name 😅) and some fun use cases.

Until next time ✌️.

  1. This one has a quarter of it looped off due to the 0.25 from: value. 

···

Spot an issue, anything to add?

Reach Out.