UIPanGestureRecognizer fails to enter .ended state after screen rotation - ios

I'm having an issue with handling screen rotations when a UIPanGestureRecognizer is in the .changed state. My handling logic looks something like:
#objc fileprivate func handlePanGesture() {
let state = self.panGestureRecognizer.state
if state == .began {
// Log beginning state
} else if state == .changed {
// Track position, update constraints
} else if state == .ended {
// Reset and prepare for new gesture
}
}
Everything seems to be working well, but when the device is rotated to a new orientation (without lifting my finger), the gesture recognizer stops receiving updates for the active touch, but never triggers the handlePanGesture() with the .ended state set.
Right now I'm handling this by looking out for viewWillTransition(to:with:) and cleaning up the state when that happens, but this approach fails when, e.g., an iPad is rotated from portrait-right-side-up to portrait-upside-down--there is never any size transition. Is there something that can be done to cancel the gesture recognizer on rotation (and trigger a call to handlePanGesture())?

You also need to check for the .cancelled state.
btw, switch on the state rather than if, else if, else if, else if

Related

iOS 14 UISlider ignoring additional GestureRecognizer (PanGesture, LongTapGesture, TapGesture)

I have a view controller in which a user can move around UIButton, UISlider, and a custom UIView based control by panning the control around the screen. This view controller is used to create custom layout of control by the user. This is all done by adding PanGestureRecognizer to the UIControl to move the position of the control relative to user's finger location.
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
panRecognizer.delegate = self;
myslider.addGestureRecognizer(panRecognizer)
~~~~~~~~~~~~
//pan handler method
#objc func pan(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: view)
guard let gestureView = gestureRecognizer.view else {
return
}
//Move the center to user's finger location
gestureView.center = CGPoint(x: gestureView.center.x + translation.x, y: gestureView.center.y + translation.y);
gestureRecognizer.setTranslation(.zero, in: view);
if (gestureRecognizer.state == .ended) {
//Save the new location inside data. Not relevant here.
}
}
This worked fine in iOS 13 and below for all the control i mentioned above (with the UISlider being a bit glitchy but it still responded to the pan gesture and i don't need the value of the uislider anyway so it's safe to ignore). However testing the app in iOS 14 reveals that UISlider completely ignore the PanGesture (proven by adding breakpoint that never got called inside the pan gesture handling method).
I have looked at apple's documentation regarding UISlider and found no change at all related to gesture handling so this must be done deliberately in deeper lever. My question is: is there any way to "force" my custom gesture to be executed instead of UISlider's gesture without the need to create transparent button overlay (which i don't know will work or not) / creating dummy slider just for this ?
Additionally i also added UILongPressGestureRecognizer and UITapGestureRecognizer to the control. Which worked fine on other UIButton but completely ignored by the UISlider in iOS14 (in iOS 13 everything worked fine).
Thanks.
Okay.. I found the answer myself after some more digging and getting cue from "claude31" about making a new clean project and testing from there.
The problem was with the overriden beginTracking function. This UISlider of mine is actually subclassed into a custom class and in there the beginTracking function is overriden, as per code below:
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let percent = Float(touch.location(in: self).x / bounds.size.width)
let delta = percent * (maximumValue - minimumValue)
let newValue = minimumValue + delta
self.setValue(newValue, animated: false)
super.sendActions(for: UIControl.Event.valueChanged)
return true
}
This is to make the slider move immediately to the user finger location if the touch is inside the boundaries of the UISlider (without the need to first touch the thumbTrack and sliding it to the position the user wants).
In iOS 13 this function does not block the gestureRecognizer from getting recognized. However in iOS 14B4, overriding this function, with sendActions(for:) method inside it cause the added gestureRecognizer to be ignored completely, be it pan gesture, long press gesture, or even tap gesture.
For me, the solution is to simply add a state to check whether the pan gesture is required in this view controller or not. Because, luckily, i only need the added gestureRecognizer in view controller that does not require the beginTouch function to be customized and vice versa.
Edit:
Originally i wrote that the cause of the problem is due to always true return value, however, I just reread the documentation and the default return value for this function is also true. So i think the root causes of this problem is the sendActions(for:) method, causing the added gestureRecognizer to be ignored. My Answer above has been edited to reflect this.

How do I implement framework function touchDown in a turnbased loop?

