Detect any tap outside the current view - ios

Is there a way to detect any tap outside the current view? I unsuccessfully tried to implement something with the hitTest method but I am not sure to understand it well.

What you have to do is, In touchesBegan you have to get the first touch object from the touches set and you have to check the location of that touch in a view(inside which you want to detect the touch).
After you get the location of touch in View, you have to check whether your currentView(The view which you have to check whether tap was inside or outside).
If currentView's frame contains the touch location, that means touch has occurred inside the view otherwise outside.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch = touches.first
guard let location = touch?.location(in: self.view) else { return }
if !currentView.frame.contains(location) {
print("Tapped outside the view")
} else {
print("Tapped inside the view")
}
}
Hope it Helps!

You can use UIGestureRecognizerDelegate protocol
extension YourViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch) -> Bool {
return (touch.view === self.view)
}
}
This will only returns "true" when the touch was on the background view but "false" if it was inside the your View.
Note : The identity operator === to compare touch.view with self.view. You want to know whether both variables refer to the same object.
And in viewDidLoad() you will create the gestureRecognizer and set delegate.
let gestureRecognizer = UITapGestureRecognizer(target: self,action: #selector(yourActionMethod))
gestureRecognizer.cancelsTouchesInView = false
gestureRecognizer.delegate = self
view.addGestureRecognizer(gestureRecognizer)

Related

Capturing a scroll wheel event in MacCatalyst

I have been able to capture the mouse scroll wheel with a UIPanGestureRecognizer.
`
let tPan = UIPanGestureRecognizer(target: self, action: #selector(self.wasPanned))
tPan.maximumNumberOfTouches = 1
tPan.minimumNumberOfTouches = 1
if #available(macCatalyst 13.4, *) {
tPan.allowedScrollTypesMask = .discrete
}
self.addGestureRecognizer(tPan)
}
#objc func wasPanned(_ pan: UIPanGestureRecognizer) {
let thispan = pan
print(pan)
}
`
However, this captures any pan action. How can I determined if this was a consequence of the mouse scroll wheel?
You can check the event passed to the delegate method gestureRecognizer:shouldReceiveEvent: and only accept the event whose type is scroll:
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool {
event.type == .scroll
}
}
Or you can simply set the property allowedTouchTypes to an empty array, which means all touch types are ignored:
gestureRecognizer.allowedTouchTypes = []

Preventing touch passing from UIControl to ParentView

I have created a custom control for Uber like OTP TextField. I want to consume the touch in my control and not let it propagate through the UIResponder Chain. So I have overridden the methods as described in apple documentation.
extension BVAPasswordTextField {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
becomeFirstResponder()
}
override func touchesMoved(Set<UITouch>, with: UIEvent?) {
}
override func touchesEnded(Set<UITouch>, with: UIEvent?) {
}
override func touchesCancelled(Set<UITouch>, with: UIEvent?) {
}
override func touchesEstimatedPropertiesUpdated(Set<UITouch>) {
}
}
Now in some view controller I want to dismiss the keyboard when the user taps anywhere outside my custom control.
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecogniser = UITapGestureRecognizer(target: self, action: #selector(ViewController.backgroundTapped))
tapGestureRecogniser.numberOfTapsRequired = 1
view.addGestureRecognizer(tapGestureRecogniser)
// Do any additional setup after loading the view, typically from a nib.
}
#objc func backgroundTapped() {
self.view.endEditing(true)
}
But whenever I tap on the textfield backgroundTapped also gets called.
Note:- It is a control where based on enum values you can create different UI Components for taking input. So this control can be shared among the whole team... I won't be the only guy using it.... So I want it to behave exactly like UITextfield in touch scenario
You can handle the tap gesture using UITapGestureRecognizerDelegate. It allows you to decide whether or not gesture should begin. In your case, it should be done basing on the location of the touch.
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.view)
// return true is location of touch is outside our textField
return !textField.frame.contains(location)
}
}
Just make sure you've set a delegate for your gesture at viewDidLoad
tapGestureRecogniser.delegate = self
UPDATE
If I got your setup right, you have a view and some subviews that are used for input. And you want to resignFirstResponder when this super-view is touched. In that case you can use gestureRecognizerShouldBegin like that
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.view)
for subview in self.view.subviews {
if subview.frame.contains(location) {
return false
}
}
return true
}
In my opinion, if you want to handle some view interaction in a specific way, you need to do it using that specific view. And what you're doing now feels like trying to change behavior on one view, using another view.

Unproblematic use of UIPinchGestureRecognizer with UILongTapGestureRecognizer

