How do I only enable UIPanGestureRecognizer only after UILongPressGesture? - ios

I'm new to swift and wasn't sure if this was possible but I'm running into an issue where I am in a UICollectionView, which is scroll enabled. By default, I only want the scroll enabled. However, if the user holds long enough I want the scroll to be disabled and the UIPanGestureRecognizer enabled. I'm having trouble enabling UIPanGesture after UILongPress. After the pangesture is done, the scroll should be enabled again and pan disabled.

First, the compiler is complaining about your variable has no default value.
You can workaround this by adding an initialize method and set gesture there.
Or you can set the variable as an Optional variable which has default value nil. But after that, you have to unwrap to get real value.
Or if you real know what this is:
class YourViewController: xxxx, yyyy {
lazy var panGesture: UIPanGestureRecognizer! = {
let pan = UIPanGestureRecognizer(…
pan.delegate = self
return pan
}
…
}
Second, you could use UIGestureRecognizerDelegate to help.
At first, your pan gesture is disabled. After your long press, you disable the collection view isScrollEnabled and enable your pan gesture. (And after pan gesture finished, you disable pan gesture and reenable collection view isScrollEnabled)

Related

How to disable paging of UIPageViewController only for landscape orientation?

I have a scroll transition style UIPageViewController that needs to disable paging only when device is in landscape orientation. But paging should be enabled in portrait orientation.
I have encountered similar questions here in SO but not my specific need. Some of them are:
How do I Disable the swipe gesture of UIPageViewController?
Disable Page scrolling in UIPageViewController
Disable/enable scrolling in UIPageViewController
Restrict UIPageViewController (with TransitionStyleScroll) pan gesture to a certain area
All of the above points to either completely disabling or restricting pan gesture to a certain area.
Now if I take the approach of completely disabling:
I will need to track device orientation change
Disable when orientation is set to landscape
Again enable when orientation is changed to portrait
If I take the approach of restricting to a certain area:
I will need to find that certain area
That certain area (described in previous point) needs to be calculated
differently for portrait & landscape orientation
Certain area for portrait orientation needs to be the area of the
whole UIPageViewController bounds
Certain area for landscape orientation needs to be a very minimum area
(whose frame could be 0, 0, 1, 1) where user won't be able to
perform pan operation. This frame calculation needs to be very
precise because my UIPageViewController takes the whole bounds of
the main screen in landscape orientation.
Then again may need to track device orientation change for different
calculation of the certain area
There are some techniques where the authors suggest:
pvc.dataSource = nil // prevents paging
pvc.dataSource = `a valid dataSource object` // enables paging
So, manual enable + disable again. Track orientation change and enable/disable.
This isn't safe to use for my specific use case as there is a possibility of assigning data source multiple times.
There are other approaches which, I think, can't be modified to fit the use case.
Is there a shortcut way to achieve what I need?
Answering to my own question as I've already achieved what I needed to.
Subclassing UIPageViewController is the easiest way. We have to find the underlying UIScrollView that is used by the page view controller to handle its pan gesture related work. We will add another UIPanGestureRecognizer to that internal scroll view. This pan gesture recognizer won't perform any action essentially but it will block the internal pan gesture recognizer to be recognized for landscape orientation only.
Sample implementation:
class CustomPageViewController: UIPageViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
if let underlyingScrollView = view.subviews.compactMap({ $0 as? UIScrollView })
.first {
let pangestureRecognizer = UIPanGestureRecognizer()
pangestureRecognizer.delegate = self
underlyingScrollView.addGestureRecognizer(pangestureRecognizer)
// at this point, the underlying scroll view will have two pan gesture
// recognizer side by side. We have the control of our added pan gesture
// recognizer through the delegate. We can conditionally recognize it or not
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer)
-> Bool {
// Returning true from here means, page view controller will behave as it is
// Returning false means, paging will be blocked
// As I needed to block paging only for landscape orientation, I'm just returning
// if orientation is in portrait or not
return UIApplication.shared.statusBarOrientation.isPortrait
}
}

GestureRecognizer doesn't work on TableView

I'm trying to find a way to hide the keyboard as soon as the user taps elsewhere on the screen after filtering the content of my tableview.
But somehow when I add an UITapGestureRecognizer my TableView freezes and it's impossible to go further into the application.
Here's what I do :
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
self.view.addGestureRecognizer(tap)
func dismissKeyboard() {
view.endEditing(true)
}
I've tried this on a Blank ViewController with a TextField and it works. What am I missing? Is there a specific way to add GestureRecognizer to table views? Because as soon I add the Gesture it breaks everything. I also tried using resignFirstResponder with the same results.
Any advice? Thanks!
Try setting the cancelsTouchesInView variable to false. This defaults to true, by setting it to false you allow the touches to be responded to by your gesture and then pass through your gesture back down to the view so the tableView can respond as well.
tap.cancelsTouchesInView = false
Documentation on Apple
Discussion Excerpt from Docs:
When this property is true (the default) and the receiver recognizes its gesture, the touches of that gesture that are pending are not delivered to the view and previously delivered touches are cancelled through a touchesCancelled:withEvent: message sent to the view. If a gesture recognizer doesn’t recognize its gesture or if the value of this property is false, the view receives all touches in the multi-touch sequence.

