I'm trying to create a custom progress bar like so (ignore the thumb for now):
At the moment I'm able to gradually reveal the wave using another rectangle applied as a mask. I'm not sure how to clip the left portion of the line as the progress increases. I tried setting the wave view with its mask as a mask to the line but that didn't work. Should I update the width of the line based on the progress or is it possible to use a mask in a way such that, as the wave starts to turn visible from the leading edge, the line shrinks towards the trailing edge? Please advise on an efficient way to do this. Any help is appreciated.
struct Line: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.height/2))
path.addLine(to: CGPoint(x: rect.width, y: rect.height/2))
return path
}
}
struct Wave: Shape {
var strength: Double
var frequency: Double
var phase: Double
var animatableData: Double {
get { phase }
set { self.phase = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = Double(rect.width)
let height = Double(rect.height)
let midHeight = height / 2
let wavelength = width / frequency
let firstX = 0.0
let firstRelativeX = firstX / wavelength
let firstSine = sin(firstRelativeX + phase)
let firstY = strength * firstSine + midHeight
path.move(to: CGPoint(x: firstX, y: firstY))
for x in stride(from: 0.0, through: width, by: 1) {
let relativeX = x / wavelength
let sine = sin(relativeX + phase)
let y = strength * sine + midHeight
path.addLine(to: CGPoint(x: x, y: y))
}
return path
}
}
struct ContentView: View {
@State private var phase = 0.0
@State private var progress = 0.0
var body: some View {
VStack {
GeometryReader(content: { geometry in
ZStack {
Line()
.stroke(Color.white.opacity(0.5), lineWidth: 5.0)
.frame(width: geometry.size.width)
Wave(strength: 10, frequency: 30, phase: phase)
.stroke(Color.white, lineWidth: 5)
.mask(
Rectangle()
.frame(width: (geometry.size.width * progress), height: geometry.size.height)
.padding(.trailing, geometry.size.width * (1 - progress))
)
}
})
Slider(value: $progress, in: 0...1)
.padding()
}
.background(Color.black.ignoresSafeArea())
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.phase = .pi * 2
}
}
}
}
A Shape
is greedy and consumes as much space as available. So your code can be simplified by applying a fixed width only where it is actually needed. Then, you don't need to fix the width of the line, you just need to add padding.
Also, if you provide an alignment
parameter to the .mask
then the padding on the mask can be avoided too.
GeometryReader { geometry in
let progressWidth = geometry.size.width * progress
ZStack {
Line()
.stroke(Color.white.opacity(0.5), lineWidth: 5.0)
.padding(.leading, progressWidth)
Wave(strength: 10, frequency: 30, phase: phase)
.stroke(Color.white, lineWidth: 5)
.mask(alignment: .leading) {
Rectangle()
.frame(width: progressWidth)
}
}
}