How do I find out which direction a user is panning with UIPanGestureRecognizer? - ios

So I am using UIPanGestureRecognizer in my project which I added to a view. I would like to know when a user either goes up, down,left or right. I am using the left and right feature to scrub through video. The up and down gesture is still to be determined. I have used the following code but I can't seem to figure it out. Thanks for the help!
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else {
// user dragged towards the left
print("left")
}
}
EDIT: Using Slider
if let duration = avPlayer?.currentItem?.duration {
let totalSeconds = CMTimeGetSeconds(duration)
let value = Float64(scrubberSlider.value) * totalSeconds
let seekTime = CMTime(value: Int64(value), timescale: 1)
avPlayer?.seek(to: seekTime, completionHandler: { (completedSeek) in
//perhaps do something later here
})
}

Joe's answer is close, but it won't take into account direct vertical or horizontal pans. (I'd comment on his answer except the formatting won't take.) Try this:
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else if vel.x < 0 {
// user dragged towards the left
print("left")
}
if vel.y > 0 {
// user dragged towards the down
print("down")
}
else vel.y < 0 {
// user dragged towards the up
print("up")
In essence, you are getting the CGPoint of the gesture (x,y) and determining the velocity of the movement. You have an alternative to this - taking the starting and ending point:
var startingPoint = CGPoint.zero
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
startingPoint = recognizer.location(in: self.videoView)
}
if recognizer.state == .ended {
let endingPoint = recognizer.location(in: self.videoView)
[ do the same comparing as above]
}
}
The advantage of the second option is you aren't doing unnecessary calculations during the pan. The disadvantage is that there are certain scenarios (like animating view movements) that are not conducive to it.
EDIT: I'm adding a bit more verbiage after reading your comment. It sounds to me that you may not be fully understanding what a pan gesture really is.
Like most (all?) gestures, it has a beginning, an in-between, and and end.
It is a two-dimensional drag with two components, both x and y.
There are actually SEVEN possible states, but FOUR of them (cancelled, failed, possible, recognized) do not happen with a pan gesture, leaving THREE states (began, changed, ended) that trigger.
I threw out one example - moving a view with a pan gesture - earlier. Now I'll try a second one - tracing an outline of, say, the Statue of Liberty in an image.
Here you want all THREE states, in order to know when to being tracing, when the path changes, and when it ends. And restricting this to the change state, I think you can see where both the X and the Y coordinate changes.
So yes, a logging of "left, up, left, up, left" is quite possible.I would think that if you traced a completely vertical line across the entire screen you might expect all "up" or "down" values in your log, but the odds of any human being panning that perfect is unlikely, so sure, a few "left" or "rights" may happen.
My tweak to Joe's code was to eliminate those moments of perfection. If vel.x == 0 you would have "left", and where bel.y == 0 you would have "down".
Again, if you simply want to know what the "result" of the pan is, use .began and .ended and ignore .changed - do not use recognizer.velocity but recognizer.state.
The "if" statements both of us gave you are really frameworks. If you understand both state and the two-dimensional nature of things, and you need to use .changed, then adapt those "if" statements - maybe compare the velocity of X to Y and take the greater, or eliminate those changes where the change in X or Y was under a threshold.

Try this code: tested in Swift 3.
Updated Answer: Below code will give you a starting and end location of your view when touch began.
if recognizer.state == .began {
let vel = recognizer.velocity(in: view) // view is your UIView
if vel.x > 0 {
print("right")
} else {
print("left")
}
}
if recognizer.state == .ended {
let vel = recognizer.velocity(in: view)
if vel.y > 0 {
print("down")
} else {
print("up")
}
}
Note : Your answer actually hidden in your code ?
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else {
// user dragged towards the left
print("left")
}
if vel.y > 0 {
// user dragged towards the down
print("down")
}
else {
// user dragged towards the up
print("up")
}
}
hope this helps...

