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

SwiftUI长按按钮加载条效果故障:长按2秒触发弹窗代码无效

长按按钮填充动画+延迟触发需求的问题修复

我需要实现一个「Complete」按钮,要求:

  • 用户长按按钮2秒后才触发弹窗
  • 长按期间按钮从左到右填充颜色,模拟加载条进度

但当前代码无法实现该效果,现有代码如下:

View代码

@GestureState private var isLongPressing = false

Button("Complete") {
    withAnimation {
        viewModel.isPopupPresented.toggle()
        viewModel.stopProgressTimer()
    }
}
.font(.system(size: 20, weight: .heavy, design: .rounded))
.foregroundColor(.white)
.frame(width: 150, height: 40, alignment: .center)
.background(
    ZStack {
        RoundedRectangle(cornerRadius: 20, style: .continuous)
            .fill(Color.secondaryColor)

        if isLongPressing {
            RoundedRectangle(cornerRadius: 20, style: .continuous)
                .fill(Color.red)
                .frame(width: 150 * CGFloat(viewModel.progress), height: 40, alignment: .leading)
        }
    }
)
.gesture(
    LongPressGesture(minimumDuration: 2.0)
        .updating($isLongPressing) { value, state, _ in
            state = value
            if value {
                viewModel.startProgressTimer()
            } else {
                viewModel.stopProgressTimer()
            }
        }
        .onEnded { value in
            if isLongPressing {
                // Trigger your action here
                viewModel.isPopupPresented.toggle()
                viewModel.stopProgressTimer()
            }
        }
)

ViewModel代码

@Published var progress: Double = 0.0
private var progressTimer: Timer?
@Published var isPopupPresented = false

func resetProgress() {
    progressTimer?.invalidate()
    progress = 0.0
}

func startProgressTimer() {
    progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
        if self.progress < 1.0 {
            self.progress += 0.1
        } else {
            self.progress = 1.0
            self.resetProgress()
        }
    }
}

func stopProgressTimer() {
    progressTimer?.invalidate()
    progress = 0.0
}

问题分析与修复方案

原代码核心问题

  1. 默认点击事件干扰:按钮自带的action会在普通点击时直接触发弹窗,违背「仅长按触发」的需求
  2. 进度步长不匹配:原计时器每0.1秒加0.1,1秒就填满进度,和要求的2秒长按时长不匹配
  3. 手势状态判断失效:onEnded中用isLongPressing判断会出错,因为GestureState在手势结束后会自动重置为初始值
  4. 线程安全问题:scheduledTimer默认在后台线程执行,直接更新UI可能导致异常

修正后的代码

View代码

@GestureState private var isLongPressing = false

var body: some View {
    Button("Complete") {
        // 移除默认点击事件,仅保留长按逻辑
    }
    .font(.system(size: 20, weight: .heavy, design: .rounded))
    .foregroundColor(.white)
    .frame(width: 150, height: 40, alignment: .center)
    .background(
        ZStack {
            RoundedRectangle(cornerRadius: 20, style: .continuous)
                .fill(Color.secondary)
            
            RoundedRectangle(cornerRadius: 20, style: .continuous)
                .fill(Color.red)
                .frame(width: isLongPressing ? 150 * CGFloat(viewModel.progress) : 0, height: 40, alignment: .leading)
                .animation(.linear(duration: 0.1), value: viewModel.progress)
        }
    )
    .gesture(
        LongPressGesture(minimumDuration: 2.0)
            .updating($isLongPressing) { currentState, gestureState, _ in
                gestureState = currentState
                if currentState {
                    viewModel.startProgressTimer()
                } else {
                    viewModel.stopProgressTimer()
                }
            }
            .onEnded { _ in
                // 仅当长按达到2秒时才触发弹窗
                viewModel.isPopupPresented = true
                viewModel.stopProgressTimer()
            }
            .onChanged { isActive in
                // 未达到2秒就松手,立即重置进度
                if !isActive {
                    viewModel.stopProgressTimer()
                }
            }
    )
    .sheet(isPresented: $viewModel.isPopupPresented) {
        // 弹窗内容示例
        Text("长按触发的弹窗")
            .padding()
            .background(Color.white)
            .cornerRadius(12)
    }
}

ViewModel代码

import Foundation

class ViewModel: ObservableObject {
    @Published var progress: Double = 0.0
    private var progressTimer: Timer?
    @Published var isPopupPresented = false
    
    func resetProgress() {
        progressTimer?.invalidate()
        progress = 0.0
    }
    
    func startProgressTimer() {
        // 先重置进度,避免重复累加
        resetProgress()
        // 2秒填满进度,每0.1秒的步长为 1/(2/0.1) = 0.05
        let step: Double = 0.05
        progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
            guard let self = self else { return }
            DispatchQueue.main.async {
                if self.progress < 1.0 {
                    self.progress += step
                } else {
                    self.progress = 1.0
                    self.resetProgress()
                }
            }
        }
    }
    
    func stopProgressTimer() {
        progressTimer?.invalidate()
        progress = 0.0
    }
}

关键优化点

  • 移除按钮默认点击事件,确保只有长按能触发弹窗
  • 调整进度步长为0.05,让进度在2秒内刚好填满(0.1秒×20次=2秒)
  • 利用LongPressGesture的onEnded特性:只有达到minimumDuration后才会执行,无需额外判断
  • 用DispatchQueue.main.async确保进度更新在主线程,避免UI刷新异常
  • 给进度条添加线性动画,让填充过程更流畅自然

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 06:11:15