I'm trying to fix a bug that involves UIView hitTest:withEvent: being called on my view when the touches are on the UIKeyboard, but only after the app has been in the background.
It was occurring in my app with a complex view hierarchy so I reproduced it in an app with only 2 views:
1 UIView 768x1024 (fullscreen)
1 UITextView 200x200 in the upper half of the fullscreen view
The behavior is as follows:
Tapping the textview causes the fullscreen view's hitTest method to fire, the textfield becomes first responder, and then the keyboard appears all as expected. Tapping on keyboard keys works fine.
Now dismiss the keyboard.
Send the app to the background.
Then resume the app.
Make the textview first responder again. Here's the trouble, now when tapping keys on the keyboard the fullscreen view's hitTest method is firing.
I'm seeing this on an iOS 5 iPad 2. Only on device though, never in the simulator. Any idea why hitTesting could get messed up in this way? Thanks.
The issue described above seems to be caused by the keyboard's UIWindow getting stuck in a bad state. Ensuring that the keyboard window's hidden property gets set to YES (even if it is already YES) fixes the problem for me. This can be done in your UIApplicationDelegate class:
- (void)applicationWillEnterForeground:(UIApplication *)application {
// The keyboard sometimes disables interaction when the app enters the
// background due to an iOS bug. This brings it back to normal.
for (UIWindow *testWindow in [UIApplication sharedApplication].windows) {
if (!testWindow.opaque && [NSStringFromClass(testWindow.class) hasPrefix:#"UIText"]) {
BOOL wasHidden = testWindow.hidden;
testWindow.hidden = YES;
if (!wasHidden) {
testWindow.hidden = NO;
}
break;
}
}
}
The class name of the keyboard window, at least in iOS 5 with a standard US keyboard, is UITextEffectsWindow. As usual it is not a good idea to rely on undocumented class names but in the case of an OS-specific bug it works for my purposes. There could be any number of windows, including the root application window, keyboard, alerts, and other windows that your app or other frameworks have added, so don't be too inspecific.
Got the same issue here. It does happen ONLY when I hit home and return to the app.
Does not happen in the first fresh run.
And it is related to iOS5 as well.
I got the same problem, and my work around is to listen to UIKeyboardDidShowNotification and UIKeyboardDidHideNotification, calculate the keyboard height using UIKeyboardFrameEndUserInfoKey, then in my hitTest:withEvent: method I would see whether the hit was on the keyboard "zone" or not.
Just to expand on the answer by #enzo-tran , this is what I ended up doing: I added a keyboardRect property to my UIView subclass, registered for UIKeyboardDidShowNotification and UIKeyboardDidHideNotification, and added:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint([self keyboardRect], point)) {
// Ignore
} else {
...
}
}
- (void)keyboardDidShow:(NSNotification *)notif {
CGRect keyboardRect;
[[[notif userInfo] valueForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardRect];
keyboardRect = [self convertRect:keyboardRect fromView:nil];
[self setKeyboardRect:keyboardRect];
}
- (void)keyboardDidHide:(NSNotification *)notif {
[self setKeyboardRect:CGRectZero];
}
Related
Landscape apps made with Xcode 8.3 are letterboxed on the iPhone X, and the home bar is partially disabled, meaning that the user must swipe up to "wake it" then swipe up again to exit the app. It's this second functionality that I want to implement while taking full advantage of the screen size, so how do I replicate that feature using Xcode 9?
If I set the view controller's prefersHomeIndicatorAutoHidden() to return true, the home bar temporarily disappears, but every time the user touches the screen it comes back (a bit jarring), but it still only takes a single swipe to exit the app. I have not been able to find any other options to do what I want, but clearly it should be possible since it automatically happens for older apps.
Suggestions?
[Note crossposted on Apple Developer Forum]
This behavior is set up by implementing preferredScreenEdgesDeferringSystemGestures in a UIViewController, as follows:
- (UIRectEdge)
preferredScreenEdgesDeferringSystemGestures
{
// prevent home bar from interfering
return (UIRectEdgeTop | UIRectEdgeBottom);
}
Once this is done, the Home Bar should not be auto-hidden:
- (BOOL)
prefersHomeIndicatorAutoHidden
{
return NO;
}
In addition, in a convenient place (such as viewDidAppear:), you need to notify the system that these properties have changed:
- (void)
viewDidAppear:(BOOL) isAnimated
{
[super viewDidAppear:isAnimated];
if (#available(iOS 11.0, *))
{
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
}
On iOS 7, when UIAlertView is presented, its buttons seem to automatically have exclusiveTouch=YES set. This way, if you simultaneously press more than one button on alert view, nothing terribly will happen.
This is not the case on iOS 8. When I present UIAlertView with 2 buttons. And I press these 2 buttons at the same time, the app freezes.
I cannot access the UIButtons on that UIAlertView. I cannot access any of the subviews of UIAlertView for that matter.
alertView.subview returns empty array, no matter in which part of the lifecycle I call it.
I have managed to find a solution to the problem. I implement the UIAlertView's delegate method:
-(void)didPresentAlertView:(UIAlertView *)alertView
{
UIView *view = ((UIApplication*)[UIApplication sharedApplication]).keyWindow.rootViewController.presentedViewController.view;
[self setExclusiveTouchForView:view];
}
-(void) setExclusiveTouchForView:(UIView*)view
{
for (UIView *subview in view.subviews) {
subview.exclusiveTouch = YES;
[self setExclusiveTouchForView:subview];
}
}
At first it was confusing to see that neither of all the subviews inside the view variable is of class UIButton or UIControl.
It is not actually necessary to set exclusiveTouch property on all of the subviews. But I posted it like this to reduce code complexity.
It works fine. App no longer freezes when both of UIAlertView's buttons are pressed at the same time. Only one of the presses is accepted.
Previously as noted in a few questions:
How to detect iPad user tap on keyboard hide button?
detect iPad keyboard Hiding button
...the way to detect if the user hit the hide keyboard button on an iPad was to subscribe to the UIKeyboardWillHideNotification notification.
However, apparently from iOS 6 on, this notification has been sent in a number of places not just when the user hits the retract keyboard button. For example, that notification is sent when the iPad rotates even though the keyboard itself does not hide.
I have discovered an inelegant work around, which I will post here in case anyone else is struggling with this problem, but would much appreciate any elegant solutions people have found to this problem.
The current solution I have come up with for this problem is to note that the only time the keyboard actually retracts during this notification is if the user actually hits the retract button. In other cases there is nearly 0 delay between the UIKeyboardWillHideNotification and the UIKeyboardDidHideNotification.
static BOOL immediate;
- (void) checkImmediate {
if (immediate) return;
/* Do Stuff */
}
- (void) keyboardWillHide:(NSNotification*)note {
immediate = NO;
[self performSelector:#selector(checkImmediate) withObject:self afterDelay:.01 inModes:#[NSRunLoopCommonModes]];
}
- (void) keyboardDidHide:(NSNotification*)note {
immediate = YES;
}
Just for starters: I'm already listening to keyboard will appear/disappear/change notifications. They're not firing. Neither are did appear/disappear/change.
When I have the keyboard up, and push a controller on top which also has the keyboard up (-[UITextView becomeFirstResponder] in viewWillAppear), no keyboard notifications are fired. This makes some sense, as the keyboard does not actually move in this animation, but it's certainly not desirable in this case.
How would I detect this scenario, and / or how can I get the current position of the keyboard when no notification has been fired? A global, shared listener is an option, but I'd prefer to avoid that if possible.
You would need to find the firstResponder, and if its a UITextField or UITextView then the keyboard is up or moving. No notification means its up already, so its old frame (relative to the window) is still valid. Unfortunately there is no easy way to find the firstReponder. I grabbed some code that recursively walked all the current view's subviews, looking for it.
EDIT:
- (UIView *)findFirstResponder
{
if (self.isFirstResponder) {
return self;
}
for (UIView *subView in self.subviews) {
UIView *firstResponder = [subView findFirstResponder];
if (firstResponder) return firstResponder;
}
return nil;
}
I made a widget for the iOS 5 notification center that implements a UISlider along with a UITapGestureRecognizer.
The gestureRecognizer works fine, but the UISlider is very unresponsive and will only move a small amount if the thumb is touched and dragged.
It will not move until another touch down. Is there a way to circumvent this limitation? (I didn't load any views above it, it works fine on the iPhone and iPod Touch).
I finally found a way to fix it.
You should set the superviews gesturerecogniser's cancelTouchesInView property to NO on viewWillAppear.
- (void)viewWillAppear {
if (deviceIsIPAD()) {
UIView *list = [[objc_getClass("SBBulletinListController") sharedInstance] listView];
for (UIGestureRecognizer *gr in list.gestureRecognizers) {
gr.cancelsTouchesInView = NO;
}
}
}