Greetings, traveler!
SwiftUI ProgressView is a valuable tool for displaying progress. But sometimes, we must create something custom to gain complete control over the UI. We can create a custom progress bar with masking.
Note
By the way, we have already discussed the SwiftUI progress bar with masking. There, we discussed using an image to change a progress bar shape. You can read more about it here.
Let’s create some mock views and two @State
variables:
- Progress value
- Progress bar value
We will use the second one to store progress value, calculated with a view frame.
struct ContentView: View {
@State private var progress: CGFloat = .zero
@State private var progressBarValue: CGFloat = .zero
var body: some View {
VStack {
Text(progress, format: .percent.precision(.fractionLength(.zero)))
.frame(maxWidth: .infinity, minHeight: 80)
Slider(value: $progress, in: 0...1)
.padding()
}
.padding()
}
}
After that, create a new View
. Here, we will use GeometryReader
and two rectangles stored inside the ZStack
.
var progressBar: some View {
GeometryReader { geometry in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.green)
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.gray)
}
}
}
Now, we need to calculate the progress bar value. We will use GeometryReader
to do this.
var progressBar: some View {
GeometryReader { geometry in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.green)
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.gray)
.mask {
Rectangle()
.offset(x: -progressBarValue)
}
}
}
}
This value can be used to create a mask. Since we are using ZStack
here, these two rectangles overlay one another. We will use a mask to partially or fully reveal one of them.
var progressBar: some View {
GeometryReader { geometry in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.green)
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.gray)
.mask {
Rectangle()
.offset(x: -progressBarValue)
}
}
.onChange(of: progress) { _, newValue in
progressBarValue = -geometry.size.width * newValue
}
}
}
Now, we can use it. Check out the full code:
struct ContentView: View {
@State private var progress: CGFloat = .zero
@State private var progressBarValue: CGFloat = .zero
var body: some View {
VStack {
Text(progress, format: .percent.precision(.fractionLength(.zero)))
.frame(maxWidth: .infinity, minHeight: 80)
.background {
progressBar
}
Slider(value: $progress, in: 0...1)
.padding()
}
.padding()
}
var progressBar: some View {
GeometryReader { geometry in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.green)
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.gray)
.mask {
Rectangle()
.offset(x: -progressBarValue)
}
}
.onChange(of: progress) { _, newValue in
progressBarValue = -geometry.size.width * newValue
}
}
}
}
Cool!
Text masking
We can also change the text color with a mask to ensure it is always visible. We will use the same approach: creating two views and revealing one with a mask.
import SwiftUI
struct ContentView: View {
@State private var progress: CGFloat = .zero
@State private var progressBarValue: CGFloat = .zero
var body: some View {
VStack {
progressBar
.frame(maxWidth: .infinity, maxHeight: 80)
Slider(value: $progress, in: 0...1)
.padding()
}
.padding()
}
var progressBar: some View {
GeometryReader { geometry in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.green)
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.gray)
.mask {
Rectangle()
.offset(x: -progressBarValue)
}
Text("Some text here...")
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
Text("Some text here...")
.foregroundStyle(.black)
.frame(maxWidth: .infinity)
.mask {
Rectangle()
.offset(x: -progressBarValue)
}
}
.onChange(of: progress) { _, newValue in
progressBarValue = -geometry.size.width * newValue
}
}
}
}
Conclusion
This simple example opens up opportunities for more flexible customization. You can find the source code here.
If you enjoyed this article, please feel free to follow me on my social media: