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

如何根据剩余时间优化0.1至0.9区间的Circle.trim实现?

修复SwiftUI环形计时器的进度条逻辑,替换硬凑的魔法数值

你当前的代码靠反复调数值让进度条“看起来正常”,但核心问题是第二个Circle的trim用了1.1、0.026、1.25这类无逻辑的魔法数值,完全没法复用,也搞不清背后的计算逻辑。咱们把这部分重构,用明确的比例计算来替代。

先搞懂环形进度的基础逻辑

SwiftUI里Circle().trim(from:to:)的参数是0到1的比例,对应圆周的0%到100%。你给两个圆环加了.rotationEffect(.degrees(90)),是把默认的右侧起始点转到了顶部,这个没问题。

你的背景环是trim(from:0.1, to:0.9),也就是占了圆周的80%长度(0.9-0.1=0.8)。所以进度条的范围必须和这个背景环对齐:从0.1开始,到0.1 + 0.8 * 剩余时间比例结束,这样就能完美匹配背景环的长度。

重构后的代码

struct ContentView: View {
    
    @State private var timeRemaining: TimeInterval = 10
    @State private var startTime: TimeInterval = 10 // 初始化时直接和总时长一致
    @State private var timer: Timer?
    @State private var isRunning: Bool = false
    
    // 计算剩余时间的比例:0(时间到)~1(满时长)
    private var progress: Double {
        timeRemaining / startTime
    }
    
    var body: some View {
        ZStack {
            // 背景环:固定显示80%的圆周
            Circle()
                .trim(from: 0.1, to: 0.9)
                .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                .rotationEffect(.degrees(90))
                .opacity(0.3)
            
            // 进度环:和背景环对齐,根据剩余比例计算结束位置
            Circle()
                .trim(from: 0.1, to: CGFloat(0.1 + (0.9 - 0.1) * progress))
                .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                .rotationEffect(.degrees(90))
                .animation(.easeInOut(duration: 0.8), value: timeRemaining) // 绑定timeRemaining,每次变化自动动画
            
            Text(formattedTime())
                .font(.largeTitle)
                .bold()
        }
        .frame(width: 250)
        
        Button {
            isRunning.toggle()
            if isRunning {
                startTimer()
            } else {
                stopTimer()
            }
        } label: {
            Image(systemName: isRunning ? "stop.fill" : "play.fill")
                .foregroundStyle(.foreground)
                .frame(width: 50, height: 50)
                .font(.largeTitle)
                .padding()
        }
    }
    
    private func startTimer() {
        // 防止重复启动
        guard timer == nil else { return }
        // 如果是重新开始,更新startTime为当前的总时长
        startTime = timeRemaining
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            if timeRemaining > 0 {
                timeRemaining -= 1
            } else {
                stopTimer()
            }
        }
    }
    
    private func stopTimer() {
        isRunning = false
        timer?.invalidate()
        timer = nil // 清空timer引用
        timeRemaining = 10
        startTime = 10
    }
    
    private func formattedTime() -> String {
        let minutes = Int(timeRemaining) / 60
        let seconds = Int(timeRemaining) % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }
}

关键改动说明

  1. 新增progress计算属性:直接用timeRemaining / startTime得到0到1的比例,逻辑清晰,不管总时长设成10秒还是60秒都能正常工作。
  2. 进度环的trim计算:0.1 + (0.9 - 0.1) * progress,其中(0.9-0.1)是背景环的长度(80%),乘以剩余比例后加到起始点0.1上,完美匹配背景环的范围。
  3. 动画优化:把动画绑定到timeRemaining,每次数值变化自动触发动画,替代原代码里固定1秒的动画(原动画会导致每秒的进度变化都强制1秒动画,和timer的1秒间隔冲突)。
  4. Timer管理优化:添加guard timer == nil else { return }防止重复启动,清空timer引用避免内存泄漏。

这样修改后,你完全不用再调那些莫名其妙的数值,逻辑一目了然,不管怎么改总时长,进度条都会准确对应剩余时间。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 01:07:49