SwiftUI自定义进度条:动态文本动画瞬移而非平滑移动问题
自定义进度条动态文本平滑移动问题
问题描述
开发自定义进度条时,动态百分比文本随进度变化时会出现消失后重新出现的瞬移现象,而使用固定文本时则能正常平滑移动。


现有代码
private struct MyProgressBar: View { let progress: Double var body: some View { RoundedRectangle(cornerRadius: 5) .frame(height: 20) .foregroundColor(.gray) .overlay(alignment: .leading) { GeometryReader { geo in let progressWidth = geo.size.width * progress RoundedRectangle(cornerRadius: 5) .foregroundColor(.yellow) .frame(width: progressWidth) .overlay(alignment: .topTrailing) { bubble(progress) .fixedSize() .alignmentGuide(HorizontalAlignment.trailing) { d in d[HorizontalAlignment.center] } .alignmentGuide(VerticalAlignment.top) { d in d[.bottom] } } } } .padding() } @ViewBuilder private func bubble(_ progress: Double) -> some View { Text(progress.formatted(.percent)) .monospacedDigit() .padding(5.0) .background { RoundedRectangle(cornerRadius: 2) .foregroundColor(.green) } } } struct MyProgressBarExample: View { @State private var progress: Double = 0.3 var body: some View { VStack { MyProgressBar(progress: progress) VStack { Button("change") { withAnimation() { progress = Double.random(in: 0.0...1.0) } } Slider( value: $progress.animation(), in: 0.0...1.0, step: 0.005 ) .padding() } } } }
问题根源
当progress更新时,Text的内容随之变化,SwiftUI会将其判定为全新的视图,销毁旧视图并创建新视图,导致动画中断,表现为瞬移。而固定文本时视图身份不变,动画可以正常执行。
解决方案
通过固定视图身份、调整布局结构,让SwiftUI识别到是同一个视图在更新内容而非替换,同时单独控制文本的位置动画。
修改后的代码
private struct MyProgressBar: View { let progress: Double @State private var barWidth: CGFloat = 0 var body: some View { ZStack(alignment: .leading) { // 灰色背景条 RoundedRectangle(cornerRadius: 5) .frame(height: 20) .foregroundColor(.gray) .overlay( GeometryReader { geo in Color.clear .preference(key: BarWidthKey.self, value: geo.size.width) } ) // 黄色进度条 RoundedRectangle(cornerRadius: 5) .foregroundColor(.yellow) .frame(width: barWidth * progress) // 气泡文本:独立布局,避免随进度条重建 bubbleView .fixedSize() .offset( x: barWidth * progress - bubbleHalfWidth(), y: -25 ) .animation(.easeInOut, value: progress) } .padding() .onPreferenceChange(BarWidthKey.self) { barWidth = $0 } } // 计算气泡半宽,确保对齐进度条末端中心 private func bubbleHalfWidth() -> CGFloat { let text = Text(progress.formatted(.percent)).monospacedDigit() let textSize = text.sizeThatFits(CGSize(width: .infinity, height: .infinity)) return (textSize.width + 10) / 2 // 10是左右padding总和 } // 提取气泡视图,固定id确保身份稳定 private var bubbleView: some View { Text(progress.formatted(.percent)) .monospacedDigit() .padding(5) .background( RoundedRectangle(cornerRadius: 2) .foregroundColor(.green) ) .id("progress-bubble") } // 自定义PreferenceKey获取进度条宽度 private struct BarWidthKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } } struct MyProgressBarExample: View { @State private var progress: Double = 0.3 var body: some View { VStack { MyProgressBar(progress: progress) VStack { Button("change") { withAnimation() { progress = Double.random(in: 0.0...1.0) } } Slider( value: $progress.animation(), in: 0.0...1.0, step: 0.005 ) .padding() } } } }
核心修改点
- 固定视图身份:给气泡视图添加
id("progress-bubble"),让SwiftUI确认这是同一个视图,仅更新文本内容。 - 独立布局气泡:将气泡从进度条的
overlay中移出,放到外层ZStack,避免进度条宽度变化触发气泡重建。 - 精准位置控制:通过
GeometryReader获取进度条实际宽度,结合气泡自身宽度计算偏移量,确保气泡始终对齐进度条末端中心。 - 显式绑定动画:给气泡的
offset绑定progress值的动画,确保位置变化平滑过渡。
内容的提问来源于stack exchange,提问作者Maq
相关产品推荐
相关产品推荐

