I have a navigation controller that pushes a view-controller (PARENT) that contains an UIPageViewController (PAGES). Now I used pan/swipe gestures to switch between the children of the page-view-controller. However, I can no longer pop the PARENT-view controller using the swipe gesture from the left border of the screen, because it is interpreted as a gesture in PAGES.
Is it possible to accomplish that swipe-to-pop when the left-most view-controller is shown?
Two ideas:
Return nil in pageViewController:viewControllerBeforeViewController -> doesn't work.
Restrict the touch area, as described here.
Or is there a more straightforward way?
I had the same situation as #smallwisdom, but handled it differently.
I have a view controller A that I push on top of my navigation controller's stack.
This view controller A contains a horizontal scroll view that stretches all the way from left side of the screen to the right.
In this scenario, when I wanted to swipe the screen to pop view controller A from navigation controller's stack, all I ended up doing was scrolling this horizontal scroll view.
The solution is pretty simple.
Inside my view controller A, I have code like this:
_contentScrollView = [[UIScrollView alloc] init];
[self.view addSubview:_contentScrollView];
for (UIGestureRecognizer *gestureRecognizer in _contentScrollView.gestureRecognizers) {
[gestureRecognizer requireGestureRecognizerToFail:self.navigationController.interactivePopGestureRecognizer];
}
It works great. What this does?
It is telling the scrollView's gesture recognizers that they have to wait to see if some other gesture recognizer will recognize the current gesture.
If that other fails to recognize, then they will no longer have to wait and they can try to recognize the current gesture.
If that other recognizer succeeds and recognizes the current gesture, then all of the gesture recognizers that have been waiting will automatically fail.
This other gesture recognizer they have to wait is set to be the navigation controller's interactivePopGestureRecognizer. He is in charge for the swipe-to-go-back gestures.
I mostly agree with #ancajic's answer. I would like to provide an extra-case when you set UIPageViewController's transitionStyle to 'Scroll', in which you'll not get gestureRecognizers to be set, the workaround is:
if (self.navigationController?.interactivePopGestureRecognizer != nil)
{
for view in self.pageViewController!.view.subviews
{
if let scrollView = view as? UIScrollView
{
scrollView.panGestureRecognizer.requireGestureRecognizerToFail(self.navigationController!.interactivePopGestureRecognizer!);
}
}
}
I had a similar issue in one of my projects and used the following method. In my case, it was one of those left-side menus that were really popular before iOS 7.
My solution was to set the UINavigationControllerDelegate and then implemented the following:
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animated {
// enable the interactive menu gesture only if at root, otherwise enable the pop gesture
BOOL isRoot = (navigationController.viewControllers.firstObject == viewController);
self.panGestureRecognizer.enabled = isRoot;
navigationController.interactivePopGestureRecognizer.enabled = !self.panGestureRecognizer.enabled;
}
EDIT:
Additionally, you need a hook into the UIPageViewController's gesture recognizers. (They aren't returned by the gestureRecognizers property for a scroll view style page view controller.) It's annoying, but the only way I've found to access this is to iterate through the scrollview's gesture recognizers and look for the pan gesture. Then set a pointer to it and enable/disable based on whether or not you are currently displaying the left-most view controller.
If you want to keep the right swipe enabled, then replace the pan gesture with a subclassed pan gesture recognizer of your own that can conditionally recognize based on the direction of the pan gesture.
First, find UIPageViewController's scrollView
extension UIPageViewController {
var bk_scrollView: UIScrollView? {
if let v = view as? UIScrollView {
return v
}
// view.subviews 只有一个元素,其类型是 _UIQueuingScrollView,是 UIScrollView 的子类
// view.subviews have only one item of which the type is _UIQueuingScrollView, which is kind of UIScrollView's subclass.
for v in view.subviews where v is UIScrollView {
return v as? UIScrollView
}
return nil
}
}
Second, set gestureRecognizer's dependence.
override func viewDidLoad() {
super.viewDidLoad()
if let ges = navigationController?.interactivePopGestureRecognizer {
pageViewController.bk_scrollView?.panGestureRecognizer.require(toFail: ges)
}
}
Swift version of #Lcsky's answer:
if let interactivePopGesture = self.navigationController?.interactivePopGestureRecognizer, let pageViewController = self.swipeVC?.pageViewController {
let subView = pageViewController.view.subviews
for view in subView {
if let scrollView = view as? UIScrollView{
scrollView.panGestureRecognizer.require(toFail: interactivePopGesture)
}
}
}
Related
I want to add a UIScreenEdgePanGestureRecognizer to a UITableView so that I can edge swipe from the right side to go to the next screen in my controller hierarchy.
This works fine except when the table view has section indexes shown on the side. In that case, the section index area handles the touches, so I can't swipe from the edge. I would like to be able to support both the edge pan and the section index tap and vertical pan functionality.
I tried adding a view on top of the UITableView to handle the swipe, but then that handles all touches and the table view no longer gets anything.
Okay, so I tried a bunch of stuff and came up with a non-ideal but working solution.
The first part of the problem is that the section index is in its own view as a subview of UITableView. So it captures any touches or gesture recognizers that you might add to the UITableView.
The second problem is that the section index view is not part of the public API.
So my solution is to add a UIScreenEdgePanGestureRecognizer to the section index view.
The view is an instance of the private class UITableViewIndex. To get it I created a UITableView extension:
extension UITableView {
var sectionIndexView: UIView? {
for view in self.subviews {
if view.className() == "UITableViewIndex" {
return view
}
}
return nil
}
}
NOTE the above code is fragile and not future proof, because if Apple changes the class name or its location in the view hierarchy it will no longer work. In my case the edge swipe is a convenience feature, and there is a "Next" button at the top of the screen. In addition, most of my view controllers don't have a table index, so if it stopped working it would only be on a few screens. So if this solution doesn't work in the future then it's not a huge deal.
I also wrote an convenience function to get the edge gesture recognizer from any view:
extension UIView {
var screenEdgePanGestureRecognizer: UIScreenEdgePanGestureRecognizer? {
guard let gestures = self.gestureRecognizers else {
return nil
}
for gesture in gestures {
if let edgePan = gesture as? UIScreenEdgePanGestureRecognizer {
return edgePan
}
}
return nil
}
}
I add a UIScreenEdgePanGestureRecognizer to the UITableView in the viewDidLoad method of my view controller.
Finally, in viewDidAppear of my view controller I check to see if the gesture recognizer has been added to the section index view. If it hasn't, then I add it there. You need to do this at some point after the section index has been added to the table view - i.e. not in viewDidLoad. And you need to make sure you're not adding the gesture recognizer multiple times.
I am using THIS LIB for button, Now I have a viewcontroller, which has a container view, and this container view embeds(segue) a PageViewController, the 2nd page (which is another ViewController) in this PageViewController has the BFPaperButton (which has the ripple animations when onTap gesture). The animations don't work in this setup, but if were to make the 2nd Page's ViewController as the Is Initial View Controller then the animations works perfectly, what is going on here??
EDIT
I further isolated the issue, it seems this only happens when using a PageViewController and returning the ViewController containing the button in one of the data source delegate methods of PageViewController,
and it would seem the cause is most definitely the PageViewController, it seems its blocking some gesturerecognizers or delaying them for its child views, need to find out how to fix it.
The effect that you describe is quite common when dealing with certain types of parent views that delay the content touches of their child views. The solution is usually as simple as setting the parent view's property: delaysContentTouches.
I would remember this because 9 times out of 10, this is the culprit.
However, in the case of UIPageViewController, what you are seeing is the default behaviour of a UIQueuingScrollView which does not expose that property. An ugly but effective way to get around this is to dig through the subviews of your UIPageViewController until you get to one that has some UIGestureRecognizers, and setting delaysTouchesBegan = NO.
You could do this by simply running this code after you initialize your UIPageViewController
NSArray *subviews = self.pageViewController.view.subviews;
NSArray *viewHierarchy = [#[self.pageViewController.view] arrayByAddingObjectsFromArray:subviews];
int i = 0;
for (UIView *viewToCheck in viewHierarchy) {
for (UIGestureRecognizer *gestureRecognizer in viewToCheck.gestureRecognizers) {
NSLog(#"%d gestureRecognizer: %#", i++, gestureRecognizer);
gestureRecognizer.delaysTouchesBegan = NO;
}
}
here is a quickly slapped together Swift version if you prefer:
let subViews: [UIView] = pageViewController.view.subviews
var viewHierarchy: [UIView] = [pageViewController.view]
viewHierarchy.appendContentsOf(subViews)
var i = 0
for viewToCheck in viewHierarchy {
viewToCheck.gestureRecognizers?.forEach { (recognizer) in
print("viewToCheck isa \'\(viewToCheck.self)\'")
for gestureRecognizer in viewToCheck.gestureRecognizers! {
print("\(i) gestureRecognizer: \(gestureRecognizer)")
i += 1
gestureRecognizer.delaysTouchesBegan = false
}
}
}
Answered by github user bfeher , for this issue
I have added a subview to a View Controller's view. This subview is the view of QLPreviewController.
What I am trying to achieve is to recognize swipe gestures on the subview in the parent view, i.e. the View Controller's view. In the end, I want to be able to swipe left /right on the view to load the next document for preview.
I'm aware of hit testing and understand that by just attaching a gesture recognizer to the parent view, those will not be recognized, since the subview will be the "hit-test" view.
Now what is the best (or easiest) way to recognize those gestures?
Note: I didn't manage to attach the gesture recognizers to the subview, this doesn't seem to work.
* UPDATE *
To make this more clear - this is the code from my ViewController. vContent is just a view in my ViewController, where I add the view of the QLPreviewController:
let pvVc = QLPreviewController()
pvVc.dataSource = self
vContent.addSubview(pvVc.view)
I tried adding the swipe recognizers both to the vContent and the pvVc.view. In both cases no event was fired.
let sgrLeft: UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action:Selector("handleSwipe:"))
sgrLeft.direction = UISwipeGestureRecognizerDirection.Left
sgrLeft.delegate = self
On some other view the code works fine.
Any hint is appreciated!
Thx
Eau
Well, the responder chain, the unknown animal … ;-)
You can subclass the superview and override -hitTest:forEvent:.
You rarely need to call this method yourself, but you might override it to hide touch events from subviews.
Gesture Recognizers Get the First Opportunity to Recognize a Touch, so even the subview is hitTest view. the gestureRecognizer attached on superView can recognizer touch event.
I have UIPageViewController that animates programatically. The problem is that the view controllers inside it has UIButtons inside them. When I hold down a button and wait until the UIPageViewController animates, the app crashes with the error:
'Failed to determine navigation direction for scroll'
What I think I need to do is to somehow fake that the user releases the button before the UIPageviewController animates.
However, [self.button sendActionsForControlEvents:UIControlEventTouchCancel]; doesn't seem to do the trick. Neither do UIControlEventTouchUpInside.
Is there a better way do to it or am I using sendActionsForControlEvents wrong?
All sendActionsForControlEvents: does is call any methods you've assigned to the control events passed in for the button. It doesn't call any internal methods to programmatically lift up touches or anything like that.
Right before you programmatically animate your page view controller, try using this method to effectively cancel any touches on the pan gesture recognizer of the page view controller's internal scroll view:
- (void)cancelPanGestureTouchesOfPageViewController:(UIPageViewController *)pageVC
{
// Since UIPageViewController doesn't provide any API to access its scroll view,
// we have to find it ourselves by manually looping through its view's subviews.
for (UIScrollView *scrollView in pageVC.view.subviews) {
if ([scrollView isKindOfClass:[UIScrollView class]]) {
// We've found the scroll view, so use this little trick to
// effectively cancel any touches on its pan gesture recognizer
BOOL enabled = scrollView.panGestureRecognizer.enabled;
scrollView.panGestureRecognizer.enabled = !enabled;
scrollView.panGestureRecognizer.enabled = enabled;
}
}
}
(Note that this is messing with the internal view hierarchy of UIPageViewController, so this method is kind of ugly and may break in the future. I generally don't recommend doing stuff like this, but I think in this instance it should be okay.)
I have a UIPageViewController with multiple UIViewController, each one containing a UITextField.
Whenever I slide the UIPageViewController, the keyboard automatically dismisses. Is there a way to prevent this behavior?
Invoking becomeFirstResponder on the new UITextfield but the animation the won't fix the problem.
You can try to embed the PageViewController as a ChildViewController of an other viewController.
I believe this way the navigation in the PageViewController will not effect the keyboard.
I am not sure if that is needed, but if it is still not working you can set your ParentViewController as the firstResponder when transition occurs.
unfortunately this seems to come from the animation of the transition showing as done, before it is actually done,
the workarounds I can think of are
1. made the animating false
2. set textFieldShouldEndEditing in the next VC to return NO or handle it with a bool
3. add a delay in the animation, or in the next VC viewWillAppear
When using setViewControllers on UIPageViewController (setting a controller without the scroll), it seems that the completion block is called just before the scrollView has reached its final position. When it does, it dismisses the first responder.
The solution we found was to first, grab the scroll view:
// In UIPageViewController subclass
for subview in self.view.subviews {
if let scrollV = subview as? UIScrollView {
scrollV.delegate = self
self.scrollView = scrollV // Optional, we don't really need it
}
}
The scroll view position is done when its offset is the x position of the middle controller. That is, the offset will equal the view's length.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x == self.view.width {
// Transition is 'really' done.
}
}
At that point, you can send a notification that transition is completed.
What we did, is hold a completion block and call it when the transition is done.
Create a function, so that a controller can pass a block in:
func transitionCompleted(completed:(()->())?) {
self.transitionCompletedBlock = completed
}
When transition is completed:
self.transitionCompletedBlock?()
The controller with the keyboard will look like:
pagerController.transitionCompleted(completed: {
self.textfield.becomeFirstResponder()
})