Handling UIPanGestureRecognizer gestures for multiple Views (one covers the other)

Sorry for such a long question, but felt I should convey what I have tried.
I've got a view viewA within a navigation controller. I am then adding a subview viewB (that contains a UITableView) to viewA and offsetting its origin height so that it covers only half the screen (with the other half overflowing off out the bottom of the screen). I want to be able to then drag this viewB upwards but it get stopped when it hits the bottom of the navigation bar and similarly get stopped when dragged back down when it hits the origin offset point. This I have achieved successfully.
However, I want the UITableView interaction to only be enabled when viewB is in its upper position and thus not respond to gestures in any other position. Essentially, dragging viewB up so that it completely covers viewA should enable interaction with the UITableView.
The tricky part here is that I want it to do the following:
If viewB is in its upper position so that it is covering the screen, the UITableView content offset is 0 (i.e. we are at the top of the table) and the user makes a pan gesture downwards, the gesture should not interact with the UITableView but should move viewB downwards.
Any other pan gesture in the above condition should be an interaction with the UITableView.
If viewB is in its upper position so that it is covering the screen, the UITableView content offset is NOT at 0 (i.e. we are NOT at the top of the table) and the user makes a pan gesture downwards, the gesture should interact with the UITableView.
I've been very close to achieving this but I can't get it quite right.
Attempts So Far
I'm using a UIPanGestureRecognizer to handle the dragging of the view. I have tried adding this to:
viewB with the UITableView user interaction initially disabled. This allows me to drag viewB up and down without interfering with the UITableView. Once viewB is in its upper position I enable UITableView user interaction which then correctly allows me to interact with the UITableView without moving viewB.
However, by enabling UITableView user interaction, this means touches never reach the UIPanGestureRecognizer, meaning I can never detect for the scenario described in point (1.) above and thus can't re-disable UITableView user interaction to make viewB movable again.
Maybe it is possible to do it this way by overriding the gesture recognition methods used by the UITableView? If this is possible can anyone point me in the right direction?
a new view added in front of the UITableView. I thought maybe I could forward the touch gestures to the UITableView behind it when necessary but I still haven't found a way to do this.
All I have been able to do is disable the gesture recognizer which allows me to interact with the UITableView, but then I have the same issue as above. I can't detect when to re-enable it.
the UITableView within viewB. This seemed to be the most promising way so far. By setting the return values of the following methods I can enable and disable recognition of either viewB and the UITableView.
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if pulloverVC.view.frame.origin.y == bottomNavbarY &&
pulloverVC.tableView?.contentOffset.y == 0 { // need to add gesture direction check to this condition
viewBisAtTop = true
return false // disable pullover control
}
return true // enable pullover control
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer as! UIPanGestureRecognizer).velocityInView(view).y < 0 && viewBisAtTop { // gesture direction check not wanted here
return true // enable tableview control
}
viewBisAtTop = false
return false // disable tableview control
}
The top method is called first when a gesture is made (I have checked with print statements) followed by the bottom method. By making different combinations of true/false for the 2 methods I can alternate interaction between viewB and the UITableView.
To detect whether the user is swiping downwards I am calling velocityInView() on the recognizer (as shown in the bottom method). I was intending on making this check in the top methods if statement and I think this would work, however, although velocityInView() works fine in the bottom method, it does not in the top one (velocity is always 0).
I have scoured SO for some solution and find many similar queries about gesture handling for views that cover each other, but these all seem to be regarding one gesture type, e.g. pinch, on one view, and another type, e.g. pan, on the other. In my case the gesture type is the same for both.
Maybe someone has a clever idea? Or maybe this is actually very simple to do and I have made this incredibly complicated? xD
Managed to get this working.
Of the methods described in my question above I removed the top one keeping just this (it has a few changes):
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if ((gestureRecognizer as! UIPanGestureRecognizer).velocityInView(view).y < 0
|| pulloverVC.tableView.contentOffset.y > 0)
&& pulloverVC.view.frame.origin.y == bottomNavbarY {
return true // enable tableview control
}
return false
}
The if statement checks that the covering UITableView is in its upper position AND that either the user is is not dragging downwards or the table content is offset (we are not at the top of the table). If this is true, then we return true to enable the tableview.
After this method is called, the standard method implemented to handle my pan gesture is called. In here I have an if statement that sort of checks the opposite to above, and if that's true, it prevents control over the covering viewB from moving:
func handlePanGesture(recognizer: UIPanGestureRecognizer) {
let gestureIsDraggingFromTopToBottom = (recognizer.velocityInView(view).y > 0)
if pulloverVC.view.frame.origin.y != bottomNavbarY || (pulloverVC.view.frame.origin.y == bottomNavbarY && gestureIsDraggingFromTopToBottom && pulloverVC.tableView.contentOffset.y == 0) {
...
This now keeps the UITableView interaction off unless its parent view viewB is in the correct position, and when it is, disables the movement of viewB so that only interaction with the UITableView works.
Then when, we are at the top of the table, and drag downwards, interaction with the UITableView is re-disabled and interaction with its parent view viewB is re-enabled.
A wordy post and answer, but if someone can make sense of what I'm saying, hopefully it will help you.

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.

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