How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?

huangapple go评论71阅读模式
英文:

How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?

问题

根据文档,.OnEnded 事件处理程序将在成功检测到 LongPressGesture 后触发。我如何在用户在检测到手势后停止按下时触发事件?

这是一个示例:

  • 用户按下,例如 2 秒。

  • 某物出现

  • 用户在另外 2 秒后释放

  • 那个东西消失

英文:

Based on the documentation, the .OnEnded event handler will fire when the LongPressGesture has been successfully detected. How can I fire an event when the user stops pressing after the gesture has been detected?

Here is an example:

  • User presses for e.g. 2 seconds.

  • ** Something appears **

  • User releases after another 2 seconds

  • ** That something disappears **

答案1

得分: 14

我成功解决了它,尽管如果有人有更简单的解决方案,我将很乐意接受。

基本上,我需要将两个LongPressGesture链接在一起。

第一个将在长按2秒后生效 - 这是*something*应该出现的时候。

第二个将在Double.infinity时间后生效,意味着它永远不会完成,因此用户可以按住不放。对于这个效果,我们只关心当它被取消时的事件 - 这意味着用户停止按住。

@GestureState private var isPressingDown: Bool = false

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
    .updating($isPressingDown) { value, state, transaction in
        switch value {            
            case .second(true, nil): //这意味着第一个手势已完成
                state = true //更新GestureState
            default: break
        }
    })
    .
[...]

something.opacity(isPressingDown ? 1 : 0)

当通过调用.sequenced(before:)方法将两个LongPressGesture连接起来时,你会得到一个SequenceGesture<LongPressGesture, LongPressGesture>作为返回值,它在其Value枚举中有.first(Bool).second(Bool, Bool?)两种情况。

.first(Bool)情况是当第一个LongPressGesture尚未结束时。

.second(Bool, Bool?)情况是当第一个LongPressGesture已结束时。

因此,当SequenceGesture的值为.second(true, nil)时,这意味着第一个手势已完成,第二个手势尚未定义 - 这是something应该显示的时候 - 这就是为什么我们在该情况下将state变量设置为true的原因*(state变量封装了isPressingDown变量,因为它作为第一个参数传递给.updating(_:body:)方法)*。

而且,我们不必担心将state设置回false,因为在使用.updating(_:body:)方法时,如果用户取消手势,状态会返回到其初始值 - 初始值为false - 这将导致"something"消失。(这里取消意味着在手势结束所需的最小秒数之前松开手指 - 对于第二个手势,这是无限秒数。)

因此,重要的是要注意.updating(_:body:)方法的回调在手势被取消时不会被调用,根据这个文档的"Update Transient UI State"部分。


编辑于2021年3月24日:

我遇到了在我的视图中更新@ObservedObject@Published属性的问题。由于重置GestureState时不会调用.updating()方法闭包,因此需要另一种方法来重置@Published属性。解决此问题的方法是添加另一个名为.onChange(of:perform:)的View Modifier:

Model.swift:

class Model: ObservableObject {
    @Published var isPressedDown: Bool = false
    
    private var cancellableSet = Set<AnyCancellable>()

    init() {
        //对isPressedDown进行某些操作
        $isPressedDown
            .sink { ... }
            .store(in: &cancellableSet)
    }
}

View.swift:

@GestureState private var _isPressingDown: Bool = false
@ObservedObject var model: Model

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
    .updating($_isPressingDown) { value, state, transaction in
        switch value {            
            case .second(true, nil): //这意味着第一个手势已完成
                state = true //更新GestureState
                model.isPressedDown = true //更新@ObservedObject属性
            default: break
        }
    })
    .onChange(of: _isPressingDown) { value in
        if !value {
            model.isPressedDown = false //重置@ObservedObject属性
        }
    })
英文:

I managed to solve it, although if anyone has an easier solution I would gladly accept.

Basically I need to chain 2 LongPressGesture-s together.

The first one will take effect after a 2 second long press - this is when the something should appear.

The second one will take effect after Double.infinity time, meaning that it will never complete, so the user can press as long as they want. For this effect, we only care about the event when it is cancelled - meaning that the user stopped pressing.

@GestureState private var isPressingDown: Bool = false

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
    .updating($isPressingDown) { value, state, transaction in
        switch value {            
            case .second(true, nil): //This means the first Gesture completed
                state = true //Update the GestureState
            default: break
        }
    })
    .
[...]

something.opacity(isPressingDown ? 1 : 0)

When sequencing two LongPressGesture-s by calling the .sequenced(before:) method, you get a

SequenceGesture&lt;LongPressGesture, LongPressGesture&gt; as return value

which has a .first(Bool) and a .second(Bool, Bool?) case in its Value enum.

> The .first(Bool) case is when the first LongPressGesture hasn't ended yet.
>
> The .second(Bool, Bool?) case is when the first LongPressGesture has ended.