Okay, now I'm getting the correct mental picture. You want scrub control. This is something very different, and I would recommend a UISlider over working with gestures - highly recommend it. For starters, they have the pan gesture already built in! Here's what I think apps like YouTube, QuickTime, etc. do.
(1) Let's take a specific example of having a video that is 1:53:22 in length, or (1*60*60)+(53*60)+22 = 6802 seconds in length.
(2) Add a "scrubber" subview to your main screen. You'll probably want a UISlider, two UILabels (one to each side of the slider), and anything else you think for a polished look.
(3) The UISLider will have a minimumValue of 0 seconds and a maximumValue of 6802 seconds. Of course, you'll want that max value to be calculated on each change of source.
(4) A question you'll want to answer for your app is whether to go the route of iTunes (where this scrubber view is always visible) or YouTube (where it is overly visible when the user or mouse cursor hovers over an area). For the former, you just need to position this scrub view in a position on the screen. For the latter though, you may wish to use a pan gesture - but only for visibility. Hold that thought.
(5a) You need two, maybe three more things on you UISlider. First is an automatic value update. Again it will depend on the visibility of the entire scrub view. You want to update, once a second, both the left hand UILabel and the UISLider value if it's always visible. For a disappearing one you probably can get away with only updating it once a second when it's visible.
(5b) The second thing you need to do with the UISlider is track changes the user makes to it (the "scrubbing") while it's visible. The event you are looking for is UIControl.valueChanged(). It will trigger anytime the user works with the slider, giving you the new seconds value to "scrub" the video to.
(5c) The third thing you might want to do with the UISlider is customize it a few ways - change the thumb image and the slider itself. My app changes the thumb image. These can only be done in code, there are no IB properties available.
Back to #4. All of the above doesn't need a pan gesture, unless you want the scrub view to appear only when needed.
If you have a mental picture of what I've described above, all you want to know is if a pan gesture has happened. No regards for direction. You might wish to have some regards for screen area - do want this scrub view to appear when a user pans over an area where the scrub view will not appear?
Wire up a CALayer (or the entire video view) with the pan gesture. Then code for a state of UIGestureRecognizer.began. Make the scrub view visible by changing it's alpha state from 0 to 1, or "sliding" it into view by changing it's origin or height. Add a UIView.animate(withDuration:) to it for a good effect.
Now, all that's left is setting the scrub view back to it's natural state. You'll need to code the reverse of whatever you did, and attach it to a timer set for however many seconds you want it visible.
TL;DR;
My app uses 4 UISliders that change various things (height, width, saturation, grill thickness) of of a photo effect that uses CoreImage. Performance is very tight, about 5/100 of a second to grab the new values of all 4 sliders and update the image.
These sliders are always visible today, but my next update (about 2 weeks away) will feature a "sliding control board" - think a keyboard with sliders and other controls on it. (There's limitations on the alpha value for a custom keyboard that forced me to write my own, but that's a separate discussion.)
So I know a "sliding scrub view" is possible. What I don't know for you is if you set the alpha value to a view to zero, will it detect pan gestures? I don't know, thus a CALayer may be needed.

Related

Move objects around, with gesture recognizer for multiple Objects

I am trying to make an app where you can use Stickers like on Snapchat and Instagram. It fully worked to find a technique, that adds the images, but now I want that if you swipe the object around the object changes its position (I also want to make the scale / rotate function).
My code looks like this:
#objc func StickerLaden() {
for i in 0 ..< alleSticker.count {
let imageView = UIImageView(image: alleSticker[i])
imageView.frame = CGRect(x: StickerXScale[i], y:StickerYScale[i], width: StickerScale[i], height: StickerScale[i])
ImageViewsSticker.append(imageView)
ImageView.addSubview(imageView)
imageView.isUserInteractionEnabled = true
let aSelector : Selector = "SlideFunc"
let slideGesture = UISwipeGestureRecognizer(target: self, action: aSelector)
imageView.addGestureRecognizer(slideGesture)
}
}
func SlideFunc(fromPoint:CGPoint, toPoint: CGPoint) {
}
Here are the high-level steps you need to take:
Add one UIPanGestureRecognizer to the parent view that has the images on it.
Implement UIGestureRecognizerDelegate methods to keep track of the user touching and releasing the screen.
On first touch, loop through all your images and call image.frame.contains(touchPoint). Add all images that are under the touch point to an array.
Loop through the list of touched images and calculate the distance of the touch point to the center of the image. Chose the image whose center is closest to the touched point.
Move the chosen image to the top of the view stack. [You now have selected an image and made it visible.]
Next, when you receive pan events, change the frame of the chosen image accordingly.
Once the user releases the screen, reset any state variables you may have, so that you can start again when the next touch is done.
The above will give you a nicely working pan solution. It's a good amount of things you need to sort out, but it's not very difficult.
As I said in my comment, scale and rotate are very tricky. I advise you to forget that for a bit and first implement other parts of your app.