I'm using a simple turn based loop, switching between player turn and enemy turn. In the player turn the loop should "pause" until framework function touchDown has been activated, then pass the turn to the enemy. I'm not sure how to implement this as touchDown triggers automatically whenever you press the screen.
Am I to use an observer of some sort or a #selector? Tried both unsuccessfully, but that may just be for my lack of experience. If I use the selector the loop doesn't pause to wait for the screen to be clicked. As for the observer, I'm just not sure how its implemented. I also tried just adding the whole touchDown method into the player turn but that obviously didn't work.
The loop simply looks like this:
var state = "PLAYER"
func runGame() {
while(gameIsProgressing) {
if(state == "PLAYER") {
print("Player is doing stuff")
//THE touchDown METHOD SOULD BE TRIGGERD HERE
state = "ENEMY"
}else if(state == "ENEMY") {
print("\n🙈ENEMY IS DOING STUFF🙈")
state = "PLAYER"
}
}
}
Also, it is important that touchDown is triggerd only in the player turn, and not whenever the screen is pressed.
Any ideas?
Since you are programming something with a GUI here, you should not think in this imperative way. There is no (good) way to "pause" a while loop "until" something happens. You should think in a more event-driven way.
Instead of thinking about the while loop, think in terms of the events that happen. In this case, the event is touchDown. Note that I don't know of a library function called touchedDown (did you mean touchesBegan or is this an action for a UIControl?), but I'll assume it exists. When the player touches the screen, what do you want to happen? Well, probably something like this:
guard gameIsProgressing else { return }
if(state == "PLAYER") {
someFunctionThatShowsWhatThePlayerDid()
state = "ENEMY"
print("\n🙈ENEMY IS DOING STUFF🙈")
someFunctionThatMakesTheEnemyDoStuff(completion: {
self.state = "PLAYER"
})
}

Track UIPanGestureRecognizer position after first touch condition

I would like to get get the location of first touch when users starts panning from the bottom of the screen and only then read position and use it.
I know that I can check .begin state but don't know how to keep track of the pan if this condition is true. It seemed like very easy thing to do but I didn't manage to do it.
#objc private func handleSwipe(swipe: UIPanGestureRecognizer) {
if swipe.state == .began && swipe.location(in: self.view).y < self.view.frame.height * 0.15 {
// do something with swipe.translation(in: self.view).y
}
}
Just store the coordinates in a variable, and retrieve it when you handle the .changed state. All gesture recogniser callbacks are on the main thread, so you shouldn't have issues with maintaining your own state between these callbacks.

Swift: How can I wait for an event and then switch the view?

Hey I'm doing a mini game application atm, which switches from one minigame view to the next one. Is there some kind of event listener in swift? If a game (SpriteKit Game) is finished it should fire an event, that its done now, and the view controller should then know, that it should switch to the next game view. I tried around with segues, but it doesn't seem to work like it should.
I know a way, but it's not a very 'clean' way. You could add an if statement to your update function to check for a condition. For example a gameOver boolean that becomes true when a certain condition is met.
If it's true you can move to another scene.
var gameOver = false
override func update(currentTime: CFTimeInterval) {
if gameOver {
goToGameOverView()
}
}
func goToGameOverView() {
// Your code here
}
Maybe there's a better way, but this is the way that I know.

While-loop in UILongPressGestureRecognizer not working

I'm trying to adjust the position of my textField as I long-press my view, but for some reason the while-loop never stops running. My code looks like this:
func buttonLongPressed(gestureRecognizer:UIGestureRecognizer){
if textEdit.editing == true{
self.textEdit.endEditing(true)
}
while gestureRecognizer.state == UIGestureRecognizerState.Began{
println("BEGAN")
self.textEdit.frame = CGRectMake(0, gestureRecognizer.locationInView(self.view).y, self.view.frame.width, 44)
}
}
I don't understand why this shouldn't work, and how to do it in any other way.
Any suggestions would be appreciated.
The gesture recognizer calls your action method when its state changes--it's not valid to poll the state from a while loop, it will never change.
It probably works like this:
Events are sent from the touch screen with a signal to wake up your app.
The gesture recognizer looks at the queued events and decides to enter "Began" state.
The gesture recognizer code invokes your action method (buttonLongPressed)
Your code enters a while look and reads gestureRecognizer.state repeatedly.
You can see, if your action method never returns, the gesture recognizer will never wake up again and look at new input.
You can probably just change your function like this:
func longPressAction( g:UILongPressGestureRecognizer )
{
switch g.state
{
case: .Changed
{
// handle one drag update... but don't loop
}
}
}

Resources