SceneKit – pan gesture is moving node too quickly - ios

I'm currently trying to use a pan gesture recognizer to move a node in SceneKit. I'm only moving it along the X-axis, however my gesture moves the object a lot further/faster then it should even when only using small gestures. I'm not 100% sure what I'm doing wrong here but here's the code for my gesture recognizer:
#objc func handlePan(_ pan:UIPanGestureRecognizer) {
if pan.state == .changed {
let translation = pan.translation(in: pan.view!)
node!.position = SCNVector3(x:node!.position.x + Float(translation.x), y:node!.position.y, z:node!.position.z)
pan.setTranslation(CGPoint.zero, in: pan.view!)
}
}
As I say the object is being moved it's just being launched at incredible speed and distance. The effect almost appears cumulative.
I thought this could be the case if I didn't reset the translation of my pan gesture recognizer, but I am doing that here
pan.setTranslation(CGPoint.zero, in: pan.view!)
I'm actually trying to get this work in an ARKit scenario, but I've stripped all that out to just get a node moving correctly but I'm still having issues.
The pan is added to an ARSCNView whereas the node I'm trying to manipulate is added as a childNode to the ARSCNView.scene.rootNode so I'm wondering if it's the positions/coordinates of these that are the problem.

let translation = pan.translation(in: pan.view!)
This code returns CGPoint with gesture position in the view in points (which is could be pixels). But SCNNode position (in real world) is position in meters. So, when you're adding one point for X position in SCNVector, you're actually adding one meter for that.
To convert screen point into 3D world coordinates use unprojectPoint method of ARSCNView. You probably will need to save previous gesture position to be able to find position changes.

Related

Xcode 9 Swift 4 - Reposition views by dragging

Im quite new to iOS development. But have years of programming experience.
Anyway, Im having a hard time finding a solution for my problem.
In my app i render rows of colored circles based on data from the server.
Each of these circles has different properties set to them on the server.
One of these is the "offset" property.
This should be used to render the circle with a distance from its left sibling, or the start of the parent view if its the first.
Each circle should then also be able to be moved by dragging it to the right or left. But never less then 0 from its left sibling.
In android this was very easy, just set the left-margin on drag, and all was good.
But in xcode im having a very hard time figuring out how to get this done.
Im sure its me thats way to inexperienced. So I hope someone that has a bit more knowledge about swift can help me with this.
Heres some images to make clear what Im looking to achive.
First render where one circle has an offset
The gesture where the 3. last circle is drages to the right
The result of the gesture
I need this to move seamless, so not reposiotioning after the gesture ends, but move along with the finger.
As you can see, the circles right of the one that is drages, keep their relative position to the one that is moved.
Thank you.
There are couples of ways to do this.The First possible solution can be using the Swipe gestures to move the objects.
override func viewDidLoad() {
super.viewDidLoad()
let swipeGesture = UISwipeGestureRecognizer(target: self, action: "handleSwipe:")
swipeGesture.direction = [.Down, .Up]
self.view.addGestureRecognizer(swipeGesture)
}
func handleSwipe(sender: UISwipeGestureRecognizer) {
print(sender.direction)
}
Use these Gestures to move along the objects with your fingers either you can use .left and .right gestures depending upon your need.
The Second solution for drag components can be a Pan Gesture
func detectPan(recognizer:UIPanGestureRecognizer) {
var translation = recognizer.translationInView(self.superview!)
self.center = CGPointMake(lastLocation.x + translation.x, lastLocation.y + translation.y)
}
The translation variable detects the new coordinates of the view when panning. The center of the view will be adjusted according to the changed coordinates.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// Promote the touched view
self.superview?.bringSubviewToFront(self)
// Remember original location
lastLocation = self.center
}
When the view is clicked, the current view will be displayed in front of the other views and the center of the view will be assigned to the lastlocation variable
Hope this helps.

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

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

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.

How can I add sizing handles to a UIView?

