Use right to left UIScreenEdgePanGestureRecognizer on UITableView that has section indexes - ios

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.

Related

How can I get a Table View to register taps when each cell has an Image View covering it?

I've been trying to create table view cells each with a UIImageView serving as a background for them. However, when I tap on each cell the table view will not register the click and will not transition to the view controller I have hooked up, even while I'm using the didSelectRowAtIndexPath function.
I realize it's probably an issue with the ImageView obstructing the table views ability to register the cell tap. The cells will transition however when I drag my finger/mouse on it from left to right, just not on taps. I tried to use a Tap Gesture Recognizer on the Image View however it only worked for the cell at the very top and not all of them. How do I tackle this issue?
Here is an image of the table view cells so you have an idea of what I'm working with: http://imgur.com/a/Ku4uD. Thank you!
If you uncheck User Interaction Enabled on your Image View, the problem should be solved. When running into a problem always check the user interaction of the most child view and work your way up.
One trick I have also learned is to create a subclass of a child and override touchesShouldCancel like so:
override func touchesShouldCancel(in view: UIView) -> Bool {
print("touchesShouldCancel")
//Run extra code that you want when the user selects this view
//then still retrieve the tap by its parent.
return false
}
I am unsure of exactly what your problem is, but I would delete whatever segue that you have, add a new one by dragging from the yellow circle on the left side of the center portion of the top of your tableView ViewController inside the storyboard, to the viewController that you desire it to segue to. Give the segue an appropriate identifier, and then inside your tableView class under tableView didSelectRow add performSegue(withIdentifier: "ChosenIdentifier", sender: indexPath)
Then in prepare forSegue add in:
if let vc = sender.destination as? TheViewControllerYouAreSegueingTo {
if let indexPath = sender as? IndexPath {
vc.variableIdentifyingWhatCellWasClicked = indexPath.row
}
}
with whatever adjustment is needed to meet your specific needs.

Tap gesture only responds to last view

I'm adding several views in the code below:
for var i=0;i<sets.count;i++ {
setView=UIView(frame: CGRectMake(0,y,400,65))
x=20
for var c=0;c<sets[i].count;c++ {
imageView=UIImageView(frame: CGRectMake(x,0,60,60))
dieFaces=types[sets[i][c]] as! NSArray
file="\(dieFaces![0]).png"
print(file)
imageView!.image=UIImage(named: file)
setView!.addSubview(imageView!)
x+=60
}
setView!.tag=i
setView!.addGestureRecognizer(tap)
scrollView.addSubview(setView!)
y+=66
}
Only the last view added is responding to the tap. What am I doing wrong?
A tap UITapGestureRecognizer can only be attached to a single view, so only the last view is responding.
You'll need to create a new gesture recognizer for each setView you're attaching it to.
UIGestureRecognizer can be added to only one view. So when you add it to another one it just removes itself from the previous view.
I can suggest two options:
Add recognizer to a superview. In this case it's UIScrollView
Or create more recognizers(one per view) and use the same target and action.

Recognize swipe gesture in view not in subview

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.

Combine UIPageViewController swipes with iOS 7 UINavigationController back-swipe gesture

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)
}
}
}

touches methods not getting called on UIView placed inside a UIScrollView

I have a Custom Scroll View, subclassing UIScrollView. I have added a scroll view in my viewcontroller nib file and changed its class to CustomScrollView. Now, this custom scroll view (made from xib) is added as a subview on self.view.
In this scroll view, I have 3 text fields and 1 UIImageView(named signImageView) added from xib. On clicking UIImageView (added a TapGestureRecogniser), a UIView named signView is added on the custom scroll view. I want to allow User to sign on this view, So I have created a class Signature.m and .h, subclassing UIView and implemented the touches methods (touchesBegan, touchesMoved and touchesEnded) and initialised the signView as follows:
signView = [[Signature alloc]initWithFrame:signImageView.frame];
[customScrollView addSubview:signView];
But when I start signing on the signView, the view gets scrolled and hence the touches methods don't get called.
I have tried adding signView on self.view instead of custom scroll view, but in that case the view remains glued to a fixed position when I start scrolling. (Its frame remains fixed in this case)
Try setting canCancelContentTouches of the scrollView to NO and delaysContentTouches to YES.
EDIT:
I see that similiar question was answered here Drag & sweep with Cocoa on iPhone (the answer is exactly the same).
If the user tap-n-holds the signView (for about 0.3-0.5 seconds) then view's touchesBegan: method gets fired and all events from that moment on go to the signView until touchesEnded: is called.
If user quickly swipes trough the signView then UIScrollView takes over.
Since you already have UIView subclassed with touchesBegan: method implemented maybe you could somehow indicate to user that your app is prepared for him to sign ('green light' equivalent).
You could also use touchesEnded: to turn off this green light.
It might be better if you add signImageView as as subView of signView (instead of to customScrollView) and hide it when touchesBegan: is fired). You would add signView to customScrollview at the same place where you add signImageView in existing code instead.
With this you achieve that there is effectively only one subView on that place (for better touch-passing efficiency. And you could achieve that green light effect by un-hiding signImageView in touchesBegan:/touchesEnded:
If this app-behaviour (0.3-0.5s delay) is unacceptable then you'd also need to subclass UIScrollView. There Vignesh's method of overriding UIScrollView's touchesShouldBegin: could come to the rescue. There you could possibly detect if the touch accoured in signView and pass it to that view immediately.
When ever you add a scrollview in your view hierarchy it swallows all touches.Hence you are not getting the touches began. So to get the touches in your signon view you will have to pass the touches to signon view. This is how you do it.
We achieved this with a UIScrollView subclass that disables the pan gesture recogniser for a list of views that you provide.
class PanGestureSelectiveScrollView: UIScrollView {
var disablePanOnViews: [UIView]?
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let disablePanOnViews = disablePanOnViews else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
let touchPoint = gestureRecognizer.location(in: self)
let isTouchingAnyDisablingView = disablePanOnViews.first { $0.frame.contains(touchPoint) } != nil
if gestureRecognizer === panGestureRecognizer && isTouchingAnyDisablingView {
return false
}
return true
}
}

Resources