SwiftUI:如何处理按钮的短按和长按?

67

我在SwiftUI中有一个按钮,希望在“点击按钮”(正常点击/触摸)和“长按”时具有不同的操作。

在SwiftUI中是否可能实现这一点?

以下是我现在拥有的按钮的简单代码(仅处理“正常”的点击/触摸情况)。

Button(action: {self.BLEinfo.startScan() }) {
                        Text("Scan")
                    } .disabled(self.BLEinfo.isScanning)

我已经尝试添加“长按手势”,但它仍然只“执行”“正常/短”点击。这是我尝试的代码:

Button(action: {self.BLEinfo.startScan() }) {
                        Text("Scan")
                            .fontWeight(.regular)
                            .font(.body)
                        .gesture(
                            LongPressGesture(minimumDuration: 2)
                                .onEnded { _ in
                                    print("Pressed!")
                            }
                        )
                    }

谢谢!

杰拉德


我使用自定义的UIScrollView和自定义的UIView解决了这个问题:https://gist.github.com/danhalliday/79b003d1cdbb84069c5c9f24fe069827 - Dan Halliday
14个回答

59

我尝试了许多方法,但最终我做了类似于这样的事情:

    Button(action: {
    }) {
        VStack {
            Image(self.imageName)
                .resizable()
                .onTapGesture {
                    self.action(false)
                }
                .onLongPressGesture(minimumDuration: 0.1) {
                    self.action(true)
                }
        }
    }

这仍然是一个带有效果的按钮,但短按和长按是不同的。


21
请注意,从 Xcode 11.2.1 / iOS 13.2 开始,顺序似乎很重要。在使用 onLongPressGesture() 之前使用 onTapGesture() 将忽略后者。 - Koraktor
2
这段程序相关的内容翻译成中文:这不具有触摸或按压动画,并且阻止了“action”中的代码。 - Faruk

54

结合高优先级手势和同时手势应该能解决问题。

Button(action: {}) {
    Text("A Button")
}
    .simultaneousGesture(
        LongPressGesture()
            .onEnded { _ in
                print("Loooong")
            }
    )
    .highPriorityGesture(
        TapGesture()
            .onEnded { _ in
                print("Tap")
            }
    )

发现这是与其他视图交互时非常方便的模式。

这个解决方案也可以在ScrollView中使用 :) 谢谢。 - Liviu
1
这在iOS上运行良好,但在macOS上似乎无法工作。在Xcode版本13.3.1(13E500a)中,两个手势都无法被识别。 - Chuck H
2
这似乎可以消除在iOS 16.4中轻触按钮时的动画效果。你知道如何避免这种情况吗? - Nicolas Gimelli
你真是个救命稻草! - undefined

12

我刚刚发现效果取决于实现的顺序。按照以下顺序实现手势检测似乎可以检测并识别所有三个手势:

  1. 处理双击手势
  2. 处理长按手势
  3. 处理单击手势

在 Xcode 版本 11.3.1 (11C504) 上测试通过。

    fileprivate func myView(_ height: CGFloat, _ width: CGFloat) -> some View {
    return self.textLabel(height: height, width: width)
        .frame(width: width, height: height)
        .onTapGesture(count: 2) {
            self.action(2)
        }
        .onLongPressGesture {
            self.action(3)
        }
        .onTapGesture(count: 1) {
            self.action(1)
        }
}

6

以下是我的实现,使用了一个修饰符:

struct TapAndLongPressModifier: ViewModifier {
  @State private var isLongPressing = false
  let tapAction: (()->())
  let longPressAction: (()->())
  func body(content: Content) -> some View {
    content
      .scaleEffect(isLongPressing ? 0.95 : 1.0)
      .onLongPressGesture(minimumDuration: 1.0, pressing: { (isPressing) in
        withAnimation {
          isLongPressing = isPressing
          print(isPressing)
        }
      }, perform: {
        longPressAction()
      })
      .simultaneousGesture(
        TapGesture()
          .onEnded { _ in
            tapAction()
          }
      )
  }
}

在任何视图中都可以像这样使用:

.modifier(TapAndLongPressModifier(tapAction: { <tap action> },
                                  longPressAction: { <long press action> }))

它只是通过缩小视图来模仿按钮的外观。在scaleEffect之后,您可以放置任何其他效果,以便在按下时呈现您想要的外观。


这仅适用于 tapAction,在我的情况下 longPressAction 从未被调用(Xcode 13.1)。 - Papyrus

3

我需要在构建的应用程序中执行此操作,所以想要分享一下。请参考下面的代码,它相对而言比较易于理解,并且在SwiftUI的主要元素内保持一致。

这个答案与上面的答案之间的主要区别在于,它允许根据状态更新按钮的背景颜色,并且还涵盖了希望长按操作在手指抬起时而不是在时间阈值过去时发生的用例。

