Postponing gesture recognizer in swift (UISwipeGestureRecognizer) - swift

I have a swipe gesture recognizer that I turn off after the user swipes. That’s when I have a series of animations. 1st, a view will move in the direction that the user swipes. Then based on the location that it stops, another animation occurs based on the location that it stopped in. I then turn on the gesture recognizer all the way at the end of the function that handles this swipe. The problem is that the recognizer is turning on too quickly and therefore if the user were to quickly swipe in another direction, the animation would only occur in the wrong place (due to the fact that the view is in a different place). My question is, how can I create a function that waits X amount of time and then turns back on the recognizer?

// 1
var toDoSmth: (()->void)? = nil
// 2
toDoSmth = {
// turns back on the recognizer
// and do any delayed task
}
// 3
let delayedTime = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: delayedTime) {
toDoSmth()
}
Here: 1, 2 - created variable/autoclosure;
3 - run your code after 1 second.

Related

SwiftUI: Sequence gestures starting with tap

I want to create the "one finger zoom gesture" from the iOS Maps App. So a tap (touch down, then touch up), then long press (only touch down), and finally a drag up/down.
I was expecting something like this:
let tapGesture = TapGesture().onEnded { _ in
print("tap")
}
let longPressGesture = LongPressGesture(minimumDuration: 0.00001)
.onChanged { _ in
print("long press update")
}
.onEnded { _ in
print("long press")
}
let dragGesture = DragGesture(minimumDistance: 1)
.onChanged { gesture in
print("drag")
}
let combined = tapGesture.sequenced(before: longPressGesture).sequenced(before: dragGesture)
SomeView()
.gesture(combined)
Only the LongPressGesture after the initial tap never gets called. I've tried a LongPressGesture instead of the initial TapGesture, but two sequential long presses are called instantly (so the "touch up" event is not regarded).
An initial DragGesture(minimumDuration: 0) with onEnded and a corresponding #State to track the initial tap might work, but I have a ScrollView beneath (for the map-like panning and zooming), so I'm guessing a DragGesture cannot be the first one.
I also tried setting a gestureEnabled flag after a first tap via onTapGesture() and then using the .gesture(combined, including: gestureEnabled ? .all : .none), with a few more edge cases handled I managed to get it working, only to find out that double taps no longer worked anywhere else on the View.
I should clarify that there's a ScrollView with text underneath the view, so long press, double tap, scrolling (horizontal and vertical) should all keep working.
Any ideas?
Currently, my best alternative is just using the long press + drag. It works with the additional tap as well (since it's not necessary in that case). Only downside is it also triggers if the user holds down for a short time before attempting a scroll.

Why is touchesCancelled being called after UITapGestureRecognizer?

After finally finishing my coding within touchesBegan, touchesMoved, touchesEnded, touchesCancelled I started adding code for recognizing taps. I implemented it with the code below.
For some reason after every UITapGestureRecognizer, I get a touchesCancelled. Did I leave something out, is there a threshold I can modify, or do I somehow have to create my own code that says, I just did a tap, ignore the first cancel that comes along?
So essentially I get a touchesBegan, UITapGestureRecognizer, followed by a touchesCancelled.
class GameScene: SKScene, SKPhysicsContactDelegate{
let tapRec = UITapGestureRecognizer()
....
tapRec.addTarget(self, action:#selector(GameScene.tappedView(_:) ))
tapRec.numberOfTouchesRequired = 1
tapRec.numberOfTapsRequired = 1
self.view!.addGestureRecognizer(tapRec)
....
}
#objc func tappedView(_ sender:UITapGestureRecognizer) {
....
}
That's just what a gesture recognizer is / does. When a gesture recognizer recognizes its gesture, the touches for this gesture recognizer are sent to their hit-test views as a touchesCancelled(_:with:) message, and then no longer arrive at those views. The gesture recognizer takes over so it signs the view off from its touch sequence in good order.
If you're going to have both a gesture recognizer and manual touch events, you have to do more work to mediate between them. If you really don't want the standard behavior, you can set the gesture recognizer's cancelsTouchesInView property to false.

How would I set up a swipe gesture that moves the keyboard curser?

I am currently making an iOS keyboard extension. Right now I am trying to set it up so that when I swipe on the space bar the cursor moves but I am having some trouble. Ideally when swiping to the left the cursor will go to the left and the opposite for right. When swiping the cursor speed should be proportional to the swipe speed(Fast swipe means fast cursor movement).
I know what the code to move the cursor is:
adjustTextPositionByCharacterOffset(x)
Where when x is positive it move forward x characters and when its negative it moves backwards x characters.
I have tried to use pan gesture and long hold gesture recognizers but I can not get it to give the right behavior.
I was able to solve this problem by using a pan gesture recognizer and comparing it to the last x value the pan gesture returned.
#IBAction func spacePanGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(self.view) //Get translation
if Int(translation.x) > swipeLastTouchX {
//If the new translation is higher than the last, move the cursor
proxy.adjustTextPositionByCharacterOffset(user.swipeSensitivity.rawValue)
} else if Int(translation.x) <= swipeLastTouchX {
proxy.adjustTextPositionByCharacterOffset(-user.swipeSensitivity.rawValue)
}
swipeLastTouchX = Int(translation.x)
}

