Button SwiftUI:如何处理tap和amp;长按按钮?

Button SwiftUI:如何处理tap和amp;长按按钮?,button,swiftui,long-press,Button,Swiftui,Long Press,我在SwiftUI中有一个按钮,我希望能够对“点击按钮”(普通点击/点击)和“长按”进行不同的操作 这在SwiftUI中可能吗 下面是我现在使用的按钮的简单代码(仅处理“正常”点击/触摸情况) 我已经尝试添加“长按手势”,但它仍然只“执行”正常/短按。这就是我尝试的代码: Button(action: {self.BLEinfo.startScan() }) { Text("Scan") .f

我在SwiftUI中有一个按钮,我希望能够对“点击按钮”(普通点击/点击)和“长按”进行不同的操作

这在SwiftUI中可能吗

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

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

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


Gerard

这没有经过测试,但您可以尝试在按钮上添加
长按手势

大概看起来是这样的

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

我尝试了很多事情,但最后我还是做了这样的事情:

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

        // Apply the modifier
        Button(action: self.onReloadDefaultAction) {
            Text("Reload")
        }
            .modifier(LongPressModifier(
                isDisabled: self.sessionButtonsDisabled,
                completionHandler: self.onReloadPressed))
    }

    // Ignore the default click
    private func onReloadDefaultAction() {
    }

    // Handle the simultaneous gesture
    private func onReloadPressed(isLongPress: Bool) {

        // Do the work here
    }

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

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

  • 处理双击手势
  • 长按手势
  • 处理一个简单的点击手势
  • 在Xcode版本11.3.1(11C504)上测试


    我想我会把这篇文章发回去,以防其他人在挣扎。奇怪的是,苹果的默认行为对大多数控件有效,但对按钮无效。在我的例子中,我希望在支持长按的同时保持按钮效果

    一种不太复杂的方法是忽略默认的按钮动作,创建一个同时处理正常和长时间点击的手势

    在视图中,可以应用自定义长按修改器,如下所示:

        Button(action: {
        }) {
            VStack {
                Image(self.imageName)
                    .resizable()
                    .onTapGesture {
                        self.action(false)
                    }
                    .onLongPressGesture(minimumDuration: 0.1) {
                        self.action(true)
                    }
            }
        }
    
    var body: some View {
    
            // Apply the modifier
            Button(action: self.onReloadDefaultAction) {
                Text("Reload")
            }
                .modifier(LongPressModifier(
                    isDisabled: self.sessionButtonsDisabled,
                    completionHandler: self.onReloadPressed))
        }
    
        // Ignore the default click
        private func onReloadDefaultAction() {
        }
    
        // Handle the simultaneous gesture
        private func onReloadPressed(isLongPress: Bool) {
    
            // Do the work here
        }
    
    我的长按修改器实现看起来像这样,使用了我在另一篇文章中找到的拖动手势。不是很直观,但它工作可靠,尽管我当然不希望自己编写这个管道

    struct LongPressModifier: ViewModifier {
    
        // Mutable state
        @State private var startTime: Date?
    
        // Properties
        private let isDisabled: Bool
        private let longPressSeconds: Double
        private let completionHandler: (Bool) -> Void
    
        // Initialise long press behaviour to 2 seconds
        init(isDisabled: Bool, completionHandler: @escaping (Bool) -> Void) {
    
            self.isDisabled = isDisabled
            self.longPressSeconds = 2.0
            self.completionHandler = completionHandler
        }
    
        // Capture the start and end times
        func body(content: Content) -> some View {
    
            content.simultaneousGesture(DragGesture(minimumDistance: 0)
                .onChanged { _ in
    
                    if self.isDisabled {
                        return
                    }
    
                    // Record the start time at the time we are clicked
                    if self.startTime == nil {
                        self.startTime = Date()
                    }
                }
                .onEnded { _ in
    
                    if self.isDisabled {
                        return
                    }
    
                    // Measure the time elapsed and reset
                    let endTime = Date()
                    let interval = self.startTime!.distance(to: endTime)
                    self.startTime = nil
    
                    // Return a boolean indicating whether a normal or long press
                    let isLongPress = !interval.isLess(than: self.longPressSeconds)
                    self.completionHandler(isLongPress)
                })
        }
    }
    

    我必须为我正在构建的应用程序这样做,所以我只想与大家分享。请参阅底部的代码,它相对来说是不言自明的,并且在SwiftUI的主要元素中都有

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

    正如其他人所指出的,我无法直接将手势应用于按钮,必须将其应用于按钮内的文本视图。这有一个不幸的副作用,那就是减少了按钮的“点击框”,如果我按下按钮边缘附近,这个手势就不会触发。因此,我删除了按钮,并专注于直接操作文本视图对象(这可以替换为图像视图或其他视图(但不是按钮!))

    以下代码设置了三种手势:

  • 长按手势,可立即触发并反映问题中的“轻触”手势(我还没有测试过,但可以用轻触手势代替)

  • 另一个最短持续时间为0.25的长按手势,反映问题中的“长按”手势

  • 最小距离为0的拖动手势,允许我们在手指末端从按钮上抬起时执行事件,而不是在0.25秒时自动执行(如果这不是您的用例,您可以删除此操作)。您可以在此处阅读更多关于此的信息:

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

    我还在下面添加了根据状态更新按钮颜色的代码。需要注意的一点是,我必须在长按拖动手势的统一部分添加按钮颜色的代码,因为很短的处理时间将不幸导致按钮在长按手势和拖动手势之间切换回黑色按钮颜色(理论上这不应该发生,除非我在什么地方有个bug!)

    您可以在此处阅读有关手势的更多信息:

    如果你修改了下面的内容,并注意苹果的手势说明(这也是一个有用的答案:),你应该能够设置复杂的定制按钮交互。将手势用作构建块,并将它们组合起来,以消除单个手势中的任何缺陷(例如,LongPress手势没有在结束时执行事件的选项,也没有在达到条件时执行事件的选项)

    另外,我有一个全局环境对象“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)
    
        }
    
    }
    
    试试这个:)

    手柄isInactive、isPressing、isLongPress和Tap(单击)

    基于

    我尝试将其作为viewmodifier,但没有成功。我希望看到一个使用@GestureState变量包装器的示例,其使用方式与@State/@Published绑定到视图组件中的@Binding的方式相同

    测试:Xcode 12.0测试版,macOS Big Sur 11.0测试版

    import SwiftUI
    
    enum PressState {
    
        case inactive
        case pressing
        case longPress
        
        var isPressing: Bool {
            switch self {
            case .inactive:
                return false
            case .pressing, .longPress:
                return true
            }
        }
        
        var isLongPress: Bool {
            switch self {
            case .inactive, .pressing:
                return false
            case .longPress:
                return true
            }
        }
        
        var isInactive : Bool {
            switch self {
            case .inactive:
                return true
            case .pressing, .longPress:
                return false
            }
        }
    }
    
    
    struct ContentView: View {
        
        @GestureState private var pressState: PressState = PressState.inactive
        @State var showClick: Bool = false
        
        var press: some Gesture {
            LongPressGesture(minimumDuration: 0.8, maximumDistance: 50.0)
                .sequenced(before: LongPressGesture(minimumDuration: .infinity, maximumDistance: 50.0))
                .updating($pressState) { value, state, transaction in
                    switch value {
                    case .first(true): // first gesture starts
                        state = PressState.pressing
                    case .second(true, nil): // first ends, second starts
                            state = PressState.longPress
                        default: break
                    }
                }
        }
        
        var body: some View {
            ZStack{
                
                Group {
                Text("Click")
                    .offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -120 : -100) : -40)
                    .animation(Animation.linear(duration: 0.5))
                    .opacity(showClick ? 1 : 0 )
                    .animation(Animation.linear(duration: 0.3))
                    
                Text("Pressing")
                    .opacity(pressState.isPressing ? 1 : 0 )
                    .offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -100 : -80) : -20)
                    .animation(Animation.linear(duration: 0.5))
                
                Text("Long press")
                    .opacity(pressState.isLongPress ? 1 : 0 )
                    .offset(x: 0, y: pressState.isLongPress ? -80 : 0)
                    .animation(Animation.linear(duration: 0.5))
                }
                
                Group{
                Image(systemName: pressState.isLongPress ? "face.smiling.fill" : (pressState.isPressing ? "circle.fill" : "circle"))
                    .offset(x: 0, y: -100)
                    .font(.system(size: 60))
                    .opacity(pressState.isLongPress ? 1 : (pressState.isPressing ? 0.6 : 0.2))
                    .foregroundColor(pressState.isLongPress ? .orange : (pressState.isPressing ? .yellow : .white))
                    .rotationEffect(.degrees(pressState.isLongPress ? 360 : 0), anchor: .center)
                    .animation(Animation.linear(duration: 1))
                
                Button(action: {
                    showClick = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
                        self.showClick = false
                    })
                }, label: {
                    ZStack {
                        Circle()
                            .fill(self.pressState.isPressing ? Color.blue : Color.orange)
                            .frame(width: 100, height: 100, alignment: .center)
                        Text("touch me")
                    }}).simultaneousGesture(press)
                }.offset(x: 0, y: 110)
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    

    作为跟进,我遇到了同样的问题,我尝试了所有这些答案,但不喜欢它们的工作方式。我最终使用了.contextMenu。它更简单,产生了几乎相同的效果

    检查


    这是一个

    这是我使用修饰符的实现:

    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> }))
    
    .modifier(TapAndLongPressModifier)(tapAction:{},
    longPressAction:{})
    

    它只是通过将视图缩小一点来模仿look a按钮。您可以在
    scaleEffect
    之后放置任何其他您想要的效果,使其在按下时看起来像您想要的样子。

    将高优先级手势和同时手势相结合应该不会产生任何效果
    Button(action: {})
    {
        Text("A Button")
    }
    .simultaneousGesture(
        LongPressGesture()
            .onEnded { _ in
                print("Loooong")
            }
    )
    .highPriorityGesture(TapGesture()
                            .onEnded { _ in
                                print("Tap")
                            })