Is there a way to make a UITextField move when user drags across screen? - ios

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.

Related

Pan view using UIPanGestureRecognizer within a functional UIScrollView

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.

Gesture recognizer callback over multiple views

I have a custom UIView that consists of 9 equally-sized subviews. It essentially looks like a tic-tac-toe board.
I've added a UIPanGestureRecognizer to this custom view. Each time the user pans over one of the subviews, I want to take an action. For example, the user could pan over the first 3 (of the 9) subviews in one gesture, and in this case I'd want to take 3 actions.
I could try to do some fancy math and figure out the frame of each subview, then figure out when the gesture crosses from one subview to another. However, I feel like there should be a more elegant way to get a callback when a gesture touches a new UIView. Does a functionality like this exist?
I was able to find a more elegant way by using hitTest, which returns the lowest subview with user interaction enabled. I defined the callback for the pan gesture recognizer as such:
var panSelected = Set<UILabel>()
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
let view = recognizer.view
let loc = recognizer.location(in: view)
if let gridLabel = view?.hitTest(loc, with: nil) as? UILabel {
if !panSelected.contains(gridLabel) {
// my code here
}
}
try to use stack view to group views have same gesture, & using
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(stackViewTapped))
myStackView.addGestureRecognizer(tapGesture)
if there's different method needs to be implemented, just add another stack view.

Long press gesture recognizer issue

In Interface Builder, I've added a press gesture recognizer to a MKMapView.
An event is sent after 1 second (I am using it to add a pin to the map). I have checked the "Cancel touches in view" behavior of my gesture recognizer, but my issue is that once the long press gesture is recognized, if you keep your finger on the screen and drag it on the map view, the event (for long press) will be sent continuously while dragging, as if it was actually a drag gesture recognizer, resulting in dozens of pins being added to my map...
How can I fix this ?
Thank you.
According to the documentation:
Long-press gestures are continuous. The gesture begins
(UIGestureRecognizerStateBegan) when the number of allowable fingers
(numberOfTouchesRequired) have been pressed for the specified period
(minimumPressDuration) and the touches do not move beyond the
allowable range of movement (allowableMovement). The gesture
recognizer transitions to the Change state whenever a finger moves,
and it ends (UIGestureRecognizerStateEnded) when any of the fingers
are lifted.
With the important point highlighted.
I believe you may not be filtering the state in your gesture recognizer's delegate method.
You will need something like this:-
- (void)longPressGestureRecognizerStateChanged:(UIGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
// do your stuff...
}
}

UISwipeGestureRecognizer interferes with slider

I have a view in an iOS application (Obj-C) which has an image view in the centre, and immediately below that a slider.
The image view shows album artwork, and the slider can be used to adjust the now-playing track position.
There is also a pair of left and right Swipe Gesture Recognizers. These are used to skip to the next or previous tracks.
The problem is that the swipe gesture recognizers seem to over-ride the users moving the slider thumb.
In my gesture recognizer code I check that the point touched was inside the image view, but it still stops the slider from being moved. (The thumb moves, but jumps back to it's original position when you remove your finger).
This is the code I use to reject the gesture if it's not inside the image view.
- (IBAction)swipeLeftGestureAction:(UISwipeGestureRecognizer *)sender {
// Get the location of the gesture.
CGPoint tapPoint = [sender locationInView:_artworkImageView];
// Make sure tap was INSIDE the artwork image frame.
if( (tapPoint.x <0)
|| (tapPoint.y < 0 )
|| (tapPoint.x > _artworkImageView.frame.size.width)
|| (tapPoint.y>_artworkImageView.frame.size.height))
{
NSLog(#"Outside!");
return;
}
NSLog(#"Swipe LEFT");
[_mediaController skipNext];
}
So my question is, how do I limit the gesture to work ONLY when swiped across the image view?
Try to put the code that restricts the gesture's area in gestureRecognizer:shouldReceiveTouch: and return NO in case you don't want the gesture to receive this touch. It should prevent the gesture from over taking the slider interaction.
If you're only interested in swipes that are inside the image view, then you should add the swipe gesture recognizer to the image view instead of adding it to your entire view. Then you won't need any special logic.
There is a dedicated method for checking if a point is inside a view
BOOL isPointInsideView = [_artworkImageViewpointInside:tapPoint withEvent:nil];
But I think what is happening is that if you will look at the tapPoint is that it actually outside of your imageView
And if your slider is really close to the imageView so the slider captures the movement, what you should do is check on the slider if it is intended for the slider and propagate on to the imageView if needed
Or inherit the UISlider and reduce its response rect

Detecting UIView grid squares where a UIPanGestureRecognizer begins and ends

I'm designing a game that takes place on a grid. The grid needs to respond to pan gestures that begin in one square and end in another. I have a custom view controller class called GameVC which contains the grid of UIViews of subclass GameGridSquare. I want the game to perform an action when, for example, a pan gesture begins and ends in neighboring squares. I have a storyboard wired with properties that name each square by row and column: self.A1, self.A2, ... self.H7, self.H8. As a pan gesture is recognized, I want the GameVC to receive the two GameGridSquares where UIGestureRecognizerStateBegan and UIGestureRecognizerStateEnded so it can determine the appropriate action, like so:
-(void)validatePanGestureFrom:(GameGridSquare*)beginSquare
to:(GameGridSquare*)endSquare
What's the best way to do this? My recognizers are functional and returning coordinates, but I think I need to explore hit-testing.
If I add recognizers to the individual GameGridSquares, I get undesired results. For example, a pan that begins in A1 and ends in B2 would be recognized by A1 alone. This suggests that I need a pan recognizer on GameVC that can detect when the gesture begins and ends in separate subviews.
From what I've read, I believe that the UIGestureRecognizerDelegate protocol may be helpful here. I also understand that a custom UIPanGestureRecognizer subclass would allow me to override hitTest:withEvent but I'm not sure where to even begin with that. Any ideas about how I should approach this?
This was what I was after:
Find which child view was tapped when using UITapGestureRecognizer
UIView* view = gestureRecognizer.view;
CGPoint loc = [gestureRecognizer locationInView:view];
UIView* subview = [view hitTest:loc withEvent:nil];

Resources