Storyboard gesture recognizer - ios

After creating an Xcode project from the iPad "master/detail" storyboard template, I cannot seem to find the UIGestureRecognizer instance that's responsible for the Mail-style swipe to show the master view in portrait mode.
I need to do this in order to make it ignore touches on certain UI elements, but it doesn't seem to be handled by any of the 4 gesture recognizers returned by the gestureRecognizers method of the master view. When I set a delegate on them, its functions only get called when interacting with the master view itself, and not with a swipe on, e.g. the detail area. The detail view returns an empty array from gestureRecognizers.
A project-wide search for "gesture" reveals nothing, and I see no gesture recognizers in the storyboard. Where is this handler created and managed in the default Xcode "master/detail" template, and how can I access it in order to set a delegate?

I expect it is on the split view controller itself rather than the master or detail view controllers. You can turn it on or off using the presentsWithGesture property (5.1 and later only).

Related

Preventing unhandled touch events on a child view controller from passing through to container view

I have a container view controller managing its own full-screen content view, with several gesture recognizers attached. A child view controller can be overlaid over a portion of the screen; its root view is a UIView providing the opaque background color, which is covered by a UIScrollView, which in turn contains a complex view hierarchy of stack views, etc.
Scrolling in the child works correctly, as well as any user interactions with its subviews. The problem I'm having is that any taps or other non-scrolling gestures on the the scroll view itself (i.e. not inside any of its subviews) fall through the empty UIView behind it and are unexpectedly handled by the gesture recognizers on the root view of the parent (container) controller. I want those touches to be swallowed up by the child's background view so that they are ignored/cancelled.
My first thought was to override nextResponder on the child VC to return nil, assuming that would prevent touch events from passing to the superview. No success there, so I tried overriding the touch handling methods (touchesBegan: etc.) on the child controller, but they never get called. Then I substituted a simple UIView subclass to be the root view of my child controller, likewise trying both of those approaches there instead. Again returning nil for nextResponder has no effect, and the touch methods never get called.
My responder chain looks to be set up exactly as I would expect: scroll view --> child VC's root view --> child VC --> parent's root view --> parent VC. That makes me think my controller containment is set up correctly, and makes me suspect that the gesture recognizers on the parent's root view are somehow winning out over the responder chain in a way that I don't understand.
This seems like it should be easy. What am I missing? Thanks!
I think I understand better what's going on here thanks to this very helpful WWDC video.
Given an incoming touch, first the system associates that touch with the deepest hit-tested view; in my case that's the UIScrollView. Then it apparently walks back up the hierarchy of superviews looking for any other attached recognizers. This behavior is implied by this key bit of documentation:
A gesture recognizer operates on touches hit-tested to a specific view and all of that view’s subviews.
The scroll view has its own internal pan recognizer(s), which either cancel unrecognized touches or possibly fall back on responder methods that don't happen to forward touches up the responder chain. That explains why my responder methods never get called, even when my own recognizers are disabled.
Armed with this information, I can think of a few possible ways to solve my problem, such as:
Use gesture delegate methods to ignore touches if/when the associated view is under a child controller.
Write a "null" gesture recognizer subclass that captures all touches and ignores them, and attach that to the root view of the child controller.
But what I ended up doing was simply to rearrange my view hierarchy with a new empty view at the top, so that my child controller views can be siblings of the main content view rather than its subviews.
So the view hierarchy changes from this:
to this:
This solves my problem: my gesture recognizers no longer interact with touches that are hit-tested to the child controller's views. And I think it better captures the conceptual relationships between my app's controllers, without requiring any additional logic.

Swift canEditRowAtIndexPath not working in UIPageViewController EZSwipeViewController

I have the following scenario and need help in resolving a tricky situation in the scenario
There is an Xcode project and am using EzSwipeController for swipe (pagination effect) between three View Controllers at the moment.
In my first ViewController (this viewController is fetched from my custom dynamic framework as part of my requirement) -
Code to fetch ViewController:
userProfile.createProfileUI(userSession!) { result in
switch result {
case let .Success(profileViewController):
myDetailsVC = profileViewController //myDetailsVc is passed to EZSwipControllerDataSource array
default:
break
}
}
The other two ViewControllers are within my project storyboard
The Problem -
In the first ViewController, there is a tableView with canEditRowAtIndexPath enabled for few cells (phone numbers).
So when I try to swipe the row, the EZSwipeController responds first and
hence, I am not able to edit the row.
Here is what is happening - http://recordit.co/SOJgdeYchP
Here is what should happen - http://recordit.co/EBPSbjH31q
How do I handle this problem? Is there a way where I can override the default swipe controller action when I try to edit the row?
Please help!
Attach the swipe gesture recognizer to a parent in the hierarchy.
If you're using a UITableViewController, replace it with a UIViewController with a UITableView inside it. Then just drag the gesture recognizer onto the view controller in Storyboard, and it'll attach to the UIViewController's Content View instead of the UITableView.
Though at the end of the day, this is inherently a flawed approach, since swiping to flip pages in an app is only ever viable if you don't have any elements on the pages that also have swipe gestures in them. If you do, even if you code a workaround to make the gesture recognizer for the element in the view controller (in this case, a table view cell) fire instead of the page flipping swipe gesture, that creates an inconsistent user experience.
My suggestion: don't use a table view for such a form altogether. On top of the aforementioned mechanical UX issue, from a user perception perspective, there's nothing indicating visually that this is a table view and not just a scroll view, so there's nothing indicating to the user that swipe actions (Delete) are available. Use a scroll view instead, and take a different approach to deleting (Delete buttons that are always visible, Delete buttons that are only visible after the user hits Edit somewhere, etc.).