So when the SequenceGesture's value is .second(true, nil), that means the first Gesture has completed and the second is yet undefined - this is when that something should be shown - this is why we set the state variable to true inside that case (The state variable encapsulates the isPressingDown variable because it was given as first parameter to the .updating(_:body:) method).

And we don't have to do anything about setting the state back to false because when using the .updating(_:body:) method the state returns to its initial value - which was false - if the user cancels the Gesture. Which will result in the disappearance of "something". (Here cancelling means we lift our finger before the minimum required seconds for the Gesture to end - which is infinity seconds for the second gesture.)

> So it is important to note that the .updating(_:body:) method's callback is not called when the Gesture is cancelled, as per this documentation's Update Transient UI State section.


EDIT 03/24/2021:

I ran into the problem of updating an @Published property of an @ObservedObject in my view. Since the .updating() method closure is not called when resetting the GestureState you need another way to reset the @Published property. The way to solve that issue is adding another View Modifier called .onChange(of:perform:):

Model.swift:

class Model: ObservableObject {
    @Published isPressedDown: Bool = false
    
    private var cancellableSet = Set&lt;AnyCancellable&gt;()

    init() {
        //Do Something with isPressedDown
        $isPressedDown
            .sink { ... }
            .store(in: &amp;cancellableSet)
    }
}

View.swift:

@GestureState private var _isPressingDown: Bool = false
@ObservedObject var model: Model

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
    .updating($_isPressingDown) { value, state, transaction in
        switch value {            
            case .second(true, nil): //This means the first Gesture completed
                state = true //Update the GestureState
                model.isPressedDown = true //Update the @ObservedObject property
            default: break
        }
    })
    .onChange(of: _isPressingDown) { value in
        if !value {
            model.isPressedDown = false //Reset the @ObservedObject property
        }
    })

答案2

得分: 9

以下是您要翻译的内容:

"It seems like there may be a more elegant solution here, although it's not apparent right away.

Instead of using a LongPressGesture, you can use a DragGesture with a minimum distance of 0

@State var pressing = false
Text("Press Me").gesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
pressing = true
})
.onEnded({ _ in
doThingWhenReleased()
})
)

In this example, pressing will be true when you are pressed down, and will turn to false when it's released. The onEnded closure will be called when the item is released. Here is this feature bundled into a ViewModifier to use, although it may be better as a standalone Gesture() if you want it to have a similar api as other gestures:

struct PressAndReleaseModifier: ViewModifier {
@Binding var pressing: Bool
var onRelease: () -> Void

func body(content: Content) -> some View {
    content
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .onChanged{ state in

                    pressing = true
                }
                .onEnded{ _ in
                    pressing = false
                    onRelease()
                }
        )
}

}

extension View {
func pressAndReleaseAction(pressing: Binding, onRelease: @escaping (() -> Void)) -> some View {
modifier(PressAndReleaseModifier(pressing: pressing, onRelease: onRelease))
}
}

You can use it like this

struct SomeView: View {
@State var pressing
var body: some View {
Text(pressing ? "Press Me!" : "Pressed")
.pressAndReleaseAction(pressing: $pressing, onRelease: {
print("Released")
})
}
}

The only downside to this is that if the user drags outside of the button and releases, onRelease() is still fired. This could probably be updated inside the ViewModifier to get the expected outcome, where the user can drag outside to cancel the action, but you might have to use a GeometryReader."

英文:

It seems like there may be a more elegant solution here, although it's not apparent right away.

Instead of using a LongPressGesture, you can use a DragGesture with a minimum distance of 0

@State var pressing = false
Text(&quot;Press Me&quot;).gesture(
    DragGesture(minimumDistance: 0)
        .onChanged({ _ in
            pressing = true
        })
        .onEnded({ _ in
            doThingWhenReleased()
        })
)

In this example, pressing will be true when you are pressed down, and will turn to false when it's released. The onEnded closure will be called when the item is released. Here is this feature bundled into a ViewModifier to use, although it may be better as a standalone Gesture() if you want it to have a similar api as other gestures:

struct PressAndReleaseModifier: ViewModifier {
    @Binding var pressing: Bool
    var onRelease: () -&gt; Void

    func body(content: Content) -&gt; some View {
        content
            .simultaneousGesture(
                DragGesture(minimumDistance: 0)
                    .onChanged{ state in
                        
                        pressing = true
                    }
                    .onEnded{ _ in
                        pressing = false
                        onRelease()
                    }
            )
    }
}

extension View {
    func pressAndReleaseAction(pressing: Binding&lt;Bool&gt;, onRelease: @escaping (() -&gt; Void)) -&gt; some View {
        modifier(PressAndReleaseModifier(pressing: pressing, onRelease: onRelease))
    }
}

You can use it like this

