TLDR; If the user drags their finger over multiple nodes, how can I get all the nodes they touched while dragging?
I'm trying to build a Wordsearch-like game to teach myself SwiftUI on iOS.
I've built a grid of tiles as shown (ignore blue colour tiles as they are not relevant for my question).
I'd like the user to drag their finger over the grid, and for every tile they swipe over, I'll call a method on my model to mark that tile as "selected".
I've tried a DragGesture on each tile's view, like so;
.gesture(
DragGesture()
.onChanged { _ in
myModel.SelectTile(self.tile.id);
}
)
However as the user's finger moves out of the starting tile and into the next, the next node's drag gesture is not actioned until the user lifts their finger and starts a new gesture on that tile.
Is this behaviour possible with SwiftUI? I realise I can probably do this more easily with SpriteKit but that seems a little overkill for this kind of "game".
I've found another way to accomplish what I need;
By applying the DragGesture to the grid itself; I can get the touch location within the grid.
Therefore a bit of simple division and flooring gives me the location of the touched tile in the grid, like so;
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _gesture in
let tileSize = geometry.size.width / CGFloat(self.game.grid.width);
let x : Int = Int(_gesture.location.x / tileSize);
let y : Int = Int(_gesture.location.y / tileSize);
self.game.SelectTile(_x: x, _y: y);
}
)
Related
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
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.
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.
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.
I'm trying to add the "virus" as a child of the "cell", however only the cell appears. I tried adding both to the scene and then adding cell to virus, but the program broke. What is the proper way to add a child to an existing SkSpriteNode?
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let virus : VirusNode = VirusNode.create(CGPoint(x: self.frame.maxX / 2, y: self.frame.maxY / 2))
let cell = SKShapeNode(circleOfRadius: CGFloat(10))
cell.fillColor = UIColor.greenColor()
cell.position = CGPoint(x: self.frame.maxX / 2, y: self.frame.maxY / 2)
cell.addChild(virus) //virus is just a SKSpriteNode
self.addChild(cell)
}
Your issue is you need to understand how frame works when dealing with parent child relationships.
The child's frame will always be in the coordinate system of the parent, so when you create your virus. You need to keep this in mind.
As of right now you are setting your virus to the center of the scene. You then set your cell at the center of the scene. When you add the virus to the cell, you are changing the frame to be the location of the parent (Center of the screen) + location of the child (Center of the screen), which puts the virus on the top right of the scene. Which is not what you want.
Instead, set the virus at position (0,0). When you add it to cell, you will find that he is located in the center of your cell.
In regards to the program crashing, if you add an SKNode to another SKNode(or SKScene), you need to remove it from its parent before you can give it to another SKNode. Think of your job like child protective services, you need to remove the child from his original home before you can give the child to his new parents, otherwise you may end up with a wicked confrontation between the two parents that you can't control.