I would like to create a stepper component with an animated bar. Here is the result I get:
The idea is that the bar should always be centered, and also I would like to animate the blue bar when the value changes, but I can't get it working.
Here is my code:
struct Stepper: View {
@Binding var currentIndex: Int
var total: Int
var body: some View {
ZStack(alignment: .center) {
ZStack(alignment: .leading) {
Color.gray.opacity(0.4)
Color.blue
.frame(width: 175.5 / CGFloat(total) * CGFloat(currentIndex))
}
.frame(width: 175.5, height: 2)
Text("\(currentIndex)")
.foregroundColor(.black)
.offset(x: -113)
Text("\(total)")
.foregroundColor(.black)
.offset(x: 113)
}
.frame(width: .infinity, height: 18)
}
init(withTotal total: Int,
andCurrentIndex currentIndex: Binding<Int>) {
self._currentIndex = currentIndex
self.total = total
}
func update(to value: Int) {
guard value >= 0, value <= total else {
return
}
withAnimation {
currentIndex = value
}
}
}
And how I call this in a container view:
struct StepperVC: View {
@State private var currentIndex: Int = 1
var body: some View {
VStack(spacing: 32) {
Stepper(withTotal: 8, andCurrentIndex: $currentIndex)
Button(action: {
currentIndex += 1
}, label: {
Text("INCREMENT")
})
Button(action: {
currentIndex -= 1
}, label: {
Text("DECREMENT")
})
}
}
}
Could you help me understanding why the animation doesn't work? Also, is there a better way to layout the UI?
Thank you!
According to Binding
animatable concept for custom controls (to give possibility for control's users to manage if control behavior should be animatable or not) Stepper
should handle
Stepper(withTotal: 8, andCurrentIndex: $currentIndex.animation(.default))
and so animatable part be like
ZStack(alignment: .leading) {
Color.gray.opacity(0.4)
Color.blue
.frame(width: 175.5 / CGFloat(total) * CGFloat(currentIndex))
}
.frame(width: 175.5, height: 2)
.animation(_currentIndex.transaction.animation, value: currentIndex) // << here !!
Here is fixed Stepper
(tested with Xcode 12.1 / iOS 14.1)
struct Stepper: View {
@Binding var currentIndex: Int
var total: Int
var body: some View {
ZStack(alignment: .center) {
ZStack(alignment: .leading) {
Color.gray.opacity(0.4)
Color.blue
.frame(width: 175.5 / CGFloat(total) * CGFloat(currentIndex))
}
.frame(width: 175.5, height: 2)
.animation(.default, value: currentIndex) // << here !!
Text("\(currentIndex)")
.foregroundColor(.black)
.offset(x: -113)
Text("\(total)")
.foregroundColor(.black)
.offset(x: 113)
}
.frame(width: .infinity, height: 18)
}
init(withTotal total: Int,
andCurrentIndex currentIndex: Binding<Int>) {
self._currentIndex = currentIndex
self.total = total
}
}