I'm trying to dynamically create views (UIImageView and UITextView) at runtime by user request and then allow the user to move and resize them. I've got everything working great, except for the resizing. I tried using the pinch gesture recognizer, but find it too clumsy for what I want. Therefore, I would like to use sizing handles. I believe I could put a pan gesture recognizer on each handle, and adjust the view frame as one of them is moved.
The problem is, I'm not quite sure how to create the sizing handles. I would indicate all the things I've tried, but truthfully, I'm not quite sure where to start. I do have a few ideas...
1) Possibly use coregraphics to draw boxes or circles on the corners and sides? Would I create a new layer and draw them on that? Not sure.
2) Stick a little image of a box or circle on each corner?
3) XIB file with the handles already placed on it?
Any suggestions appreciated. I just need to be pointed in the right direction.
Edit: Something like what Apple uses in Pages would be perfect!
First, I suggest create a custom View subclass to UIView, you will handle all of the behavior here. Let's call it ResizableView.
In the custom view, You need to draw layer or view for these dot at corner and add PangestureRecognized to them.Then you can track the location of these dot using recognizer.locationInView() when user drag them, which you will use to calculate the scale of View.Here is the code you can refer to:
func rotateViewPanGesture(recognizer: UIPanGestureRecognizer) {
touchLocation = recognizer.locationInView(self.superview)
let center = CalculateFunctions.CGRectGetCenter(self.frame)
switch recognizer.state {
case .Began:
initialBounds = self.bounds
initialDistance = CalculateFunctions.CGpointGetDistance(center, point2: touchLocation!)
case .Changed:
//Finding scale between current touchPoint and previous touchPoint
let scale = sqrtf(Float(CalculateFunctions.CGpointGetDistance(center, point2: touchLocation!)) / Float(initialDistance!))
let scaleRect = CalculateFunctions.CGRectScale(initialBounds!, wScale: CGFloat(scale), hScale: CGFloat(scale))
self.bounds = scaleRect
self.refresh()
case:.Ended:
self.refresh()
default:break
Step by step
touchLocation location of the Pangesture
center is the center of ResizableView
initialBounds is the initial bounds of the ResizableView when PanGesture begin.
initailDistance is the distance between the center of the ResizableView of touchPoint of the dot the user is dragging.
Then you can calculate the scale given initialDistance, center, touch location
Now you have scaled the view as You want. You also need to refresh the position of these dot at corner accordingly, that's what refresh() for, you need to implement it yourself.
CalculateFunctions
I tend to define some helpFunctions to help me calculate.
CalculateFunctions.CGPointGetDistance is used to calculate the distance between center of the view and touch location.
CalculateFunctions.CGRectScale return the scaled CGRect given the the scale you just calculated.
CalculateFunctions.CGRectGetCenter return the center point of a CGRect
That's just a rough idea. Actually there are many Libraries you can refer to.
Some suggestions:
SPUserResizableView
This is a ResizableView exactly what you want, but it was written in ObjC and hasn't been updated for a long time. But you can refer to it.
JLStickerTextView This may not fit your requirement very well as it is for text(edit, resize, rotate with one finger) However, this one is written in Swift, a good example for you.
If you have any questions, feel free to post it.Good Luck:-)

How to relatively move number of objects by moving one of them with Swift?

I have a question.
I have 4 Image View objects is my storyboard. Those represents "corners". I'd like to have this option, when you move one of corners, and other three moves relatively.
Like on animated gif here
What I do now is applying Pan Gesture Recognizer to Image Views in storyboard. Then I add code to ViewController:
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
recognizer.view!.center = CGPoint(x:recognizer.view!.center.x + translation.x,
y:recognizer.view!.center.y + translation.y)
recognizer.setTranslation(CGPointZero, inView: self.view)
}
But I'm not sure what to do next. I need to recognise which corner is tapped and moving, so I could apply special relation moving function for other corners.
Heyo,
One technique is to find a vector representing the pan gesture, and then using that vector to update the rest of the image views. For instance, UIPanGestureRecognizers extends from UIGestureRecognizer, which has the method:
func locationOfTouch(_ touchIndex: Int, inView view: UIView?) -> CGPoint
The pseudocode would go somewhat like this:
Get 1 imageView's last known location as a CGPoint
Detect the pan gesture
Retrieve the location of the touch, as a CGPoint
Now that you have two CGPoints, 1 representing the start position and 1 representing the end position, you can subtract the two points to form a vector with a length and direction. Using this information, you can come up with a formula to displace the other 3 imageViews.

Resources