Swift: Long press cancelled by movie playing. How do I persist the long press?

I'm building some functionality similar to SnapChat. Press and hold on a view, it brings up a movie to play, then returns to the main view controller when the movie finishes (currently working), or when the user picks up their finger (not working. That's what this question is about).
My problem lies in the IBAction, when the video comes up, the UIGestureRecognizerState changes from Began to Cancelled.
I'd like for the state to not change until the user lifts their finger, which should register UIGestureRecognizerState as Ended
Any tips on how to do that would be awesome. Thanks!
class ViewController: UIViewController {
var moviePlayer: MPMoviePlayerViewController!
#IBAction func handleLongPress(recognizer: UILongPressGestureRecognizer) {
println("\(recognizer)")
if recognizer.state == UIGestureRecognizerState.Began {
playVideo()
}
if recognizer.state == UIGestureRecognizerState.Ended {
self.moviePlayer.moviePlayer.stop()
}
}
func videoHasFinishedPlaying(notification: NSNotification){
println("Video finished playing")
}
func playVideo() {
// get path and url of movie
let path = NSBundle.mainBundle().pathForResource("IMG_8602", ofType:"MOV")
let url = NSURL.fileURLWithPath(path!)
moviePlayer = MPMoviePlayerViewController(contentURL: url)
presentMoviePlayerViewControllerAnimated(moviePlayer)
// construct the views
moviePlayer.view.frame = self.view.bounds
self.view.addSubview(moviePlayer.view)
// remove controls at top and bottom of video
moviePlayer.moviePlayer.controlStyle = MPMovieControlStyle.None
NSNotificationCenter.defaultCenter().addObserver(self, selector: "videoHasFinishedPlaying:",
name: MPMoviePlayerPlaybackDidFinishNotification, object: nil)
}
}
Edit: One possible solution
So this was a total fluke, but I figured out how to do this in this context.
I simply removed the line presentMoviePlayerViewControllerAnimated(moviePlayer) from my playVideo function.
Not entirely sure why that worked but it worked for my context.
This is ONE possibility but I'm open to better suggestions.
Add a 'master view' on top of where you need the touch event
Add the gesture recognizer to that view
Handle the logic within the gesture as you are now, but the subview that needs to be added needs to be added below the view thats receiving the touch.
You can now get the .ended state when the user lifts their finger.
Word of caution. If your view has other touch events make sure this 'master view' is not over top of them because it will intercept those events and hog it for its gesture (unless you set it up to recognize them simultaneously). If thats the case, you can make this 'master view' only a small port of the screen where you need to listen for the touch event without interruption

Using UIPageViewControllers scroll over entire view, including UIButtons

I have a UIView with a PageViewController and 4 UIButtons as subviews. The PageViewController and the button subviews are independent from each other. (PageViewController scrolls across 4 pages while the UIButtons remain constant)
The problem I'm facing is that the buttons take quite a bit of space and I'd like to use this scroll gesture anywhere on the UIView including the areas with buttons.
If the user taps on the button, its respective action would occur but I'd like if I can START a scroll/swipe gesture on a button and have the PageViewControllers scroll feature work.
I've tried/searched several ways including using UIScrollView instead of a PageViewController. Any help would be greatly appreciated. Thanks in advance. :)
Just add your own gesture recogniser and watch for gestures which velocity is greater in the x direction and going from left to right.
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer.isMemberOfClass(UIPanGestureRecognizer)){
let point:CGPoint = (gestureRecognizer as UIPanGestureRecognizer).velocityInView(self)
if (abs(point.x) > abs(point.y)){
return true
}
}
}
Handle the gesture over here:
func handlePanGestureRecognizer(gesture: UIPanGestureRecognizer){
let translation:CGPoint = gesture.translationInView(self)
if(translation.x > 0){
//its going from left to right...
//...transition to the previous page
}else{
//...transition to the next page
}