I have a view that contains a subview that contains a pan gesture. subview contains an additional subSubview that also has a pan gesture recognizer of its own.
view -> subview -> subSubview
Normally, both subview and subSubview pan without issue.
Then I perform:
view.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
This also scales the subview and subSubview by 2.0
Now when I try to pan, the pan gesture method for subview continues to work perfectly.
However, the subSubview pan gesture now only gets picked up about 10% of the time. I haven't been able to figure out any pattern as to what causes it to trigger those few times.
Additionally, the subSubview correctly picks up touchesBegan every single time, as it should, but again doesn't fire the pan gesture except for 1 out of ten tries.
As soon as I scale the view back down to 1.0 everything goes back to normal.
Any thoughts as to what is going on here and what I can do to fix it?
Thanks!
Instead of using the transform which scales the appearance of the view but doesn't actually change its bounds, try actually changing your width / height constraints (which is animatable) as this affects the actual bounds/size of your view which is what the gesture recognizer cares about.
Related
The Problem
I have a UIScrollView containing a UIView that I wish to allow the user to pan using a UIPanGestureRecognizer.
In order for this to work as desired, users should be able to pan the view with one finger, but also be able to pan the scroll view with another finger - doing both at the same time (using one finger for each).
However, the scroll view ceases to work when the user is panning a view contained within it. It cannot be panned until the view's pan gesture ends.
Attempted Workaround
I tried to work around this by enabling simultaneous scrolling of both the pan view and the UIScrollView that contains it by overriding the following UIGestureRecognizerDelegate method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
However, this makes it so that panning the view also moves the scroll view. Each element's panning gesture should be independent of the other, not linked.
Demo Project
I have created a simple demo project that should demonstrate this, here:
https://github.com/jeffc-dev/ScrollViewPannerTest
This project contains a scroll view with a square view that should be able to be panned independently of its containing scroll view, but can not.
Why I'm Doing This
The point of this is to make it easier/quicker for a user to find a destination to pan the view to. The is somewhat analogous to rearranging icons in Springboard: You can use one finger to pan an app icon while simultaneously panning between pages with another finger, quickly finding a place to drop it. I'm not using a paged scroll view - just a normal one - and I want it to be a seamless panning gesture (I don't need/want the user to have to enter a 'wiggle mode') but the basic principle is the same.
UPDATE: DonMag helpfully came up with the idea of using a UILongPressGestureRecognizer to move the view out of the scroll view for panning, which does seem promising. However, if I went that route I think I'd need to seamlessly transition to using a UIPanGestureRecognizer after doing so (as I do use some pan gesture recognizer-specific functionality).
I'm sure there are different ways to do this, but here is one approach...
Instead of using a UIPanGesture I used a UILongPressGesture.
When the gesture begins, we move the view from the scrollView to its superview. While we continue to press the view and drag it around, it is now independent of the scrollView. When we end the gesture (lift the finger), we add the view back to the scrollView.
While dragging, we can use a second finger to scroll the content of the scroll view.
The main portion of the code looks like this:
#objc func handleLongPress(_ g: UILongPressGestureRecognizer) -> Void {
switch g.state {
case .began:
// get our superview and its superview
guard let sv = superview as? UIScrollView,
let ssv = sv.superview
else {
return
}
theScrollView = sv
theRootView = ssv
// convert center coords
let cvtCenter = theScrollView.convert(self.center, to: theRootView)
self.center = cvtCenter
curCenter = self.center
// add self to ssv (removes self from sv)
ssv.addSubview(self)
// start wiggling anim
startAnim()
// inform the controller
startCallback?(self)
case .changed:
guard let thisView = g.view else {
return
}
// get the gesture point
let point = g.location(in: thisView.superview)
// Calculate new center position
var newCenter = thisView.center;
newCenter.x += point.x - curCenter.x;
newCenter.y += point.y - curCenter.y;
// Update view center
thisView.center = newCenter
curCenter = newCenter
// inform the controller
movedCallback?(self)
default:
// stop wiggle anim
stopAnim()
// convert center to scroll view (original superview) coords
let cvtCenter = theRootView.convert(curCenter, to: theScrollView)
// update center
self.center = cvtCenter
// add self back to scroll view
theScrollView.addSubview(self)
// inform the controller
endedCallback?(self)
}
}
I forked your GitHub repo and added a new controller to demonstrate: https://github.com/DonMag/ScrollViewPannerTest
You'll see that it is just a Starting Point for this approach. The view being dragged (actually, in this demo, you can use two fingers to drag two views at the same time) uses closures to inform the controller about the dragging...
Currently, "drag/drop" does not affect any other subviews in the scrollView. The only closure that does anything is the "ended" closure, at which point the controller re-calcs the scrollView's contentSize. The "moved" closure could be used to re-position views -- but that's another task.
I'm new to coding so I'm trying some small projects in swift. Right now, I'm trying to make a text box inside the ViewController move when the user drags it along the screen. For the text box, I am currently using a UITextField but I have no idea how to program its movement according to drag.
You'll want to add a UIPanGestureRecognizer to your view. There's all sorts of built in gesture recognizers for detecting various gestures like a tap or in this case a pan (drag). You can check them out here: https://developer.apple.com/documentation/uikit/uigesturerecognizer
Here we'll create a pan gesture recognizer, and add it to our view. Assume myView is your UITextField. A good place to do this is in your view controller's viewDidLoad() method.
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(sender:)))
myView.addGestureRecognizer(pan)
The moment your finger touches the screen, we say that a touch sequence has begun. The touch sequence ends when there are no more fingers on the screen. The pan gesture will determine if this touch sequence looks like a pan, and if so, the method handlePan will be called at various stages. Here, the gesture itself will be passed into the method, which we use to determine translation and move our view accordingly. Add this as a method of your view controller.
#objc func handlePan(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: sender.view)
self.myView.center.x += translation.x
self.myView.center.y += translation.y
sender.setTranslation(CGPoint.zero, in: sender.view)
}
The first line gets the translation in the view which the gesture is attached to (myView). We then adjust myView's position based on this translation, and then we set the translation to zero. This is so that the next time this method is called, the translation will be a delta relative to the previous call.
The property sender.state will tell you the state the gesture is currently in, for example, .began, .changed, .ended. Since a pan is a continuous gesture, our method will be called many times, whenever there's a finger movement.
I use a ImageScrollView from here, which is basically a UIScrollView to allow pinch to zoom into a picture. I now wanted to add the possibility to swipe down the picture to dismiss the view controller. I created a UIPanGestureRecognizer and it works fine if zoom scale is at the minimum value (so the whole picture is visible without zoom). But how can I skip the pan gesture recognizer if the zoom scale is above the minimum value? Because it lays on top of the ImageScrollView, I can't scroll in the picture because scrolling gesture is fetched by the UIPanGestureRecognizer. Any idea how to solve this?
For show image like whatsApp imageView functionality you go with the apple framework QuikLook. It will automatically handle Zoom, Dismiss the image while Swipe etc.
It also support for the documents. It will reduce your effort a lot
It's too easy to handle this operation by adding a trigger on swipe action (gesture calling method).
When zoomScale > minimumValue; set returnstatement
A simple example with Swift 4:
let zoomScale: 1.0
let minimumValue: 0.5
func handlePanGesture(gesture: UIPanGestureRecognizer) {
if (#<set pan gesture down moving condition>#) {
if (zoomScale > minumumValue) {
return
}
}
// perform your next operations
}
I have a UIView at the bottom of my screen with a UIButtonon top, showing, when I press the button, view slides up with a nice animation.
Everything works fine, now I would like to enable swipe up gesture in the UIButton, so user doesn't actually have to press the button but just slide the finger.
To slide up the view, I just set the frame of it in button action. Now, would be also nice to swipe the view with the finger, something like when you release the view goes up or down, but if you don't release the swipe, the view stays at your finger. Does that make sense?
How could I achieve this?
[UIView animateWithDuration:1.0 animations:^{
self.tableView.contentInset = contentInsets;
self.tableView.scrollIndicatorInsets = contentInsets;
self.sliding.view.frame = CGRectMake(0, 418, 320, 150);
}];
Do you want to swipe the view, or swipe the button?
In either case, what you would do would be to create and configure a UISwipeGestureRecognizer, and attach it to the field that you want to respond to the swipe gesture.
I would suggest attaching the gesture recognizer to the view that's being moved, not to the button. I think it would be confusing to swipe on a button and have it slide a view to the side as a result.
Take a look at the docs on UISwipeGestureRecognizer in Xcode. You'll want to set the direction and number of touches required.
You'll want to create a swipe gesture recognizer with initWithTarget:action:, then attach it to the view with the UIView method addGestureRecognizer:. If you are attaching the gesture recognizer to a non-control, you might need to set the userInteractionEnabled flag on the view to YES.
In order to achieve this, you need to use swipe gesture recognizers.
UISwipeGestureRecognizer object is will help you to swipe movement with your fingers in any way.
It's include left and right direction due to understand the user which way to swipe .
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.