Modifier Monday: .trim(from:to:)
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:
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:
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:
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:
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:
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 ✌️.
-
This one has a quarter of it looped off due to the 0.25 from: value. ↩