Lost Gestures iPad landscape - ios

I have an app with an MGSplitView containing a table view and a UIWebView, which is fixed to landscape. The web view has a UITapGestureRecognizer (for a triple tap) attached to the web view. Taps in the left portion of the web view work; taps on the right side of the web view are lost - the action is not triggered, and the gesture delegate messages are not received.
The problem seems not to lie in the MGSplitViewController, as switching to a UISplitViewController has the same issues; changing from a tap to a long press also has the same results.
Tap locations are reported with the x coordinate at or close to the max width of the gesture.view, and yet are clearly made close to the centre of the display, which I expect has something to do with the root of the problem - and yet the web view contents are clearly visible and correctly placed.
All the view controllers involved implement shouldAutorotate and supportedInterfaceOrientations, so being stuck in portrait seems unlikely i.e. MGSplitViewController, my UITableView subclass (left hand panel) and UIViewController subclass for the right hand panel.
My gesture recognizer delegate and output from one triple-tap (the view in the right hand panel web view):
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
NSLog(#"%s", __PRETTY_FUNCTION__);
NSLog(#"gestureRecognizer view frame: %#", NSStringFromCGRect(gestureRecognizer.view.frame));
NSLog(#"location %#", NSStringFromCGPoint([gestureRecognizer locationInView:gestureRecognizer.view]));
return YES;
}
-[DocumentBrowser gestureRecognizer:shouldReceiveTouch:]
gestureRecognizer view frame: {{0, 0}, {703, 704}}
location {703, -20}
-[DocumentBrowser gestureRecognizer:shouldReceiveTouch:]
gestureRecognizer view frame: {{0, 0}, {703, 704}}
location {414.5, 204.5}
-[DocumentBrowser gestureRecognizer:shouldReceiveTouch:]
gestureRecognizer view frame: {{0, 0}, {703, 704}}
location {414.5, 204.5}
The first location reported seems strange.

Check your orientation and view resizing mechanisms. I've seen this several times when something is wrong in these areas - if you log the touch locations, I think you will probably find that they stop at 768 points from the left-hand side, i.e. there is a view somewhere that thinks it is in portrait orientation.

Related

scrolling UIScrollView from edge of the screen

I have an iPad app that uses a horizontal scroll view with a bunch of controls as subviews. The user has to be able to use all the controls inside the scrollview and only scroll the view if they deliberately drag a finger from outside the left or right edge of the screen.
I've implemented this by putting this code in the UIScrollView's pointInside:forEvent: method.
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// scroll the view if the user's finger has dragged from off-screen.
[self setScrollEnabled:(point.x - self.contentOffset.x < 9 || point.x > self.contentOffset.x + self.frame.size.width - 9) ||
self.isDecelerating];
return [super pointInside:point withEvent:event];
}
When the iPad is in UIInterfaceOrientationLandscapeRight (home button on right side of the screen), I can drag my fingers from outside the right side or left side inwards and the UIScrollView scrolls as intended. However, when I switch orientations, dragging from the left side (home button) scrolls fine while the right side (near the camera) only works about 50% of the time.
I've tried extending the frame of the scroll view to be slightly outside the bounds of the screen, but I still only have problems with this type of gesture on that one side on UIInterfaceOrientationLandscapeLeft. Ideas?
EDIT: I added two UIScreenEdgePanGestureRecognizers for both sides of the screen — the gesture is more responsive now, but dragging from the right side of the screen while the home button is on the left is still sketchy at best. I have no idea why this would be happening for just that interface orientation.

Weird behaviour of custom UIViews when touched

In a what would be a (yet another) word game for iOS I have a scroll view holding an image view with game board.
And underneath I have draggable custom views representing letter tiles.
For the scroll view I've added a double tap gesture recognizer for toggling its zoomScale (between 50% and 100% image width):
This works well, but I had to disable Autolayout (because otherwise scroll view's contentOffset would jump to {0,0} when I dragged the tiles).
So I had to add the following code to ViewController.m to adjust scroll view's frame after device rotation:
- (void) adjustSubViews
{
_scrollView.frame = CGRectMake(0,
0,
self.view.bounds.size.width,
self.view.bounds.size.height - kHeight - 2 * kPadding);
}
this seems to work too - but my problem is that after first device rotation to landscape and back "something breaks" and the tiles are suddenly draggable by touching underneath them:
Why does it happen please?
Below is an Xcode screenshot (here fullscreen) showing the scroll view properties:
UPDATE: I've added size logging to the Tile.m:
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
NSLog(#"%s %#", __PRETTY_FUNCTION__, NSStringFromCGSize(self.frame.size));
[_smallImage setHidden:YES];
[_smallLetter setHidden:YES];
[_smallValue setHidden:YES];
[_bigImage setHidden:NO];
[_bigLetter setHidden:NO];
[_bigValue setHidden:NO];
[self.superview bringSubviewToFront:self];
}
and while initially it logs the expected tile size of 45x45 (set by me in Tile.xib):
-[Tile touchesBegan:withEvent:] {45, 45}
After I rotate the device to landscape and back to portrait I suddenly get the height of 160:
-[Tile touchesBegan:withEvent:] {45, 160}
The tile becomes draggable blow its image because the view containing the tile is auto-resized by it's superview (controller's view) that changes its frame during rotation.
This behaviour can be easily avoided by setting autoresizing mask of the tile view to UIViewAutoresizingNone (by deselecting all springs and struts in Interface Builder).
Note the tools to debug problems with view sizing like this one:
LLDB command po viewVariableOrAddress (calls description method of its parameter and prints the result).
-[UIView recursiveDescription] called from LLDB by the following command: po [viewVariableOrAddress recursiveDescription]. Note that his method is private and must not be called in production code (the app will be rejected otherwise).
Reveal — in a natural GUI, represents the same information that you can get at runtime in debugger console.

Panning objects on a UIScrollView

I'm working on an app where I have several UIView objects that are subviews on a UIScrollView object. I create the subviews programmatically and place them on the scroll view according to the properties of associated objects. The user is allowed to move these subviews around on the scroll view. Usually this works, but sometimes the scrollview grabs the pan gesture.
What I'd like to do is to suppress the scroll view gesture recognizer if the touch location is inside one of the subviews.
I can find the scroll view gesture recognizer by looking through the scroll view's array of gesture recognizers and looking for a UIScrollViewPanGestureRecognizer object. I assume there can only be one.
An idea I have is to make my view controller be a delegate of this gesture recognizer and then have the delegate suppress it if the touch is within the bounds of one of the subviews.
Is this the best way to handle this scenario, or is there a better way?
I've done something similar, described in my answer to my own question here.
How to get stepper and longpress to coexist?
Hmmm. Looks like it will be more difficult than I anticipated to recognize the scrollview's UIScrollViewPanGestureRecognizer. Any hints on doing this would be appreciated.
My idea doesn't work. In order to code my idea, I had to make my VC be the delegate of the scrollview's pan gesture recognizer. However, when I do that, I get this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.'
Here is the code I used. In viewDidLoad I called a method which got the scrollview's pan gesture recognizer and set self as delegate (self.scrollViewPanGestureRecognizer is just a property to store it):
self.scrollViewPanGestureRecognizer = [self.scrollView panGestureRecognizer];
self.scrollViewPanGestureRecognizer.delegate = self;
I then implemented this delegate method:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//Disable touch if touch location is in a subview.
BOOL enableGestureRecognizer = YES;
if (gestureRecognizer == self.scrollViewPanGestureRecognizer) {
CGPoint touchLocation = [touch locationInView:self.scrollView];
for (UIView *s in self.scrollView.subviews) {
if (CGRectContainsPoint(s.frame, touchLocation)) {
enableGestureRecognizer = NO;
}
}
}
return enableGestureRecognizer;
}
Seemed like a good idea, but it looks like I can't make my VC be the delegate.
Just tried setting scrollEnabled to NO on the scroll view. That successfully disabled scrolling, but it did not fix the problem. Views still occasionally do not respond to gestures. Thinking that perhaps some bug caused the gesture recognizer to fall off the object, I asked the debugger to display the gesture recognizers for the problematic views. They were still there. I'm stumped.
UPDATE: New information. I finally realized that the subviews that aren't responding are the ones on the right side of the screen. After carefully testing, it seems that this happens only in landscape orientation and only when the finger location is to the right of the right edge in portraite, i.e. 320 points. Apparently, something is not being handled property when rotating to landscape. Everything appears normal, but the gestures aren't being recognized.
Just for grins, I decided to display the frames and bounds and content area in the method viewDidLayoutSubviews. What I get is:
self.view.frame is {{0, 0}, {480, 320}}
self.view.bounds is {{0, 0}, {480, 320}}
self.scrollView.frame is {{0, 0}, {480, 320}}
self.scrollView.bounds is {{0, 0}, {480, 320}}
self.scrollView.contentSize is {480, 320}
I seem to have missed something. What else needs to be set when rotating?
use requireGestureRecognizerToFail: method.
you want your scroll view pan gesture (scrollViewGesture) to be failed when one of the gestures happen on its subView.
So, when you add pan gesture to your subView (subViewGesture), set below property as
scrollViewGesture.requireGestureRecognizerToFail =subViewGesture;
I found the solution. I'd forgotten that the subviews are not placed directly into the scroll view. There is a view originally occupying the bounds of scrollview onto which the subviews are placed. The hierarchy is like this:
self.view
scroll view
UIView (fills whole scroll view)
subview1
subview2
subviewn
In my code to handle rotation, I was not resizing the UIView into which the subviews are placed. Correcting this issue solved the problem.
I'd originally tried placing the subviews without their UIView superview in between them and the scroll view, but it didn't work for some reason. Adding this extra layer solved that problem, but I forgot to handle the resizing when rotating.
So I guess the gesture recognizers did not respond because although they were visible, they were outside the bounds of their superview.
I'm making this answer a community wiki because I haven't completely worked out this solution yet. The main thing is to take advantage of this from the documentation:
Subclasses can override the
touchesShouldBegin:withEvent:inContentView:, pagingEnabled, and
touchesShouldCancelInContentView: methods (which are called by the
scroll view) to affect how the scroll view handles scrolling
gestures.
One solution: instead of playing around with gesture recognizers, just disable all of them and use touchesBegan, touchesMove and touchesEnded directly. It might be a bit of work, but pretty sure it will work exactly the way you want.
You need to disable user interaction on the subviews, disable scrolling on the scrollview, and modify the scrollview's contentOffset directly.

