iOS: Draggable UIView (vertically only) - ios

In my app I have a small subview that I'd like to tether in the bottom right corner and allow to be moved only vertically for a fixed distance.
I have some code to allow it to be dragged, but no further constraints:
- (void)viewDragged:(UIPanGestureRecognizer *)gesture
{
CGPoint translation = [gesture translationInView:keyView];
// move view
keyView.center = CGPointMake(keyView.center.x + translation.x,
keyView.center.y + translation.y);
// reset translation
[gesture setTranslation:CGPointZero inView:keyView];
}
I've looked around on StackOverflow and tried a couple of ways, but the only one that worked didn't take into account diagonal movement, and I couldn't find anything in regards to tethering it to one spot.
So does anyone know how this can be done? Many thanks
EDIT: Also, does anyone know of a good, informative reference doc (other than the Apple one) for topics of this nature?

can you just add in a line to the view dragged code that says if keyview.centre.y is less than some threshold (where you want it to stop), then don't change it below that. eg.
keyView.centre = CGPointMake(keyView.center.x,
keyView.center.y - translation.y < threshold ? threshold : keyView.center.y);
also remove the change to the x parameter to stop it moving diagonally.

Related

Swift - PanGesture Recognizer - move with object - speed of movement

I am using PanGesture Recognizer in Swift for iOS. Inside the action method, I am calling 3rd party method, that takes move direction and speed. From this, it calculates position of object:
objectPos += normalize(move) * speed
Problem is, that if I put my finger on a certain object and move with fingers, objects is not at the same position under my finger. It starts to move slower / faster. Moving directions are OK. Problem is with acceleration / decceleration - if I move faster, objects move faster.
In gesture callback I have tried:
let move = recognizer.translation(in: self.view);
let speed = sqrt((move.x * move.x) + (move.y * move.y));
and
let move = recognizer.velocity(in: self.view);
let speed = dt * sqrt((move.x * move.x) + (move.y * move.y));
Usually dt = 1.0 / 60.0. It is the gesture callback refresh rate (in code, I am calculating dt manually using difference of CFAbsoluteTimeGetCurrent()). Without this, If I use velocity directly to calculate speed, movement is too fast.
I have tried to calculate difference manually by subtracting current and last position, but still no luck.
I have also tried to "change speed" accoring to current view width and height, but none ot if worked. I am probably missing something, but dont know what.
It would be easier if you just computed the moves of the object based on acceleration when the user stops touching the object.
As long as the user has their finger on the object, it is much easier to just set the position of the object to the position you get from the pan gesture recognizer.
Ok... I have found the problem. Movement of th eobject it in screen normalized coordinates with corners [0,0] - [1,1]. So movement in Y axis (height) was correct, but in X axis (width) the speed was about half.
Multiply move.x with correct aspect ratio solved the problem. Basically, the moevement in X axis is enlarged manually.

ScrollView in SpriteKit

