SwiftUI TapGesture onStart / TouchDown - swift

I am using the TapGesture in SwiftUI for MacOS. TapGesture is only recognized on TouchInsideOut event, when releasing the press again. I want to call an action on touchdown and another on the end gesture.
There is a onEnded state available for TapGesture but no onStart.
MyView()
.onTapGesture {
//Action here only called after tap gesture is released
NSLog("Test")
}
Is there any chance to detect touch down and touch release?
I tried using LongPressGesture aswell, but couldn't figure it out.

If by pure SwiftUI then only indirectly for now.
Here is an approach. Tested with Xcode 11.4.
Note: minimumDistance: 0.0 below is important !!
MyView()
.gesture(DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
.onChanged { _ in
print(">> touch down") // additional conditions might be here
}
.onEnded { _ in
print("<< touch up")
}
)

The selected answer generates a continuous stream of events on touch, not just one on touch-down.
This works as requested:
MyView()
.onLongPressGesture(minimumDuration: 0)
{
print("Touch Down")
}

Related

How to catch all events in Swift without breaking other event handlers

How can I catch any touch up event in my application view without affecting any other event in subview or subview of the subview?
Currently whenever I add UILongPressGestureRecognizer to my root view, all other addTarget functions break in subviews and their subviews.
func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith: UIGestureRecognizer) -> Bool {
return true
}
override func viewDidLoad() {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
button.setTitle("Click me", for: .normal)
button.addTarget(self, action: #selector(buttonClick), for: .touchDown)
self.view.addSubview(button)
initLongTapGesture() // This kills the button click handler.
}
func initLongTapGesture () {
let globalTap = UILongPressGestureRecognizer(target: self, action: #selector(tapHandler))
globalTap.delegate = self
globalTap.minimumPressDuration = 0
self.view.addGestureRecognizer(globalTap)
}
#objc func tapHandler(gesture: UITapGestureRecognizer) {
if ([.ended, .cancelled, .failed].contains(gesture.state)) {
print("Detect global touch end / up")
}
}
#objc func buttonClick() {
print("CLICK") // Does not work when initLongTapGesture() is enabled
}
The immediate solution to allow the long press gesture to work without preventing buttons from working is to add the following line:
globalTap.cancelsTouchesInView = false
in the initLongTapGesture function. With that in place you don't need the gesture delegate method (which didn't solve the issue anyway).
The big question is why are you setting a "long" press gesture to have a minimum press duration of 0?
If your goal is to monitor all events in the app then you should override the UIApplication method sendEvent. See the following for details on how to subclass UIApplication and override sendEvent:
Issues in overwriting the send event of UIApplication in Swift programming
You can also go through these search results:
https://stackoverflow.com/search?q=%5Bios%5D%5Bswift%5D+override+UIApplication+sendEvent
Another option for monitoring all touches in a given view controller or view is to override the various touches... methods from UIResponder such as touchesBegan, touchesEnded, etc. You can override these in a view controller class, for example, to track various touch events happening within that view controller.
Unrelated but it is standard to use the .touchUpInside event instead of the touchDown event when handing button events.

SwiftUI: Sequence gestures starting with tap

