英文:
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<LongPressGesture, LongPressGesture>
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<AnyCancellable>()
init() {
//Do Something with 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): //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
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("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<Bool>, 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.
答案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)
[...]
-
我们需要一个布尔值来保存长按状态(按下或未按下)和一个计时器。
-
添加一个标准的SwiftUI按钮。按钮操作仅在释放按钮时触发(松开触摸事件),因此在我们的情况下非常有用。
-
将自定义的触摸按下修饰符应用于按钮,现在我们可以知道按钮何时被按下和何时被释放。
-
在按下触摸时初始化并触发计时器。时间间隔是您希望在将触摸按下事件识别为长按事件之前等待的时间。当计时器结束时,意味着我们现在处于“长按”状态,并将longPress设置为true。
-
(可选)在按钮释放时,如果按钮被“按得不够长”,我们可以执行所需的按钮默认操作。
-
最后,在释放触摸时,取消长按事件并清空计时器。
使用这种方法,您可以实现类似于长按的效果。我的建议是在@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)
[...]
-
We will need a boolean to hold the long press state (pressed or not). And a Timer.
-
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.
-
Apply the custom touch down modifier to the button, we are now able to tell when the button is pressed & when it is released.
-
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.
-
(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.
-
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("Tap me long")
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: handlePressing) {}
}
func handlePressing(_ isPressed: Bool) {
guard !isPressed else { return }
//MARK: - handle if unpressed
print("Unpressed")
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论