如其他人所述,我无法将手势直接应用于Button,因此必须将其应用于其中的TextView。这具有不幸的副作用,即减少了按钮的可点击范围。如果我在按钮的边缘附近按下,则手势不会触发。因此,我删除了Button并专注于直接操作我的TextView对象(这可以替换为ImageView或其他视图(但不能是Button!))。

下面的代码设置了三个手势:

  1. 一个LongPressGesture立即触发,并反映您问题中的“tap”手势(我没有测试,但可以替换为TapGesture)

  2. 另一个LongPressGesture的最小持续时间为0.25,并反映了您问题中的“长按”手势

  3. 带有最小距离为0的拖动手势,允许我们在手指离开按钮时执行事件,并且不会自动在0.25秒后执行(如果这不是您的用例,则可以删除此操作)。您可以在此处阅读更多: 如何检测SwiftUI触摸事件而没有移动或持续时间?

我们按以下顺序对手势进行排序:使用“独占”将“长按”(即上述2和3组合)与Tap(上面的第一个手势)结合起来,如果未达到“长按”的0.25秒阈值,则执行tap手势。“长按”本身是我们的长按手势和拖动手势的序列,因此只有当我们抬起手指时才执行该操作。

我还添加了代码,以根据状态更新按钮的颜色。需要注意的一点是,我必须在长按和拖动手势的onEnded部分中添加按钮颜色的代码,因为极小的处理时间不幸地导致按钮在长按手势和拖动手势之间切换回darkButton颜色(理论上不应该发生这种情况,除非我某些地方有漏洞!)。

您可以在此处阅读有关手势的更多信息: https://developer.apple.com/documentation/swiftui/gestures/composing_swiftui_gestures

如果您修改下面的代码并注意苹果有关手势的注释(这篇答案也很有用:如何在SwiftUI中触发事件处理程序,当用户停止长按手势时?),您应该能够设置复杂的自定义按钮交互。使用手势作为构建块,并将它们组合在一起以消除单个手势内的任何缺陷(例如,longPressGesture没有选项在其结束时执行事件而不是在达到条件时执行)。
附言:我有一个全局环境对象'dataRouter'(与问题无关,只是我选择在Swift视图间共享参数的方式),您可以安全地将其编辑掉。
struct AdvanceButton: View {

@EnvironmentObject var dataRouter: DataRouter

@State var width: CGFloat
@State var height: CGFloat
@State var bgColor: Color

@GestureState var longPress = false
@GestureState var longDrag = false

var body: some View {

    let longPressGestureDelay = DragGesture(minimumDistance: 0)
        .updating($longDrag) { currentstate, gestureState, transaction in
                gestureState = true
        }
    .onEnded { value in
        print(value.translation) // We can use value.translation to see how far away our finger moved and accordingly cancel the action (code not shown here)
        print("long press action goes here")
        self.bgColor = self.dataRouter.darkButton
    }

    let shortPressGesture = LongPressGesture(minimumDuration: 0)
    .onEnded { _ in
        print("short press goes here")
    }

    let longTapGesture = LongPressGesture(minimumDuration: 0.25)
        .updating($longPress) { currentstate, gestureState, transaction in
            gestureState = true
    }
    .onEnded { _ in
        self.bgColor = self.dataRouter.lightButton
    }

    let tapBeforeLongGestures = longTapGesture.sequenced(before:longPressGestureDelay).exclusively(before: shortPressGesture)

    return
        Text("9")
            .font(self.dataRouter.fontStyle)
            .foregroundColor(self.dataRouter.darkButtonText)
            .frame(width: width, height: height)
            .background(self.longPress ? self.dataRouter.lightButton : (self.longDrag ? self.dataRouter.brightButton : self.bgColor))
            .cornerRadius(15)
            .gesture(tapBeforeLongGestures)

    }

}

1
谢谢,你很棒。 - Mohsen mokhtari
2
非常感谢,这正是我要寻找的行为。此外,我可以确认,在我的情况下,使用TapGesture不起作用。 - Michael Horn

2

Kevin的答案是我需要的最接近的答案。由于在tapGesture之前订购longPressGesture会破坏我的ScrollViews,但反过来使minimumDuration参数无效,因此我自己实现了长按功能:

struct TapAndLongPressModifier: ViewModifier {
    @State private var canTap = false
    @State private var pressId = 0
    let tapAction: (()->())
    let longPressAction: (()->())
    var minimumDuration = 1.0
    func body(content: Content) -> some View {
        content
            .onTapGesture {
                if canTap {
                    tapAction()
                }
            }
            .onLongPressGesture(
                minimumDuration: 1.0,
                pressing: { (isPressing) in
                    pressId += 1
                    canTap = isPressing
                    if isPressing {
                        let thisId = pressId
                        
                        DispatchQueue.main.asyncAfter(deadline: .now() + minimumDuration) {
                            if thisId == pressId {
                                canTap = false
                                longPressAction()
                            }
                        }
                    }
                },
                
                // We won't actually use this
                perform: {}
            )
    }
}

