SwiftUI RollingCounter组件动画异常疑问:底层文本类型为何会影响VStack的offset动画表现?
SwiftUI RollingCounter组件动画异常疑问:底层文本类型为何会影响VStack的offset动画表现?
我最近在开发一个SwiftUI的RollingCounter组件,想实现数字滚动切换的动画效果——就是数值变化时,每个数字像滚轮一样上下滚动切换,而不是生硬跳转。
组件实现思路
我的实现逻辑是这样的:
- 把要显示的数值通过格式化拆成单个字符(比如把"1.24"拆成["1", ".", "2", "4"])
- 对每个数字字符,创建一个包含0到9所有数字的
VStack - 根据当前要显示的数字,通过
.offset(y:)来调整这个VStack的位置,让目标数字刚好显示在占位文本的位置 - 非数字的字符(比如小数点)就直接显示对应的文本
遇到的奇怪问题
最开始我用固定的Text("X")作为占位文本,用来测量每个字符的高度,这时候动画完全正常——VStack会平滑地上下滚动,数字切换很丝滑。
但为了让每个字符的宽度适配实际内容(比如"1"比"X"窄,小数点更窄),我把占位文本改成了Text(valueParts[index]),也就是当前要显示的实际字符。这下问题来了:
- VStack的边框看起来还是在正常上下移动,但里面的数字却出现了异常:有的数字乱序滚动,有的甚至直接淡入淡出,完全没有了原本流畅的滚动效果!
核心疑问
两种情况下,我都是修改VStack的.offset值,并且都用withAnimation包裹了状态更新。为什么仅仅是底层占位文本从固定的"X"换成了动态的实际字符,就会导致动画表现完全不同?
完整代码
struct RollingCounter: View { typealias Formatter = ((Double) -> String) var font: Font = .largeTitle @Binding var value: Double @Binding var formatter: ((Double) -> String) @State var valueParts: [String] = [] var body: some View { HStack(spacing: 0) { ForEach(0..<valueParts.count, id: \.self) { index in //Text("X") // Approach 1 Text(valueParts[index]) // Approach 2 //.id("part_\(valueParts.count - 1 - index)") .font(font) .opacity(0.2) .overlay { if let int = Int(valueParts[index]) { GeometryReader { geometry in VStack(spacing: 0) { ForEach(0...9, id: \.self) { number in Text("\(number)") //.id("\(index)_\(number)") .font(font) .border(Color.blue, width: 2) } } .border(Color.green, width: 1) .offset(y: -CGFloat(CGFloat(int) * geometry.size.height)) } //.clipped() } else { Text(valueParts[index]) .font(font) } } } .background(.red) } .onAppear { // Init with 0.00 valueParts = formatter(value).map { if Int("\($0)") == nil { "\($0)" } else { "0" } } updateText() } .onChange(of: value) { updateText() } } func updateText() { for (index, part) in Array(formatter(value)).enumerated() { withAnimation(.easeInOut(duration: 5)) { valueParts[index] = "\(part)" } } } } struct TestContentView: View { @State var value: Double = 0 @State var formatter: RollingCounter.Formatter = { value in let formatter = NumberFormatter() formatter.locale = Locale.init(identifier: "en_US") formatter.numberStyle = .currency formatter.currencySymbol = "" formatter.currencyCode = "" formatter.maximumFractionDigits = 2 formatter.minimumFractionDigits = 2 return formatter.string(from: NSNumber(value: value)) ?? "" } var body: some View { VStack(spacing: 25) { Button("Change Value") { value = (value == 1.24 ? 8.76 : 1.24) //.random(in: 1...1999) } RollingCounter(font: .system(size: 50), value: $value, formatter: $formatter) } } } #Preview { TestContentView() }
备注:内容来源于stack exchange,提问作者Andrei Herford
相关产品推荐
相关产品推荐

