You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

核心修改点

  1. 固定视图身份:给气泡视图添加id("progress-bubble"),让SwiftUI确认这是同一个视图,仅更新文本内容。
  2. 独立布局气泡:将气泡从进度条的overlay中移出,放到外层ZStack,避免进度条宽度变化触发气泡重建。
  3. 精准位置控制:通过GeometryReader获取进度条实际宽度,结合气泡自身宽度计算偏移量,确保气泡始终对齐进度条末端中心。
  4. 显式绑定动画:给气泡的offset绑定progress值的动画,确保位置变化平滑过渡。

内容的提问来源于stack exchange,提问作者Maq

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 07:07:02