I filed the inability to achieve the above in SwiftUI with Apple, and this is the response I got:
@GestureState is the way to clean up after cancelled gestures.
onEnded() tells you when it completes. The combination of the two
should let you implement the things described.
This wasn't helpful, at least based on my understanding on what was proposed. When I added the following:
@GestureState var tapGesture = false
And:
.onDrag {
handler()
}
.simultaneousGesture(DragGesture()
.updating($tapGesture) { _,_,_ in
print("updating")
}
.onEnded { _ in
print("ended")
})
The print statements never occurred on a drag.
If however I did:
.onDrag {
handler()
}
.simultaneousGesture(LongPressGesture()
.updating($tapGesture) { _,_,_ in
print("updating")
}
.onEnded { _ in
print("ended")
})
The long press gesture intercepts and stops the drag. Changing the minimum duration didn’t seem to make a difference. Nor the order in which the gesture modifiers are sequenced. Or if the additional “dummy” drag is set to be a high priority gesture.
So I ended up with a not-perfect creative workaround.
Firstly, to the root view I added a cancellation delegate:
struct RootView: View {
static var cancellation = CancellationDelegate()
...
.onDrop(of: [UTType.text], delegate: Self.cancellation)
Its purpose is to ensure that the draggable view is immediately surrounded by a potential drop target. I have a view model for it that keeps a record for when a drop target has been entered.
import SwiftUI
import UniformTypeIdentifiers
import ThirdSwiftLibs
struct CancellationDelegate: DropDelegate {
var viewModel: CancellationViewModel = .init()
// MARK: - Event handlers
/// Dragged over a drop target
func dropEntered(info: DropInfo) {
viewModel.dropLastEntered = .now
}
/// Dragged away from a drop target
func dropExited(info: DropInfo) {
}
/// Drag begins
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
// MARK: - Mutators
/// The dragged entity is dropped on a target
/// - Returns: true if drop was successful
func performDrop(info: DropInfo) -> Bool {
SchedulerViewModel.shared.reset()
return true
}
// MARK: - Checkers
/// Checks whether the drag is considered valid based on
/// the view it’s attached to
/// - Returns: true if view is drag/droppable
func validateDrop(info: DropInfo) -> Bool {
/// Allow the drop to begin with any String set as the NSItemProvider
return info.hasItemsConforming(to: [UTType.text])
}
}
class CancellationViewModel {
var dropLastEntered: Date?
}
In the card’s drag handler, I check after 3 seconds whether the card has moved. I can tell by referring to the cancellation’s dropLastEntered value. If it’s been less than 3 seconds, then it’s reasonable to assume it has moved and so there’s nothing I need to do. However if it’s been 3 or more, then it’s equally safe to assume that the drag was released at some point within the last 3 seconds, and so I should use this as a cue to reset the app state.
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if let last = RootView.cancellation.viewModel.dropLastEntered {
if last.seconds(from: .now) <= -3 {
// reset UI because we last moved a card over the dummy drop target 3 or more seconds ago
}
}
else {
// reset UI because we’ve never moved any card over the dummy drop target
}
}
The reason this isn’t perfect is because the UI reset doesn’t happen immediately. In tests, 3 seconds appears to be around when the system decides to de-activate the drag on the view due to no movement detected (the view no longer appears in the drag state the system originally activated). So, from this perspective 3 seconds is good. However, if I was to release the card myself after 1 second without any movement, then it will be another 2 seconds (3-1) before the UI realises it needs to reset due to the drag being de-activated.
Hope this helps. If someone has a better solution, I’m all ears!
dragging
? What do you do on that flag triggered? – Trumpdragging = true
even if turn it to off for first time. – Trump