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 }
问题分析与修复方案
原代码核心问题
- 默认点击事件干扰:按钮自带的action会在普通点击时直接触发弹窗,违背「仅长按触发」的需求
- 进度步长不匹配:原计时器每0.1秒加0.1,1秒就填满进度,和要求的2秒长按时长不匹配
- 手势状态判断失效:
onEnded中用isLongPressing判断会出错,因为GestureState在手势结束后会自动重置为初始值 - 线程安全问题:
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
相关产品推荐
相关产品推荐