I have found a few other questions and answers similar to this, but none of them quite work perfect for me.
My vision is to have a horizontal scrollable view at the bottom of the screen where I can scroll through what will look like cards in a hand. Ideally, I could eventually make the card in the middle scaled up a bit and give it a highlighted look to show the user which card is selected. Even better would be if I could figure out how to keep the scroll view resizing to fir the number of sprites (cards) in the view.
Anyways, I am still very new to XCode and Swift, so it is hard for me to take what I find and change it. But, I am hoping to learn fast.
What I understand so far is that a UIScrollView could overlay the scene and with a moveable spritenode I could scroll through the view. The view would then translate the coordinates somehow to the SpriteKit Scene to move the sprites that will look like they are in the view. I think that's how it works. Any help would be great. I am pretty stuck. <3
You have to make your own logic that takes place in touchesMoved() using a global/member variable.
Unfortunately, a lot of gamedev and SK is math and logic.. You have to come up with your own problems and solutions.. There is no manual because the possibilities in programming and Swift are endless :)
Moving the cards:
Basically, you compare each touch location to the last one, and this becomes a "delta value" that you can use to perform actions.
Example, if I touch in the center of the screen, my touch location is 0,0 (or whatever your anchorpoint is set to). If I move my finger right, then I'm now at say 25, 0... This creates a "delta value" of +25x.
With that delta value, you can perform various actions such as moveBy for all the cards... so if I have a deltaX of +25, then I need to move all of the card nodes to the right (by a certain amount that you will determine according to your preferences). If I have a deltaX of -25, I move the cards to the left by a certain amount.
Where you do the actual moving is up to you--you could put a function in update() or touchesMoved() that constantly moves the cards a certain direction at a certain rate of that deltaX value..
Ok that was a mouthful... Maybe this will help:
for touch in touches {
myGlobalDeltaX = myDeltaXFunc(currentTouch: touch)
myMoveFunc(cards: allTheCards, byDeltaX: myGlobalDeltaX)
- You can search on how to make a Delta function, but it really is just the same thing from Algebra.
- myMoveFunc can be something as simple as iterating through all of your card nodes then running .moveBy on them at the same time.
Middle detection:
To detect which card is in the center, you would put in touchesEnded() or update() a call to check the name / identity of the node in the center of the screen... so something like
// `self` here refers to your GameScene class' instance, which is just an `SKScene` object
let centerX = self.frame.midX
let centerY = self.frame.midY
let center = CGPoint(x: centerX, y: centerY)
let centerNode = self.nodes(at: center)
You would obviously want to change centerX and centerY to wherever it is you want the middle card to be :) Right now, this is just in the dead-center of the screen.
Once you have a centerNode, you would then just need to do whatever function you have created to "select" it.
let selectedCard = centerNode
mySelectionFunc(middleCard: selectedCard)
This may look like a lot, but I drew out the steps to make understanding it a bit easier.. You can do all of this in one line if desired.
mySelectionFunc(middleCard: self.nodes(at: CGPoint(x: self.frame.x, y: self.frame.y)))
Hope this helps some!

How can I get the point's coordinate in iOS-charts?

I'm trying to get LineChart's plots' coordinate.
I've searched several hours, but I found only one solution, but it depends on xAxis only and always set CGPoint.y to zero.
I want to get the X and Y coordinates.
The reason is that the graph's xAxis is on hour base and showing year based, so it's really hard the tap the point to show a marker even I spread the graph. I tried many things, but I couldn't figure it and not succeeded to detect the tap.
Then I thought that if I can get the point's coordinate, I can build a bd-tree to store them and find the nearest coordinates from the tapped point.
Thank you for your help.
You have to implement the ChartViewDelegate and then implement the following method to get the touch point in your chartview
-(void)chartValueNothingSelected:(ChartViewBase *)chartView{
NSArray *gestureRecognizers = chartView.gestureRecognizers;
int count = 0;
for(UIGestureRecognizer *gesture in chartView.gestureRecognizers){
if([gesture isKindOfClass:[UITapGestureRecognizer class]] && gesture.state == UIGestureRecognizerStateEnded){
float xVal = [self.chartDataView getValueByTouchPointWithPt:[gesture locationInView:self.chartDataView] axis:AxisDependencyLeft].x;
}
}
}
It's too much to explain here.
There is ChartTransformer, which is used to convert data coordinate space into pixel coordinate space and vice versa.
So, in ChartTransformer, there are several methods like pointValueToPixel and pixelToValue to get the job done.
You can try figure out a clever way to write more methods or delegates to pass the coordinates you want.
You have to look at the code in detail to know how to use them. A good example is x axis renderer's drawGridLine.

How to check if bottom of a SKSpriteNode is inside the current visible scene

I've been looking at Figure 7-3 in this sprite kit documentation: https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Actions/Actions.html but it has left me pretty confused. They seem to give the same name to multiple things e.g., camera/character.
I am in the default SKScene, MyScene class. In initWithSize, I create a SKNode *myWorld, just like the documentation suggests. I then have a series of methods that add my background images to myWorld. Scrolling that works just fine, but what I want to do is stop the vertical scrolling when the bottom of the images in myWorld reach the bottom of the scene. For the life of me, I cannot figure out how to refer to the bottom of myWorld. For the bottom of the scene, I simply do
CGPoint sceneFarBottomSide = CGPointMake(0, -self.size.height/2);
where self.anhorPoint is set to [0.5, 0.5].
How do I refer to the bottom of myWorld?
The edge of myWorld is whatever you set it to be. In other words, myWorld is a node which isn't itself a view or a sprite. It's simply an object that contains sprite or shape children (for example, SKSpriteNodes or SKShapeNodes). When you are adding your sprites to myWorld, keep track of their position. Then use their position to define the "size" of myWorld. You can use this size information along with myWorld.position to know when the (bottom) edge of myWorld is coming up.
It ended up being extraordinarily easy to resolve. Thanks Andrey for pointing me to that Apple documentation on the Adventure game, that's what really tipped me off and cleared up some of my understanding. Here's the few lines of code to get the behavior I desired:
// Move world
if (monkeyPosition.y > 0 && monkeyPosition.x > 0) {
[myWorld setPosition:CGPointMake(-monkeyPosition.x, -monkeyPosition.y)];
} else if (monkeyPosition.y > 0 && monkeyPosition.x < 0) {
[myWorld setPosition:CGPointMake(0, -monkeyPosition.y)];
}

