swiftuistepperuistepper

SwiftUI: stepper with animated bar


I would like to create a stepper component with an animated bar. Here is the result I get:

enter image description here

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!


Solution

  • Update: Xcode 13.4 / iOS 15.5

    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 !!
    

    Test module is here

    Original

    Here is fixed Stepper (tested with Xcode 12.1 / iOS 14.1)

    demo

    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
        }
    }