UIKeyboard adopts an invalid frame

I currently have an UICollectionView, and a UITextField on the bottom of the screen. The textField moves according to the frame of the keyboard (very much like an inputAccessoryView, but I didn't use it for other reasons), and the collection view gets its insets modified when the keyboard appears, very standard stuff.
Anyway, there's a button in the screen, that on press, shows a modal UIViewController that covers the entire screen, and I dismiss the keyboard if my textField isFirstResponder.
Now, when this modal view finishes its business, it gets dismissed, we are back at the collection view and textfield, but now, the collection view has infinite insets, and the keyboard doesn't show again.
I check the notification I get for UIKeyboardWillChangeFrameNotification, and this is the userInfo:
Printing description of notification:
NSConcreteNotification 0x1ee4e900 {name = UIKeyboardWillChangeFrameNotification; userInfo =
{
UIKeyboardAnimationCurveUserInfoKey = 0;
UIKeyboardAnimationDurationUserInfoKey = "0.4";
UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 0}}";
UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 1136}";
UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 568}";
UIKeyboardFrameBeginUserInfoKey = "NSRect: {{inf, inf}, {0, 0}}";
UIKeyboardFrameChangedByUserInteraction = 0;
UIKeyboardFrameEndUserInfoKey = "NSRect: {{inf, inf}, {0, 0}}";
}}
The keyboard is coming up as {{inf, inf}, {0, 0}}. I already wrote a check (with isInf) to prevent this, but now the keyboard gets permanently screwed up, and it won't show up anywhere in the app anymore, so this has become a serious issue. This happens on iOS6 and iOS7, on simulator and device.
I'm going through all the code to see if there's anything weird that is causing the issue, but I'm hoping some has ran into something similar, and has some insight on what is causing this.
After 3 months, I finally figured it out (I've been doing other stuff, FYI).
The short answer
Make sure you're not returning crap from any rotation method, otherwise keyboard goes loco.
The long version
Discovered by using the application Reveal, that draws a 3d decomposition of each layer in the app. I noticed the keyboard was being presented but it's frame was landscape, when the interface was in portrait. This immediately made me revise the whole way the rotation is being handled.
The whole problem spans from a UIViewController that posses child view controllers with a complex structure, and the rotation calls are getting forwarded to the children manually. For example, this method:
- (NSUInteger)supportedInterfaceOrientations
{
return [[[(id)self.selectedViewController viewControllers] lastObject] supportedInterfaceOrientations];
}
Was relying on whatever top-most view controller on the current selected navigation controller to return the value. Well, one of the view controllers went rogue, it wasn't returning anything here, so the return value must've been just about anything in memory or zero.
The solution now checks if the view controllers responds to the method, and if not, it defaults to portrait (which is about 90% of the app).
- (NSUInteger)supportedInterfaceOrientations
{
NSUInteger supported = UIInterfaceOrientationMaskPortrait;
UIViewController *viewController = [[(id)self.selectedViewController viewControllers] lastObject];
if (viewController && [viewController respondsToSelector:#selector(supportedInterfaceOrientations)])
supported = [viewController supportedInterfaceOrientations];
return supported;
}
Btw, the sane way to do it would be to just return YES from -(BOOL)shouldAutomaticallyForwardRotationMethods (iOS 6, 7) but this was an impossibility due to how it was implemented. For now, it's on NO and being manually handled.
There's still the question why it only mattered with presented view controllers. Although, I rather just discard it as "random behavior from returning an invalid interface orientation".

Rotate iPad while popover is visible and my UITableView moves

This is REALLY weird and I'm not sure what the heck is going on.
I've got a UISplitViewController on my iPad app. I can rotate from portrait to landscape over and over and it works exactly as expected. However, if I'm in Portrait orientation and I tap the button to show the popover which contains the left-hand side of the UISplitViewController and then rotate while it is visible, when the rotation is complete the left-hand side table is now pushed down leaving a black gap between it and the Nav Bar.
What the heck could cause that?
I have commented out my -willRotateToInterfaceOrientation:duration and -didRotateFromInterfaceOrientation: methods and the problem still exists, so it's not being caused by my rotation code. (That code was just explicitly dismissing that popover).
After the rotation which causes the problem, I recursively printed out the view hierarchy of the left side along with frame sizes:
UILayoutContainerView ({{0, 0}, {320, 748}})
UINavigationBar ({{0, 0}, {320, 44}})
UILayoutContainerView ({{0, 37}, {320, 711}})
UINavigationTransitionView ({{0, 0}, {320, 711}})
UIViewControllerWrapperView ({{0, 44}, {320, 667}})
UIView ({{0, 0}, {320, 667}})
UITableView ({{0, 0}, {320, 667}})
It shows that the navbar is 44 pixels tall, the view container which holds my left-side view controller starts at 0,44 and my tableview within starts at 0,0, but is shorter.... I don't understand why it's not displaying correctly.
Here is what is should look like in landscape orientation. On the left, you'll see the table section header "What do you want to know?" and it is right below the Nav Bar with the "Switch to Calculator" button.
When I swap to Portrait orientation and tap the "What do you want to know?" button, the popover shows which contains the left-hand side navigation controller with the UITableViewController.
Now, when I rotate back to landscape, the black gap appears between the nav bar and the UITableView.
I remembered having a problem very similar to this last year while working on this app, but I wasn't able to find any old StackOverflow posts about it. However, I remembered that there was some reason why I included the navbar (or rather, made it visible) inside the popover when in portrait mode. For aesthetic purposes, I'd rather have it visible in landscape, but not visible in the portrait popover.. and then I remembered the reason I included it was because it seemed to fix this very problem.
It appeared to be a weird bug in iOS, but showing the Nav Bar is both situations seemed to fix it. So, I decided to change it back to the way I really wanted it to be: visible in landscape, not visible in portrait... and BAM. The black gap disappears. So, it appears that weird bug is still there, but it is working in reverse now.
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
UIDeviceOrientation deviceOrientation = [UIApplication sharedApplication].statusBarOrientation;
BOOL isPortrait = UIDeviceOrientationIsPortrait(deviceOrientation);
if (isPortrait) {
self.navigationController.navigationBarHidden = YES;
} else {
self.navigationController.navigationBarHidden = NO;
}
}
}

Resources