UISystemGateGestureRecognizer and delayed taps near bottom of screen - ios

What are the standard UISystemGestureGateGestureRecognizers installed on the top level UIView of an iOS app for?
My app consists of two views - one fills the top half of the screen, the other is a custom keyboard and fills the bottom half. I found that taps on the space bar didn't always work and after some investigation found that the timing of tap events in the bottom 20 pixels or so was different to the rest of the view. For most of the view the period between touchesBegan/Ended was about 100ms, where as for the space bar it was 1-2ms. (My app is an emulator and this is too fast for it to detect the key press).
After some more digging I found the main UIView of the application (ie: my main view's superview) has 2 UISystemGestureGateGestureRecognizer's installed. By removing them in ViewDidAppear the bottom of the screen is no longer affected. (Presumably these are cancelling the touch press events to my keyboard hence the faster timing).
These system recognizers are present on at least iOS 5 through 7 and on both iPad and iPhone. I thought they may be related to swipe from top/bottom but this functionality still works with them removed.
So I have a fix, but I'd like to understand more about what's going on here - in particular what I might be breaking by removing these.

This delayed touches bothered me too.
Just as an addition to what's said before,
here's a simple fix:
override func viewDidAppear(_ animated: Bool) {
let window = view.window!
let gr0 = window.gestureRecognizers![0] as UIGestureRecognizer
let gr1 = window.gestureRecognizers![1] as UIGestureRecognizer
gr0.delaysTouchesBegan = false
gr1.delaysTouchesBegan = false
}
no need to remove those gesture recognizers.
just add this to the main view controller.

It appears that these recognizers are meant to prevent accidental touches near the top and bottom of the screen. They aren't configured with any targets, but can (like any UIResponder) absorb touches to prevent them from being passed up the responder chain.
Notes (tested on iOS 7.1):
Both gesture recognizers are always present in the key window.
I inspected both gestures' _targets ivar, and found they aren't configured with any targets at all. Swizzled out addTarget:action: to verify that targets weren't being added or removed on the fly.
delegate is always nil for both instances.
If you disable the gesture recognizers, they will re-enable themselves
The gesture that that doesn't delay content touches fires when you drag up from the bottom or drag down from the top. I couldn't trigger the instance that delays touches.

Related

Swift iOS sometimes not detecting touches

I am creating an iOS keyboard extension with UIInputViewController, and using UIButton for each key. I have two issues:
Sometimes buttons don't detect touches.
Touches at the edges of the screen are recognized with a delay.
I assume first one happens when I type too fast, or when touch area is too big (bigger than the button). I tried using different methods of detecting touch (button target, touchesBegan, touchesEnded, UITapGestureRecognizer) and all have the same issue.
I assume second happens because of the iOS edge false touch rejection. I tried changing preferredScreenEdgesDeferringSystemGestures but it had no effect.
Please help.
Turns out I was playing a quick animation on the UIButton press, so I had to set .allowUserInteraction in the animation options.
I used UILongPressGestureRecognizer with minimumPressDuration = 0 which, for some reason, is always instant even at the edges.

Accessibility voiceover single swipe gestures in Swift IOS

I am working on the IOS application, related to voice over, my Question is : When accessibility voice over was enabled how can i get the swipe gestures left, right, top and down, what re the function for detecting these in swift?
First of all, you need to let VoiceOver know that about your view (or another element). So if you are in a view controller, this should work: self.view.isAccessibilityElement = true
Second, you need to let VoiceOver know that your view will handle user interactions on its own: self.view.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction. After that your view should start getting gestures notifications.
Here's another relevant answer: https://stackoverflow.com/a/20712889/2219578
It isn't possible to catch the left, right, top and bottom VoiceOver gestures : I've seen neither a protocol nor a kind of notification for this.
However, you can detect a scrolling action and be aware of the element focus provided by VoiceOver.

Duplicate UIScrollView cancel behavior with UIPanGestureRecognizer

My app uses a paged horizontal scroll view. Each page has UIControls which the user can tap.
UIScrollView does a good job of handling cancellation of touches and swipes. If the user starts swiping fast enough, it's always a swipe. If the user touches down long enough to activate the highlighted state, the scroll view doesn't attempt to swipe.
I'm trying to duplicate this behavior with a UIPanGestureRecognizer subclass so that I can respond to downward swipes within my scrollview. However, I can't get the gesture to cancel in the event of the UIControls getting highlighted.
So far I've done the following:
self.refreshGesture.cancelsTouchesInView = YES;
self.refreshGesture.delaysTouchesBegan = NO;
self.refreshGesture.delaysTouchesEnded = NO;
This seems to duplicate the way UIScrollView passes touches to views, but it doesn't duplicate the way that UIScrollView's pan gesture recognizer gets cancelled. self.refreshGesture is always triggered no matter how slowly the user swipes, or what the state of the UIControls are.
I've tried setting the delegate on my gesture, and this may be the way to go. But I haven't found a combination that works. For example, just checking if the touch starts within a UIControl cancels too frequently. I've also tried overriding gestureRecognizerShouldBegin in my controls, but this seems like a hack and has far reaching implications (interferes with UITextView's gestures, for example).
In this GIF, you can see that the control activates on touch, and the scrollview cancels scroll if that happens. But my downward pan gesture is not cancelled in the same manner:
I wasn't able to duplicate this exactly, but there are two possibilities suggested by WWDC 2014 #235.
Add a transparent scrollview over your main content and move its gesture recognizer onto your root view. This is what did. It let me use UIScrollViewDelegate which ended up being sufficient.
Use a "timeout" gesture recognizer. The video suggests requiring the timeout gesture to fail, but in my case it worked better to use a long press gesture and cancel my pan if the long press fired. 0.1 seconds seemed to work better than their suggested 0.15 seconds.