Present view controller while touching

I want to implement a hold-to-preview button that brings up a view containing an AVPlayerLayer, which plays as long as the touch doesn't end. The video player is contained in a different view controller, and I am hoping to be able to use presentViewController:animated: when presenting it, and not just add it as a subview and child view controller.
My question is about how to deal with the touch event. I see two possible ways:
I try to transfer the active touch down event to the presented view controller (not sure if even possible), or
I try to keep the original view controller's gesture recognizer active, and then let the video view controller know when it's time to dismiss itself. I'm hoping this could be achieved either by just setting the presented view controller's userInteractionEnabled to false, or perhaps using a UIViewControllerTransitioningDelegate to present it, and then just skip calling completeTransition: or something similar (I believe touches don't register on the new view until you complete the animation, but please correct me if I'm wrong).
My question is about how to deal with the touch event.
Touches are always associated with the view that they start in. You can't transfer the touch to a different view. I've never tried it, but the options I think you should explore first are:
Use view controller containment. Make your preview view controller a child view controller of the one where the touch originates. That way the parent and its view hierarchy never go away, although they could be covered up.
Attach the gesture recognizer to the window. A window is a view, and should be able to have gesture recognizers. You could make the gesture recognizer's target the app delegate or some other object that will always be around, and have the delegate post a notification when the recognizer is triggered. Again, I haven't tried this, but it seems like it should work.

Detecting shake gesture by multiple view controllers

I need to detect shake gesture in iOS. I have done the usual stuff and it works perfectly fine. The thing is I have multiple view controllers in UITabBarController and I wish each of them to detect the shake gesture.
When shaking in any of the view controller , I get switched to a particular tab. The problem is if I shake in one view controller and try to shake in other controller the gesture is not detected unless some action is performed in that controller.
I know I need to set becomeFirstResponder but I need to know how can this property be set to the current tab of the UITabBarController so that the shake gesture is recognised by all tabs.
Write the code for detection (usually via a notification observer for shake) in a base view controller and and all the controller will subclass from this. Now you can write the code to move the particular tab in this base controller.
Problem solved.

Disable page transition at VC pushed by Navigation controller

I have root page view controller with three navigation controllers, each of them initially navigates to view controller with main table. After user clicks on the table there is a segue with push going to another VC (with detail table).
Transition style is scroll.
it's work fine. But, I have following effect: page scrolling works everywhere.
I need to scroll between pages only when user in table vc. Left swipe in detail table vc going to previous navigation controller, not back to the main table vc.
What I need to do to correct described behavior?
X-Code 5, iOS 7
Upd: I can prevent page transition by returning a nil at datasource before/after methods, but the scrolling is still showing on the screen.
When I tried to disable scroll by setting datasource to nil in 2 ways:
1) in pageViewController:didFinishAnimating) a have an exception: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: [views count] == 3'
2) in main table vc in willAppear/willDisapper and have an exception: Terminating app due to uncaught exception 'NSGenericException', reason: '* Collection <__NSArrayM: 0x8a445d0> was mutated while being enumerated.'
Upd 2: here the link to my project: http://yadi.sk/d/MWmdA3XLCdn4U . Press a blue button with "download" picture to get the file, or use the language switcher at the bottom of page.
Upd 3: Thanks to David. His solution is working. In addition: I just lay Pan GR at DetailTable VC in my storyboard and link it to a view, without any lines of code.
Two ideas come to mind:
subclass UIPageViewController, and in your subclass provide a method to change the array of view controllers from the "real" array to just the current visible one. The user will not see any affect of this, but it will prevent the page controller from doing anything.. Obviously when you get back to the root view, you tell the page view controller to undo the change (swap back to the real array).
implement the dataSource protocol, and set a flag so that when you want to prevent paging, the dataSource returns nil for the next/previous view controller.
Note: I have not done this with a pageController, but I have done similar things with navigation controllers.
EDIT1: I played with your project, and yes, its not going to be easy to prevent the scrolling, but it is possible. The key issue is that the page controller has a scrollview subview that is hosting the pages. It has a pan gesture recognizer in it to recognize the dragging behavior and respond to it. Unfortunately, for the "scrolling" variation of the page controller, the gesture recognizers are not exposed. There is an interesting thread on this whole subject here on SO.
So you have a few options:
look for the scroll view in the subviews, find the gesture recognizer, and disable it.
disable the same scrollView
do some fancy tricks after you push the first view controller. But that I mean, once the animation is over, pull that view out of the subviews, remove the paging controller from the window, and install the pulled out view directly as the root view controller. This can be made to work, but since offsets change. you may need to actually create a transparent view the same size as the window and insert the pulled view there first.
add your own pan gesture recognizer, and just ignore the results. So in viewDidLoad in any view that you don't want paging, add the code below. It seems to work quite well.
Code:
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
[self.view addGestureRecognizer:pan];
EDIT2:
The preferred way to do this is going to be using your own pan gesture recognizer, as shown in the code above. What you can do is when the view appears that you DO NOT want to activate the paging control, add the gesture recognizer in viewDidLoad. When the user navigates back to the main view, then that pan recognizer will not be active, and the page control will work normally. I tried this with your demo project, it works fine.
While the other ways can be made to work, its more complicated as you see. To change the scrollView or its recognizer, you have to go probing around in the page view subviews, which is something Apple frowns on, and is fragile (meaning it can break easy in the future). You would have to provide public methods in your subclass, and figure out how to get a reference to the page control to each subview.

Resources