struct SomeView: View {
    @State var pressing
    var body: some View {
        Text(pressing ? &quot;Press Me!&quot; : &quot;Pressed&quot;)
            .pressAndReleaseAction(pressing: $pressing, onRelease: {
                print(&quot;Released&quot;)
            })
    }
}

The only downside to this is that if the user drags outside of the button and releases, onRelease() is still fired. This could probably be updated inside the ViewModifier to get the expected outcome, where the user can drag outside to cancel the action, but you might have to use a GeometryReader.

答案3

得分: 1

我知道现在可能有点晚了,但我找到了另一种处理长按手势的方法。它还可以处理简单的轻触手势。

对于这篇文章中可能遇到的糟糕写作,我感到抱歉。

分为两个部分:

首先,我们希望能够检测到任何视图上的触摸按下事件。为此,我们使用了一个自定义的ViewModifier,可以在这个链接找到。

然后,看下面的代码。

// 1.
@State var longPress = false
@State var longPressTimer: Timer?

[...]

// 2.
Button(action: { 
    if !longPress { 
        // 5.
    }

    // 6. 
    longPress = false
    longPressTimer?.invalidate()
    longPressTimer = nil
}) {
    Color.clear
}
.onTouchDownGesture { // 3.
    // 4.
    longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
        longPress = true
    }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

[...]
    
  1. 我们需要一个布尔值来保存长按状态(按下或未按下)和一个计时器。

  2. 添加一个标准的SwiftUI按钮。按钮操作仅在释放按钮时触发(松开触摸事件),因此在我们的情况下非常有用。

  3. 将自定义的触摸按下修饰符应用于按钮,现在我们可以知道按钮何时被按下和何时被释放。

  4. 在按下触摸时初始化并触发计时器。时间间隔是您希望在将触摸按下事件识别为长按事件之前等待的时间。当计时器结束时,意味着我们现在处于“长按”状态,并将longPress设置为true。

  5. (可选)在按钮释放时,如果按钮被“按得不够长”,我们可以执行所需的按钮默认操作。

  6. 最后,在释放触摸时,取消长按事件并清空计时器。

使用这种方法,您可以实现类似于长按的效果。我的建议是在@State var longPress: Bool上监听didSet事件,但我知道什么呢?

此外,我不知道它是否性能友好,没有检查。但至少它是有效的。

当然,欢迎您们提出的所有改进和想法。
干杯。

英文:

I know it's kinda late, but I found another way to handle long press gesture. It also handle simple tap gesture.

Sorry for the bad writing you might encounter in this post.

It goes down in 2 parts:

First, we want to be able to detect touch down events on any view. For this we use a custom ViewModifier found this thread

Then, see the code below.

// 1.
@State var longPress = false
@State var longPressTimer: Timer?

[...]

// 2.
Button(action: { 
    if !longPress { 
        // 5.
    }

    // 6. 
    longPress = false
    longPressTimer?.invalidate()
    longPressTimer = nil
}) {
    Color.clear
}
.onTouchDownGesture { // 3.
    // 4.
    longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
        longPress = true
    }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

[...]
    
  1. We will need a boolean to hold the long press state (pressed or not). And a Timer.

  2. Add a standard SwiftUI Button. A Button action is only triggered when the button is released (touch up event), so very useful in our case.

  3. Apply the custom touch down modifier to the button, we are now able to tell when the button is pressed & when it is released.

  4. Init & fire the Timer at touch down. The time interval is the time you want to wait before recognising a touch down event as a long press event. When the timer ends, it means we are now in a "long press" state and we set longPress to true.

  5. (optional) At button touch up, if the button was not tapped "long enough", we execute the work we want for the button default action, if desired.

  6. Finally, still at touch up, cancel long press event & empty the timer.

With this method you achieve a long press like effect. My advice here is to listen to the didSet event on the @State var longPress: Bool, but what do I know ?

Also, I don't know if it's perf friendly, didn't check. But at least it works.

All improvements and ideas you guys might have are of course welcome.
Cheers.

答案4

得分: -1

这是一个解决方案,如果您想在释放按钮后执行操作

struct ContentView: View {

    var body: some View {
        Text("长按我")
            .onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: handlePressing) {}
    }

    func handlePressing(_ isPressed: Bool) {
        guard !isPressed else { return }
        //MARK: - 如果未按下则处理
        print("未按下")
    }
}
英文:

Here is a solution if you want to fire action after release button

struct ContentView: View {

    var body: some View {
        Text(&quot;Tap me long&quot;)
        .onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: handlePressing) {}
    }

    func handlePressing(_ isPressed: Bool) {
        guard !isPressed else { return }
        //MARK: - handle if unpressed
        print(&quot;Unpressed&quot;)
    }
}

huangapple
  • 本文由 发表于 2020年1月3日 21:44:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/59579669.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定