Prevent reordering of elements in gestureRecognizers array

I'm experiencing a bug in my app that is causing gestures to stop working that I previously added to a UITextField via addGestureRecognizer:. Essentially, I add a tap and long press gesture recognizer to the UITextField (which already has 7 gesture recognizers applied from iOS). When logging self.textField.gestureRecognizers, it shows the existing 7 gestures and then the two I added at the end of the array. The gestures work just like I expected.
However, when I present a modal view controller and then dismiss it, my two gestures stop working on the text field. I'm not sure exactly why, but the view does disappear and it resignsFirstResponder (the keyboard is always up when the modal VC is presented) which may be related. But I discovered the gestures aren't removed from the text field, but the order of the gestures in the array has changed. My custom gestures are now located at index 0 and 1 instead of 7 and 8. I believe the 7 default gestures are conflicting/overriding my custom ones (I assume later placement in the array overrides those before it) which explains why they stop working even though they're still applied.
My questions are:
- Do you know why it is reordering the elements in self.textField.gestureRecognizers?
- How do I prevent that from occurring to ensure my custom gestures always work, without breaking the default gestures for UITextField?
My current solution is to add the two gestures for the first time then store the array of total (9) gestures, then in viewDidAppear I change the gestureRecognizers array (yes it is settable) to my stored array. This guarantees the array will be the 7 built-in gestures followed by my two custom gestures in that order. But I discovered my gestures are overriding the default gestures (that bring up the popup to Cut, Copy, etc), so I have to reset the gestures back to the default 7 after my custom gesture occurs (which is just fine - I only need to trigger the action a single time after recognizing my custom gesture). Simple enough to do - I store the original gestures in a property as well. But this doesn't feel like the best solution. I'd prefer to figure out the cause and address that or go about the situation differently instead of duct-taping the code together.
My first solution was to always add my two gestures in viewDidAppear
viewDidAppear: is called when your view controller's view first appears, but it is also called again later when the presented view controller is dismissed.
Thus you are adding the gesture recognizers twice.
The simplest solution is to use a BOOL instance variable (we call this a "flag") which you set to YES the first time and test afterwards:
if (!self.addedGestures) {
self.addedGestures = YES;
// ... add them! ...
}
Now you will only add them once.
(On the other hand it might be argued that if you care about the order of the gesture recognizers in the array you are already doing something wrong. Use delegate methods to resolve conflicts between gesture recognizers - that's what they are for.)

How to make UIView stop receiving touch events?

I'm working on an app where the user is expected to rapidly touch and swipe across multiple UIViews, each of which is supposed to do an action once the user's finger has reached it. I've got a lot of views and so the typical thing to do, where I'd iterate over each view to see if a touch is inside of its bounds, is a no-go - there's just too much lag. Is there any other way to get touch events from one view to another (that is beside the first one)? I thought maybe there is some way to cancel the touch event, but I've searched and so far have come up empty.
One of the big problems I have is that if I implement my touch handling in my view controller, touchesBegan only fires for the first touch - if the user touches something and then, without moving the first finger, taps on something else, that tap is not recorded in either touchesBegan or touchesMoved. But if I implement my touch handling in the UIViews themselves, once a view registers a touch, if the user does not lift their finger up and moves it, the views around the first view do not register the touch. Only if the user lifts his finger and then puts it back down will the surrounding views register the touch.
So my question is, lets say I have two views side by side, my touch handling code is implemented in the views, and I put my finger down on view 1. I then slide my finger over to view 2 - what do I need to do to make view 2 register that touch, which started in view 1 and never "ended"?
Set userInteractionEnabled property of UIView to NO.
view.userInteractionEnabled = NO;
UIView has the following property:
#property(nonatomic, getter=isUserInteractionEnabled) BOOL userInteractionEnabled
Ok, I figured out what was going on. Thing is, I have my views as subviews of a scrollview, which is itself a subview of my main view. With scrollEnabled = NO, I could touch my subviews - but apparently the scrollview was only forwarding me the initial touch event, and all subsequent touches were part of that initial event. Because of that, I had many weird problems such as touching two views one after the other, both would select and highlight, but if I took the first finger off the screen both views would de-select. This was not the desired behavior.
So what I did is I subclassed the scrollview and overrode the touch handling methods to send the events to its first responder, which is its superview, which is the view where I'm doing my touch handling. Now it works!

Resources