2
(后来,在2023年……)
我尝试了所有我能找到的类似问题的解决方案,但每个解决方案都会产生某种冲突,其中一个手势会阻止另一个手势,或者类似的情况。
自己动手,我决定在阅读文档后实现一个符合PrimitiveButtonStyle的类型:
“指定符合PrimitiveButtonStyle的样式以创建具有自定义交互行为的按钮。”
本质上,这个样式使用onTapGestureonLongPressGesture,但是“tap gesture”只是使用协议暴露的configuration变量来激活按钮的“action”,该变量在声明按钮时定义。
作为额外的奖励,长按手势的pressed变量可以用来决定是否显示自定义反馈,指示按钮正在被按下,无论哪个手势最终成功。
// Conform to `PrimitiveButtonStyle` for custom interaction behaviour

struct SupportsLongPress: PrimitiveButtonStyle {
    
    /// An action to execute on long press
    let longPressAction: () -> ()
    
    /// Whether the button is being pressed
    @State var isPressed: Bool = false
    
    func makeBody(configuration: Configuration) -> some View {
        
        // The "label" as specified when declaring the button
        configuration.label
        
            // Visual feedback that the button is being pressed
            .scaleEffect(self.isPressed ? 0.9 : 1.0)
        
            .onTapGesture {
                
                // Run the "action" as specified
                // when declaring the button
                configuration.trigger()
                
            }
        
            .onLongPressGesture(
                
                perform: {
                    
                    // Run the action specified
                    // when using this style
                    self.longPressAction()
                    
                },
                
                onPressingChanged: { pressing in
                    
                    // Use "pressing" to infer whether the button
                    // is being pressed
                    self.isPressed = pressing
                    
                }
                
            )
        
    }
    
}

/// A modifier that applies the `SupportsLongPress` style to buttons
struct SupportsLongPressModifier: ViewModifier {
    let longPressAction: () -> ()
    func body(content: Content) -> some View {
        content.buttonStyle(SupportsLongPress(longPressAction: self.longPressAction))
    }
}

/// Extend the View protocol for a SwiftUI-like shorthand version
extension View {
    func supportsLongPress(longPressAction: @escaping () -> ()) -> some View {
        modifier(SupportsLongPressModifier(longPressAction: longPressAction))
    }
}

// --- At the point of use:

struct MyCustomButtonRoom: View {
    
    var body: some View {
        
        Button(
            action: {
                print("You've tapped me!")
            },
            label: {
                Text("Do you dare interact?")
            }
        )
        .supportsLongPress {
            print("Looks like you've pressed me.")
        }
        
    }
    
}

2
作为跟进,我遇到了同样的问题,尝试了所有这些答案,但不喜欢它们的工作方式。
我最终使用了.contextMenu,它更容易使用并且产生了几乎相同的效果。 请检查此处链接 这里是一个示例 更新: 截至iOS 16,.contextMenu已被弃用。所以我最终在按钮上使用了.simultaneousGesture,而不是按钮标签块中的内容。
即。
Button {
        // handle Button Tap
    } label: {
        // button label content here
    }
    .simultaneousGesture(LongPressGesture()
        .onEnded { _ in
            // handle long press here
        }
    )

这仍然保留了按钮动画效果。
但是请注意,在 iOS 16 之前未经测试。

2
这将导致列表无法滚动。 - gaohomway
嘿 @gaohomway,是的,但你找到了另一个解决方法吗? - Alexander Khitev

2

这个没有经过测试,但你可以尝试在你的按钮上添加一个LongPressGesture手势。

它可能看起来像这样。

struct ContentView: View {
    @GestureState var isLongPressed = false

    var body: some View {
        let longPress = LongPressGesture()
            .updating($isLongPressed) { value, state, transaction in
                state = value
            }

        return Button(/*...*/)
            .gesture(longPress)
    }
}

嗨 Kilian, 实际上我应该提到我已经尝试过添加长按手势,但仍然只会执行“正常点击”操作而不是长按操作。 我将编辑我的帖子以添加这一点(因为你是第二个建议这样做的人 - 第一个人删除了他的答案)。 - Gerard

1

只需要这样做: 第一个修饰符应该是onLongPressGesture(minimumDuration: (你想要的持续时间)) 然后接下来的修饰符应该是onLongPressGesture(minimumDuration: 0.01) <- 或其他非常小的数字

这对我来说完美地解决了问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接