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