SwiftUI:如何处理点击和长按按钮?

时间:2019-10-08 10:56:34

标签: button swiftui long-press

我在SwiftUI中有一个按钮,我希望能够对“点击按钮”(正常点击/点击)和“长按”采取不同的操作。

在SwiftUI中有可能吗?

这是我现在拥有的按钮的简单代码(仅处理“常规”轻触/触摸盒)。

    //This looks for columns in first row which has whitespace
    //and then goes down the rows to see if it is all whitespace
    for (int a=0; a<testCol[0];a++){
        for (int b=0;b<testRow[0];b++){
            if(a<494){
                if (astCheck[b][a] ==' '){
                    if (astCheck[testRow[0]-1][testCol[0]-1]==' '){
                        whiteCols[b][a] = true;
                    }else{
                        whiteCols[b][a] = false;
                        continue;
                    }

                }else if (astCheck[b][a]=='*'){
                    a++;
                    b=0;
                    //continue;
                }
            }

        }
    }

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

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

谢谢!

杰拉德

10 个答案:

答案 0 :(得分:6)

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

  1. 处理双击手势
  2. 处理longPressGesture
  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)
        }
}

答案 1 :(得分:3)

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

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

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

答案 2 :(得分:3)

我必须为正在构建的应用执行此操作,因此只想共享。请参阅底部的代码,它相对来说是自解释的,并且坚持SwiftUI的主要元素。

此答案与上述答案的主要区别在于,它允许根据状态更新按钮的背景色,并且还涵盖了一种使用情况,即希望长按的动作在手指被抬起而不是在手指抬起时发生时间阈值已通过。

正如其他人所指出的那样,我无法直接将手势应用于按钮,而不得不将其应用于其内部的文本视图。不幸的是,它具有减少按钮“点击框”的副作用,如果我在按钮边缘附近按下,手势将不会触发。因此,我删除了Button,专注于直接操作我的Text View对象(可以用Image View或其他视图替换(但不能替换Button!))。

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

  1. 可立即触发并反映您问题中“轻击”手势的LongPressGesture(我尚未测试过,但可以用TapGesture代替)

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

  3. 一个最小距离为0的拖动手势,允许我们在手指的末端从按钮上抬起而不是在0.25秒时自动执行事件(如果不是您的用例,可以将其删除)。您可以在此处了解更多信息:How do you detect a SwiftUI touchDown event with no movement or duration?

我们按以下顺序对手势进行排序:使用“排他性”组合“长按”(即上面的2和3)和Tap(上面的第一个手势),如果“长按”的0.25秒阈值不是到达后,将执行轻击手势。 “长按”本身是我们的长按手势和拖动手势的序列,因此只有在手指抬起后才能执行该动作。

我还在下面添加了代码,用于根据状态更新按钮的颜色。需要注意的一件事是,我不得不将按钮颜色的代码添加到长按和拖动手势的onEnded部分中,因为不幸的是,处理时间的缩短会导致按钮在l​​ongPressGesture和DragGesture(从理论上讲这是不会发生的,除非我的某个地方有错误!)。

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

如果您修改以下内容并注意Apple的手势说明(此答案也很有用,请阅读:How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?),则应该能够设置复杂的自定义按钮交互。将手势用作构建基块,并将它们组合起来以消除单个手势中的任何不足(例如,longPressGesture不能选择在事件结束时(而不是在达到条件时)进行事件)。

P.S。我有一个全局环境对象“ dataRouter”(与问题无关,以及与我如何选择在多个视图之间共享参数),可以安全地将其删除。

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)

    }

}

答案 3 :(得分:2)

结合高优先级手势和同步手势应该可以解决问题。

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

在与其他视图交互时也发现这是一个方便的模式。

答案 4 :(得分:1)

未经测试,但是您可以尝试向按钮中添加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)
    }
}

答案 5 :(得分:0)

如果其他任何人都在挣扎,我想将其发布回去。奇怪的是,Apple的默认行为适用于大多数控件,但不适用于按钮。就我而言,我想在支持长按的同时保持按钮效果。

一种无需太多复杂性就能起作用的方法是忽略默认按钮动作,并创建一个同时处理正常和长按的手势。

在您看来,您可以应用自定义长按修饰符,如下所示:

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

答案 6 :(得分:0)

尝试一下:)

句柄isInactive,isPressing,isLongPress和Tap(单击)

基于this

我试图使它作为viewmodifier失败。我想看到一个示例,其中@GestureState变量包装器的使用方式与@ State / @ Published在视图组件中绑定到@Binding相同。

经过测试:Xcode 12.0 beta,macOS Big Sur 11.0 beta

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

答案 7 :(得分:0)

作为后续,我遇到了同样的问题,我尝试了所有这些答案,但是不喜欢它们的全部工作原理。我最终使用了.contextMenu,它更容易实现相同的效果。

选中link here

这是example

答案 8 :(得分:0)

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

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之后加上其他想要的效果,使其在按下时看起来像您想要的。

答案 9 :(得分:0)

只需这样做: 第一个修饰符应该是 onLongPressGesture(minumumDuration: (你想要的持续时间)) 并且以下修饰符应该是 onLongPressGesture(minimumDuration: 0.01) <- 或其他一些超小数字

这对我很有效