How do I alter the touch location when handling a pinch and a pan gesture recognizer at the same time?

I'm trying to recreate an interaction similar to the photos app where you can pinch and pan a photo at the same time. Adding or removing a touch mid pan works perfectly.
In my code I'm using the location of touch to move the view. When I drag with two fingers, the pan gesture recognizers puts the point between the two fingers (as it should), but when I lift a finger it changes the point to that of that one finger, causing the view to jerk to a new position.
Setting the maximumNumberOfTouches to 1 does not solve my problem since you can touch with finger1, pan, touch with finger 2, pan, lift finger 1 and the view will jerk to the position of finger 2. Plus, I want to allow 2 finger panning since they can pinch to zoom and rotate the image as well.
I also cannot use UIScrollView for this for other reasons, but I know it doesn't have that problem.
The only solution I can think of is to get the initial touch location, then every time a finger is added or removed, offset the new location based on the old location. But I'm not sure how to get that information.
Is there an API for this? Is the above way the only way, and if so, how do I do it?
As I understand it, the issue is that your code for responding to a pan (drag) doesn't work if the user changes the number of fingers in mid-drag, because the gesture recognizer's location(in:) jumps.
The problem is that the entire basic assumption underlying your code is wrong. To make a view draggable, you do not check the location(in:). You check the translation(in:). That's what it's for.
This is the standard pattern for making a view draggable with a pan gesture recognizer:
#objc func dragging(_ p : UIPanGestureRecognizer) {
let v = p.view!
switch p.state {
case .began, .changed:
let delta = p.translation(in:v.superview)
var c = v.center
c.x += delta.x; c.y += delta.y
v.center = c
p.setTranslation(.zero, in: v.superview)
default: break
}
}
That works fine even if the user starts with multiple fingers and lifts some during the drag.
Ok, so here's how I solved it.
Inside the gesture function I have a global variable being given the touch location.
self.touchInView.x = sender.location(in: superview).x - frame.origin.x
self.touchInView.y = sender.location(in: superview).y - frame.origin.y
self.touchInParent = sender.location(in: superview)
In state == .began I have a variable called OriginalTouch which I set the location of touch.
if gesture.state == .began {
originalTouch = self.touchInView
}
Then in state == .changed I detect if the number of touches changed and calculate the offset:
//Reset original touch position if number of touch changes so view remains in the same position
if sender.numberOfTouches != lastNumberOfTouches {
originalTouch.x += (touchInView.x - originalTouch.x)
originalTouch.y += (touchInView.y - originalTouch.y)
}
lastNumberOfTouches = sender.numberOfTouches
Now I can set the view's location based on the originalTouch
self.frame.origin = touchInParent - originalTouch

Use UIPanGestureRecognizer to drag view from one position to limited position

In my application I have an UIView.I want functionality such that user can drag the view from its original position to particular limited position for this I have used **UIPanGestureRecognizer Class ** and in gestureRecognizer.state == .Changed condition I am changing the coordinates of view .I am able to drag the view to limited position when moving slowly but The problem is if the user drags the view very rapidly upward or downward the screen, then the view can be pulled beyond the limits I put on the Y position
if(upperLimit > (self.topbaseConstrant.constant * -1))
{
self.topbaseConstrant.constant += gestureRecognizer.translationInView(self.view!).y
gestureRecognizer.setTranslation(CGPointZero, inView: self.view!)
}
I have been trying to solve the issue since last three days .Please give me suggestion
Thanks in advance
Use the min function to determine upper limits
let newPosition = topbaseConstrant.constant + panGestureRecognizer.translationInView(nil).y
topbaseConstrant.constant = min(upperLimit, newPosition)
If you drag quickly and blow past your constraint, the min function will always return that upper constraint as your new position.

