I'm working on a custom slider using a RoundedRectangle
for the track with another Rectangle
for the progress with a mask applied. This largely works fine as long as the thumb is not close to the end. As the thumb is dragged close to the right side edge, the RoundedRectangle
looks rounded at both ends. How can I fix it such that only the right end of the rounded rectangle shows as rounded ?
struct Slider: View {
@Binding var progress: CGFloat
private let stroke: CGFloat = 5.0
private var thumbWidth: CGFloat { stroke * 2 }
private var thumbHeight: CGFloat { thumbWidth * 3.5 }
var body: some View {
GeometryReader { geometry in
ZStack {
Color.black.ignoresSafeArea()
VStack {
let progressWidth = geometry.size.width * progress
let sliderWidth = geometry.size.width - thumbWidth
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: stroke)
.frame(height: stroke)
.foregroundStyle(Color.white.opacity(0.5))
.padding(.leading, progressWidth)
Rectangle()
.fill(Color.white.opacity(0.5))
.mask(alignment: .leading) {
Rectangle()
.frame(width: progressWidth)
}
Capsule()
.fill(.red)
.frame(width: thumbWidth, height: thumbHeight)
.offset(x: progress * (geometry.size.width - thumbWidth))
.gesture(
DragGesture()
.onChanged { gesture in
let newProgress = min(max(gesture.location.x / sliderWidth, 0), 1)
progress = newProgress
}
)
}
.frame(height: thumbHeight)
}
}
}
}
}
struct SliderView: View {
@State private var progress: CGFloat = 0.0
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
Slider(progress: $progress)
}
}
}
Your calculations are a bit off. Try adding .opacity(0.3)
to the Capsule
and see that the horizontal center of the thumb is not always where the two other rectangles "meet".
I assume you want the horizontal center of the thumb to be where the two other rectangles meet. So you need to add an extra thumbWidth / 2
to their width/padding.
progressWidth
should be calculated from sliderWidth
not geometry.size.width
, because sliderWidth
is the total length across which the thumb can move.
let sliderWidth = geometry.size.width - thumbWidth
let progressWidth = sliderWidth * progress
Then add/subtract thumbWidth / 2
where appropriate:
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: stroke)
.frame(height: stroke)
.foregroundStyle(Color.white.opacity(0.5))
// this rectangle will at least have a width of half the thumb
.padding(.leading, progressWidth + thumbWidth / 2)
Rectangle()
.fill(Color.white.opacity(0.5))
// this does not need to be a mask at all - you can just apply .frame to the rectangle directly
// I'll assume in your real code this is not a simple Rectangle
.mask(alignment: .leading) {
Rectangle()
// this rectangle will at least be half the thumb width away from the leading side
.frame(width: progressWidth + thumbWidth / 2)
}
Capsule()
.fill(.red)
.frame(width: thumbWidth, height: thumbHeight)
.offset(x: progressWidth)
.gesture(
DragGesture()
.onChanged { gesture in
// the actual slider starts at x = thumbWidth / 2, so the x coordinate needs to be shifted here
let newProgress = min(max((gesture.location.x - thumbWidth / 2) / sliderWidth, 0), 1)
progress = newProgress
}
)
}
.frame(height: thumbHeight)