I need to use a UILongTapGestureRecognizer with a UIPinchGestureRecognizer simultaneously.
Unfortunately, the first touch of the UILongTapGestureRecognizer will be counted for the PinchGestureRecognizer too. So when holding UILongTapGestureRecognizer there just needs to be one more touch to trigger the Pinch Recognizer. One is used for long press gesture and two for the pinch.
Is there a way to use both independently? I do not want to use the touch of the UILongTapGestureRecognizer for my UIPinchGestureRecognizer.
This is how I enable my simultaneous workforce:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
//Allowing
if (gestureRecognizer == zoom) && (otherGestureRecognizer == longTap) {
print("working while filming")
return true
}
return false
}
I don't believe you have the tool for what you are looking for so I suggest you try to create your own gesture recognizer. It is not really that hard but you will unfortunately need to do both the long press and the pinch effect.
Don't try overriding UIPinchGestureRecognizer nor UILongPressGestureRecognizer because it will simply not work (or if you mange it please do share your findings). So just go straight for UIGestureRecognizer.
So to begin with long press gesture recognizer we need to track that user presses and holds down for long enough time without moving too much. So we have:
var minimumPressDuration = UILongPressGestureRecognizer().minimumPressDuration
var allowableMovement = UILongPressGestureRecognizer().allowableMovement
Now touches need to be overridden (this is all in subclass of gesture recognizer):
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
touchMoveDistance = 0.0 // Reset the movement to zero
previousLocation = touches.first?.location(in: view) // We need to save the previous location
longPressTimer = Timer.scheduledTimer(timeInterval: minimumPressDuration, target: self, selector: #selector(onTimer), userInfo: nil, repeats: false) // Initiate a none-repeating timer
if inProgress == false { // inProgress will return true when stati is either .began or .changed
super.touchesBegan(touches, with: event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if let newLocation = touches.first?.location(in: view), let previousLocation = previousLocation {
self.previousLocation = newLocation
touchMoveDistance += abs(previousLocation.y-newLocation.y) + abs(previousLocation.x-newLocation.x) // Don't worry about precision of this. We can't know the path between the 2 points anyway
}
if inProgress == false {
super.touchesMoved(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
longPressTimer?.invalidate()
longPressTimer = nil
if inProgress {
state = .ended
}
super.touchesEnded(touches, with: event)
if self.isEnabled { // This will simply reset the gesture
self.isEnabled = false
self.isEnabled = true
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
longPressTimer?.invalidate()
longPressTimer = nil
if inProgress {
state = .ended
}
super.touchesCancelled(touches, with: event)
if self.isEnabled {
self.isEnabled = false
self.isEnabled = true
}
}
So these are all only for long press. And what happens on timer is:
#objc private func onTimer() {
longPressTimer?.invalidate()
longPressTimer = nil
if state == .possible {
state = .began
}
}
So basically if we change state to .begin we trigger the gesture and rest of the events simply work. Which is pretty neat.
Unfortunately this is far from over and you will need to play around a bit with the rest of the code...
You will need to preserve touches (If I remember correctly the same touch will be reported as a comparable object until user lifts his finger):
On begin save the touch to a private property longPressTouch
On timer when long press succeeds set a property to indicate the long press has indeed triggered didSucceedLongPress = true
On begin check if another touch can be added and cancel gesture if it may not if longPressTouch != nil && didSucceedLongPress == false { cancel() }. Or allow it, this really depends on what you want.
On begin if touches may be added then add them in array and save their initial positions pinchTouches.append((touch: touch, initialPosition: touchPosition))
On touches end and cancel make sure to remove appropriate touches from array. And if long press is removed cancel the event (or not, your choice again)
So this should be all the data you need for your pinch gesture recognizer. Since all the events should already be triggering for you the way you need it all you need is a computed value for your scale:
var pinchScale: CGFloat {
guard didSucceedLongPress, pinchTouches.count >= 2 else {
return 1.0 // Not having enough data yet
}
return distanceBetween(pinchTouches[0].touch, pinchTouches[1].touch)/distanceBetween(pinchTouches[0].initialPosition, pinchTouches[1].initialPosition) // Shouldn't be too hard to make
}
Then there are some edge cases you need to check like:
user initiates a long press, uses 2 fingers to pinch, adds 3rd finger (ignored), removes 2nd finger: Without handling this you might get a little jump which may or may not be intended. I guess you could just cancel the gesture or you could somehow transform the initial values to make the jump disappear.
So good luck if you will be implementing this and let us know how it went.

UIPinchGestureRecognizer doesn't fire after custom UIGestureRecognizer fails

I have a custom UIGestureRecognizer subclass that detects a particular kind of one-finger dragging. On the same scene, I also have a UIPinchGestureRecognizer, that detects two-finger pinches.
The problem happens when the user makes a pinch gesture but puts down one finger an instant before the other: the custom gesture recognizer sees a single touch and engages (sets its state to .began) and the pinch recognizer doesn't. When the second touch is added, the custom recognizer notices and switches its state to .failed. But it's too late and the pinch gesture recognizer doesn't pick it up.
Here's the code for the custom UIGestureRecognizer subclass:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
guard touches.count == 1 else {
state = .failed
return
}
state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard touches.count == 1 else {
state = .failed
return
}
// do gesture recognizer stuff here
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = .ended
}
I've tried it with cancelsTouchesInView set to false and with delaysTouchesBegan set to true for the custom gesture recognizer. Neither made a difference in this behavior.
When I use a UIPinchGestureRecognizer with a UIPanGestureRecognizer I don't have this issue, which leads me to think it's not the intended behavior.
What's wrong with my UIGestureRecognizer fail code and how can I fix it so that a UIPinchGestureRecognizer can still recognize pinches where the touches start non-simultaneously?
Try using the UIGestureRecognizerDelegate protocol to implement gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:). That can allow you to have two gestures be recognized simultaneously.
class MyVC: UITableViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchFrom))
pinchGestureRecognizer.delegate = self
view.addGestureRecognizer(pinchGestureRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

How to detect the location of my finger through in a scrollview or webview in Swift? (using touchesMoved if possible)

I want to print the location of the user's finger on a webview.
Using this (below) doesn't work on a webview
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.view) // even when i replace this part with UIWebview
print(position.x)
print(position.y)
}
}
I think that creating a UIPanGestureRecognizer should solve it, add the following code into viewDidLoad() method:
override func viewDidLoad() {
// ...
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(webViewtouchMoved(panGesture:)))
panGesture.delegate = self
webView.addGestureRecognizer(panGesture)
// ...
}
webViewtouchMoved(panGesture:) method:
func webViewtouchMoved(panGesture: UIPanGestureRecognizer) {
if panGesture.state == .began || panGesture.state == .changed {
let position = panGesture.location(in: view)
print(position.x)
print(position.y)
}
}
also, you should add this extension for the desired ViewController:
// change 'ViewController' to your class name:
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
hope this helped.

Resources