I want to create the "one finger zoom gesture" from the iOS Maps App. So a tap (touch down, then touch up), then long press (only touch down), and finally a drag up/down.
I was expecting something like this:
let tapGesture = TapGesture().onEnded { _ in
print("tap")
}
let longPressGesture = LongPressGesture(minimumDuration: 0.00001)
.onChanged { _ in
print("long press update")
}
.onEnded { _ in
print("long press")
}
let dragGesture = DragGesture(minimumDistance: 1)
.onChanged { gesture in
print("drag")
}
let combined = tapGesture.sequenced(before: longPressGesture).sequenced(before: dragGesture)
SomeView()
.gesture(combined)
Only the LongPressGesture after the initial tap never gets called. I've tried a LongPressGesture instead of the initial TapGesture, but two sequential long presses are called instantly (so the "touch up" event is not regarded).
An initial DragGesture(minimumDuration: 0) with onEnded and a corresponding #State to track the initial tap might work, but I have a ScrollView beneath (for the map-like panning and zooming), so I'm guessing a DragGesture cannot be the first one.
I also tried setting a gestureEnabled flag after a first tap via onTapGesture() and then using the .gesture(combined, including: gestureEnabled ? .all : .none), with a few more edge cases handled I managed to get it working, only to find out that double taps no longer worked anywhere else on the View.
I should clarify that there's a ScrollView with text underneath the view, so long press, double tap, scrolling (horizontal and vertical) should all keep working.
Any ideas?
Currently, my best alternative is just using the long press + drag. It works with the additional tap as well (since it's not necessary in that case). Only downside is it also triggers if the user holds down for a short time before attempting a scroll.

Tap on SwiftUI LocationButton in UI Test

I am unable to tap on the LocationButton in a Swift UI Test. This is the button:
I am in a breakpoint in a UITest and I tried this without luck:
app.buttons.count returns 0
app.staticTexts["Current Location"].exists returns false
app.buttons["Current Location"].exists returns false
Is there something special about this button? How can I tap it in a UITest?
VStack {
LocationButton(.shareCurrentLocation) {
locationManager.requestLocation()
}
.cornerRadius(30)
.symbolVariant(.fill)
.foregroundColor(.white)
}
.accessibilityIdentifier("buttonStack")
let button = app.otherElements["buttonStack"]
Found workaround with this way but not pretty.

SwiftUI iOS - Adding Double tap cause Single tap called after a delay

I want to recognize Single tap and Double tap on the Capsule below. This code works fine :
Capsule()
.frame(width: 100, height: 42)
.onTapGesture(count: 1) {
print("Single Tap recognized instantly")
}
But When I'm adding the .onTapGesture(count: 2) to it Single tap called after 0.25ms.
Capsule()
.frame(width: 100, height: 42)
.onTapGesture(count: 2) {
print("Double tap recognized instantly")
}
.onTapGesture(count: 1) {
print("Single Tap recognized after 0.25ms")
}
How can I fix this?
Just to make one thing clear: You cannot have a single tap-gesture that should be recognized together with a double tap gesture when your goal is to detect both exclusively (i.e. the system should differentiate between a single tap and a double tap with no delay) since a single tap could theoretically always lead to a double tap. This means you always have to wait a certain period (of 0.25 ms as it seems in your case) to rule out the user intended to double-tap.
However, when your goal is to have a single tap-gesture together (in other words simultaneously) with the double-tap gesture, this is now completely feasible.
A gesture is, like so many things in SwiftUI a protocol and custom gestures can be built in a lot of ways like the body of a view. In your case, we want a double tap-gesture that runs simultaneously with a single tap gesture. The simplest way to this, you declare a computed property with an opaque return type (some Gesture) and describe your gesture in that property:
var tapGesture: some Gesture {
TapGesture(count: 2)
.onEnded {
print("Double tap")
}
.simultaneously(with: TapGesture(count: 1)
.onEnded {
print("Single Tap")
})
}
You can simply add this gesture to whatever view you desire, just like you would do the same thing with any other gesture:
Capsule()
.gesture(tapGesture)

#IBSegueAction with condition

In my app I want to perform a segue based on a UITapGestureRecognizer. In case the tap is in the top area of the screen, a segue to the SettingsView should be performed.
In UIKit this was quite simple. I've triggered the performSegue in the UITapGestureRecognizer by wrapping it in an if-statement.
Now I would like to write the SettingsView() in SwiftUI. The SettingsView() will be embedded in a UIHostingController.
My question is:
How can I perform the segue to this UIHostingController (while telling the UIHostingController which View to display)?
I tried to use the new #IBSegueAction. The reason to use this #IBSegueAction is that I can use it to tell the UIHostingController which View to display. The problem is that I can't insert a condition now. Wherever the tap is on the screen, the segue is performed. I haven't found a way to cancel the segue in #IBSegueAction.
My code currently looks like this:
#IBSegueAction func showSettingsHostingControler(_ coder: NSCoder, sender: UITapGestureRecognizer, segueIdentifier: String?) -> UIViewController? {
let location = sender.location(in: self.tableView)
if let _ = self.tableView.indexPathForRow(at: location) {
return nil
} else {
if location.y < 32 {
if location.x < view.bounds.midX {
direction = .left
} else {
direction = .right
}
return UIHostingController(coder: coder, rootView: SettingsView())
}
}
return nil
}
The result currently is that the app segues to a non-existing Nil-view when the tap is in the wrong area.
based on a UITapGestureRecognizer. In case the tap is in the top area of the screen
It sounds to me like your tap gesture recognizer is attached to the wrong view. There should not be any decision to make here. Position a view in the top area of the screen and attach the tap gesture recognizer to that. That way, if this view gets a tap, there is no decision to be made: the tap is in the right place.