Callout from MKPolylineView?

Is there any way to show a callout when a person touch a MKPolylineView?
i tried to add a UITapGestureRecognizer and in selector display a callout in some coordinate. but didnt work. any suggestions for making this?
the following is what i tried in method
- (MKOverlayView *)mapView:(MKMapView *)mapa viewForOverlay:(id <MKOverlay>)overlay
self.polylineView = [[MKPolylineView alloc] initWithPolyline: self.polyline];
self.polylineView.strokeColor = [UIColor blackColor];
self.polylineView.lineWidth = 5.0;
self.polylineView.alpha = 0.7;
UITapGestureRecognizer *touchOnView = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(addBubble:)];
[touchOnView setNumberOfTapsRequired:1];
[touchOnView setNumberOfTouchesRequired:1];
[self.polylineView addGestureRecognizer:touchOnView];
Very interesting question - I'd never even thought of putting a gesture recognizer on a map overlay. With a bit of experimentation, I've verified it is possible to detect a tap on the MKPolylineView. Just as you found, the tap gestures don't work on map overlays. So instead I put the tap gesture on the MKMapView rather than on the MKPolylineView. Then to handle the tap:
- (void)handleTapGesture:(UIGestureRecognizer*)gestureRecognizer
{
if (measureLine != nil)
{
UIView* hitView = [self.polylineView hitTest:[gestureRecognizer locationInView:self.polylineView] withEvent:nil];
}
}
hitView will be nil if your tap was outside of the MKPolylineView, or it will be self.polylineView if the tap was inside.
However, this might not behave quite as you'd like. For horizontal and vertical lines, it works perfectly because the size of the underlying view is roughly the same size as the line. But for a 45 degree line, the underlying view has to be much larger than than the line, because it is an axis aligned bounding box (AABB). If you think about a 45 degree line, to enclose it using only horizontal and vertical lines you will end up with a large area - much larger than you'd want to detect taps in.
e.g.
--------
| / |
| / |
| / |
| / |
|/ |
--------
But using a tap gesture or the hit test will always result in recognizing taps inside these AABBs. So regardless of where you try to attach your gesture - e.g. to the MKPolylineView as you tried, or to the MKMapView, you'll get spurious results. The problem gets worse for longer lines - if you imagine a line going from the top right of your map view to the bottom left, the AABB that you'd need to enclose it would cover the entire area of the map view, meaning that a tap in the top left or bottom right would be interpreted as hitting the MKPolylineView.
To solve the problem, I'd suggest the following approach:
Use a tap gesture recognizer on the map view
In your method that handles the tap:
convert the screen tap position to a map coordinate
loop through each of your polylines (unless you have only 1)
for each point in the polyline take the line segment that connects that point to the next point, and calculate the distance of your map coordinate away from this line. Use trigonometry to calculate this.
if the distance is very close to this segment, then stop checking the rest of the segments and handle whatever callouts etc you wanted to show
if the distance is not close, then move onto the next point and the line segment that connects it to the next but one point
This approach is guaranteed to work regardless of the length of your polylines, or what angle they are at. There are no AABB concerns. The downside is that all of those distance calculations might be computationally expensive - so if your polylines are made up of a great number of points, or if you have a large number of polylines, then you might not be able to do all of those calculations without blocking the UI from being responsive, meaning you'd need to move it into a background thread. If you only have a handful of polylines, and/or they are made up of few points, then you'll be fine.

Resources