Animate a View to give scrolling animation like traditional volume button

I have a volume button like this which need to be animated to give scrolling effect and increase volume of device.
I know how to increase volume of iPhone device but unable to find out how to give animation and pass values accordingly to the API (increasing volume or decreasing volume). Whether a user is scrolling up or down.
Essentially you need to add a UIPanGestureRecognizer to the image view displaying the volume thingo. Then assign a method to handle the
func volumeViewDidPan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translationInView(recognizer.view)
if recognizer.state == .Began {
initialY = translation.y
}
else {
if translation.y <= initialY {
//Their finger is moving up towards 0 or the same
//Calculate step - update image/volume
}
else {
//Their finger is moving down towards height
//Calculate step - update image/volume
}
}
}
Then based on the value, offset/steps you are doing you can trigger your volume changes.

dismiss view controller custom animation

I am trying to replicate this animation to dismiss a view controller (15 second video): https://www.youtube.com/watch?v=u87thAbT0CQ
This is what my animation looks like so far: https://www.youtube.com/watch?v=A2XmXTVxLdw
This is my code for the pan gesture recognizer:
#IBAction func recognizerDragged(sender: AnyObject) {
let displacement = recognizer.translationInView(view)
view.center = CGPointMake(view.center.x + displacement.x, view.center.y + displacement.y)
recognizer.setTranslation(CGPoint.zero, inView: view)
switch recognizer.state {
case .Ended:
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layer.position = CGPoint(x: self.view.frame.width / 2, y: self.view.frame.height / 2)
})
default:
print("default")
}
let velocity = recognizer.velocityInView(self.titleView)
print(velocity)
if velocity.y < -1500 {
up = true
self.dismissViewControllerAnimated(true, completion: nil)
}
if velocity.x > 1500 {
right = true
self.dismissViewControllerAnimated(true, completion: nil)
}
}
It may be a little hard to notice in my video, but there is a small disconnect in how fast the user flicks up, and how fast the animation completes. That is to say, the user may flip up very fast but the animation is set to a hardcoded 0.3 seconds. So if the user flicks the view fast, then as the animation completes, as soon as their finger lifts off the view, the animation actually slows down.
I think what I need is a way to take the velocity recorded in the recognizerDragged IBAction, and pass that to the animation controller, and based on that, calculate how long the animation should take, so that the velocity is consistent throughout, and it looks smooth. How can I do that?
Additioanlly, I'm slightly confused because the Apple Documentation says that the velocityInView function returns a velocity in points, not pixels. Yet different iOS devices have different points per pixels, so that would further complicate how I would translate the velocity before passing it to the animation class.
Any idea how to pass the velocity back to the animation controller, so that the animation duration changes based on that, and make it work for different iPhones ?
thanks
What you are likely looking at in the video you are trying to replicate is a UIDynamics style interaction, not a CoreAnimation animation. The velocity returned from velocityInView can be used directly in UIDynamics like this:
[self.behavior addLinearVelocity:
[pan velocityInView:pan.view.superview] forItem:pan.view];
I wrote a tutorial for doing this style of view interaction here: https://www.smashingmagazine.com/2014/03/ios7-new-dynamic-app-interactions/
To stick with UIView animations you just need to look at the frame's bottom (which is also in points) and calculate the new time. This assume that you want frame's bottom to be at 0 at the end of the animation:
animationTime = CGRectGetMaxY(frame) / velocity
You aren't showing how you created the animation controller, but just keep a reference to it and pass the time before calling dismiss. This is also assuming you are using a linear curve. With any other kind of curve, you will have to estimate what the starting velocity would have to be to be based on time and adjust.

Resources