SwiftUI:带动画栏的步进器

SwiftUI: stepper with animated bar

我想创建一个带动画栏的步进器组件。这是我得到的结果:

我的想法是该条应始终居中,而且我想在值更改时为蓝色条设置动画,但我无法使其正常工作。

这是我的代码:

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

以及我如何在容器视图中调用它:

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

你能帮我理解为什么动画不起作用吗? 另外,有没有更好的布局方式 UI?

谢谢!

最简单的解决方案是像这样更改 withAnimation 块中的 currentIndex

Button(action: {
    withAnimation {
        currentIndex += 1
    }
}, label: {
    Text("INCREMENT")
})

这里是固定的Stepper(用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
    }
}

此处修复了步进器与动画居中对齐的问题。还添加了对 INCREMENT - DECREMENT 值的验证。您现在可以在没有更新功能的情况下以您的方式使用它。目前,在您的代码中,这一行出现一个警告,这也已解决。

 .frame(width: .infinity, height: 18)

最终代码:

struct Stepper: View {
    @Binding var currentIndex: Int
    
    private var total: Int
    
    private var mainIndex: Int {
        if currentIndex >= 0 && currentIndex <= total {
            return currentIndex
            
        } else if currentIndex < 0 {
            DispatchQueue.main.async {
                self.currentIndex = 0
            }
            return 0
            
        } else {
            DispatchQueue.main.async {
                self.currentIndex = total
            }
            return total
        }
    }
    
    var body: some View {
        GeometryReader { geometry in
            HStack() {
                Text("\(mainIndex)")
                    .foregroundColor(.black)
                    .frame(width: 30)
                
                ZStack(alignment: .leading) {
                    Color.gray.opacity(0.4).frame(width: geometry.size.width - 60)
                    Color.blue.frame(width: (geometry.size.width - 60) / CGFloat(total) * CGFloat(mainIndex))
                }
                .animation(.default, value: mainIndex)
                .frame(width: geometry.size.width - 60, height: 2)
                
                Text("\(total)")
                    .foregroundColor(.black)
                    .frame(width: 30)
            }
            .frame(width: geometry.size.width, height: geometry.size.height)
            .background(Color.yellow)
            
        }
    }
    
    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
        }
    }
}

struct StepperVC: View {
    @State private var currentIndex: Int = 1
    
    var body: some View {
        VStack( alignment: .center,spacing: 32) {
            Stepper(withTotal: 8, andCurrentIndex: $currentIndex)
                .frame(width: UIScreen.main.bounds.size.width - 50, height: 50, alignment: .center)
            Button(action: {
                currentIndex += 1
            }, label: {
                Text("INCREMENT")
            })
            Button(action: {
                currentIndex -= 1
            }, label: {
                Text("DECREMENT")
            })
        }
    }
}