My pan gesture programmatically fakes a button press. If you press the actual button, the VC's viewDidLoad runs again once, stops, and is fine. But when I use the pan-gesture to programmatically fake the button press, the viewDidLoad runs about seven times, then stops.
The right-edge gesture triggers the "tomorrow" state which loads a handful of times. At that point, a left-edge gesture triggers the "today" state. That also loads 4-5 times.
Any ideas? Some code below, if needed.
Pan gesture added in viewDidLoad:
//Adds edge gesture depending on VC state
if curMainList == "Today" {
let panLeftEdgeGesture_MainVC = UIScreenEdgePanGestureRecognizer(target: self, action: "panLeftEdge_toLifeList:")
panLeftEdgeGesture_MainVC.edges = .Left
view.addGestureRecognizer(panLeftEdgeGesture_MainVC)
let panRightEdgeGesture_MainVC = UIScreenEdgePanGestureRecognizer(target: self, action: "panFromRightEdgeAction:")
panRightEdgeGesture_MainVC.edges = .Right
view.addGestureRecognizer(panRightEdgeGesture_MainVC)
self.footer_LeftBtn.addTarget(self, action: "LeftFooterBtnPress", forControlEvents: UIControlEvents.TouchUpInside)
}
else {
let panLeftEdgeGesture = UIScreenEdgePanGestureRecognizer(target: self, action: "panFromLeftEdgeAction:")
panLeftEdgeGesture.edges = .Left
view.addGestureRecognizer(panLeftEdgeGesture)
}
Gesture's triggered:
//Gestures visible when view is 'Today'
func panLeftEdge_toLifeList(sender: UIScreenEdgePanGestureRecognizer) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.performSegueWithIdentifier("segueToLifeLists", sender: nil)
}
}
func panFromRightEdgeAction(sender: UIScreenEdgePanGestureRecognizer) {
//self.tomHdrBtnPress(tomorrowHdr)
tomorrowHdr.sendActionsForControlEvents(UIControlEvents.TouchUpInside)
}
//Gesture added when viewing 'Tomorrow' list
func panFromLeftEdgeAction(sender: UIScreenEdgePanGestureRecognizer) {
//self.todayHdrBtnPress(todayHdr)
todayHdr.sendActionsForControlEvents(UIControlEvents.TouchUpInside)
}
#IBAction func todayHdrBtnPress(sender: AnyObject) {
UIView.animateWithDuration(0.75, animations: {
//Header changes
self.hdrBox.backgroundColor = UIColor(red: 46/255.0, green: 173/255.0, blue: 11/255.0, alpha: 1.0)
self.footer_RightArrow.hidden = false
self.footer_RightBtn.hidden = false
}
, completion: nil
)
curMainList = "TodayTask"
currentListEntity = curMainList
viewDidLoad()
}
#IBAction func tomHdrBtnPress(sender: AnyObject) {
UIView.animateWithDuration(0.75, animations: {
//Header changes
self.hdrBox.backgroundColor = UIColor(red: 49/255.0, green: 82/255.0, blue: 171/255.0, alpha: 1.0)
self.footer_LeftBtn.addTarget(self, action: "LeftFooterBtnPress", forControlEvents: UIControlEvents.TouchUpInside)
}
, completion: nil
)
curMainList = "TomTask"
currentListEntity = curMainList
viewDidLoad()
}
The problem is that pan gestures are continuous gestures (meaning that as you move your finger, the handler is called repeatedly). For a pan gesture, it's called once with a gesture.state of .Began, repeated with a state of .Changed as the user's finger moves, and the finally with a state of .Ended or .Cancelled when the user stops the gesture.
So, you generally would not trigger an animateWithDuration from a continuous gesture. You just update the state based upon, for example, gesture.translationInView() or what have you. Just update the views immediately to reflect the updated pan and it's called so frequently, that it ends up rendering something that feels like an animation, but it's really just a sequence of updates linked to continuous flow of updates your gesture recognizer's handler receives as the user progresses in their gesture.
The only time you would animate within a continuous gesture is upon .Ended or .Cancelled, to complete the animation. For example, maybe the user drags their finger half way across, so on .Changed you just update the view immediately without any animation, but when they lift their finger (i.e. the gesture handler receive .Ended), you might animate completion to the view's final state.
Bottom line, the handling in gesture recognizers is a very different mechanism for animation that might result from a button (in which you call the animateWithDuration methods once to initiate an animation that UIKit then takes care of for you). It's best not to conflate these two very different ways of updating/animating the UI.
You also are performing a segue in one of your pan handlers. If you want to have a segue performed interactively as you pan, you should refer to WWDC 2013 video Custom Transitions Using View Controllers, which shows how you can combine an animation controller (which dictates the nature of the animation) with an interaction controller (which links the progress of the gesture with the animation controller). But in this pattern, you perform the segue once and only once, and merely update the UIPercentDrivenInteractiveTransition in the gesture, which will link the progress of the transition with the user's finger.
`
Related
I am trying to understand a reproducible bug with my gesture recognisers. I have 2 recognisers on an MKMapView, one UITapGestureRecognizer and one UILongPressGestureRecogniser. Both of them work as expected the first time, however, if I use the long press (which adds an annotation to the map) the next tap gesture will return in the 'possible' state but never hit the 'recognized' state.
▿ Optional<Array<UIGestureRecognizer>>
▿ some : 2 elements
- 0 : <UITapGestureRecognizer: 0x7fda7543ebc0; state = Possible; view = <MKMapView 0x7fda78026e00>>
- 1 : <UILongPressGestureRecognizer: 0x7fda7543e8c0; state = Possible; view = <MKMapView 0x7fda78026e00>; numberOfTapsRequired = 0; minimumPressDuration = 0.2>
After I tap once, and nothing happens, a second tap will then perform the associated function i.e. make it to the recognized state.
I am intercepting all the clicks on the window and the tap definitely takes place each time but the first one after a long press never seems to become accepted. Is there something I'm missing here? The gestures are added as below:
let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
mapView.addGestureRecognizer(mapTap)
let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(mapLongPress(_:)))
pressGesture.minimumPressDuration = 0.2
pressGesture.numberOfTouchesRequired = 1
mapView.addGestureRecognizer(pressGesture)
Could this be to do with the other gestures which are added by default on an MKMapView?
I tried using your code and got the same result.
I solved it with a tricky solution. I hope it would be helpful for you
mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
mapTap.delegate = self
mapView.addGestureRecognizer(mapTap)
pressGesture = UILongPressGestureRecognizer(target: self, action:
#selector(mapLongPress(_:)))
pressGesture.minimumPressDuration = 0.2
pressGesture.numberOfTouchesRequired = 1
mapView.addGestureRecognizer(pressGesture)
#objc func mapTapped(_ gesture: UITapGestureRecognizer) {
// your code
}
#objc func mapLongPress(_ gesture: UILongPressGestureRecognizer) {
// your code
if gesture.state == .began {
mapTap.isEnabled = false
} else if gesture.state == .cancelled || gesture.state == .ended {
mapTap.isEnabled = true
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
In your case you expect that the tap recognizer as well as the long press recognizer operate simultaneously: When you tap the view, both should start the recognition process. When you end the tap before the minimum tap time for the long press, the tap gesture should fire, but when you end the tap later, the long press gesture should fire.
But the Apple docs say:
UIKit normally allows the recognition of only one gesture at a time on
a single view. Recognizing only one gesture at a time is usually
preferable because it prevents user input from triggering more than
one action at a time. However, this default behavior can introduce
unintended side effects. For example, in a view that contains both pan
and swipe gesture recognizers, swipes are never recognized. Because
the pan gesture recognizer is continuous, it always recognizes its
gesture before the swipe gesture recognizer, which is discrete.
In your case, the long tap gesture recognizer is continuous while the tap gesture recognizer is discrete, so there could be a problem in recognizing the tap.
I would thus try to explicitly allow both recognizers to simultaneous recognice their gestures. An example how to do this is given here.
As soon as the long press recognizer fires, you could cancel the recognition operation of the tap recognizer.
Hope this helps!
I made a simple project, with swipe gesture recogniser and animation. I made my label to move and every 3 second increase number. With every swipe I need to decrease the number. My gesture recogniser object is tied with label, i.e. it works only in label bounds. When prog is working without animation everything is ok, but when it;s animated an is moving my gesture recogniser is doing nothing. How to make a gesture recogniser work at the same time as animation, i.e. while animated to respond to my swipes. Need help.
`
#IBOutlet weak var label1: UILabel!
var number : Int = 0
var timer = Timer()
#IBAction func label1SwipeRight(_ sender: UISwipeGestureRecognizer) {
number += 1
label1.text = String(number)
}
func animate1() {
UIView.animate(withDuration: 4.0, delay: 0.0, options: .allowUserInteraction, animations: {
let num1 : CGFloat = CGFloat(arc4random_uniform(667))
let num2 : CGFloat = CGFloat(arc4random_uniform(375))
self.label1.frame.origin.y = num1
self.label1.frame.origin.x = num2
}, completion: {(bool) in
self.animate1()
print("Animation1 completed")
})
}
func timerExample() {
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
label1.text = String(Int(label1.text!)! + 1)
}`
By default view objects block user interaction while an animation is "in flight". You need to use one of the "long form" animation methods, and pass in the option .allowUserInteraction. Something like this:
UIView.animate(duration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})
Note, however, that if what you're animating is a view's position, the user won't be able to tap on the view object as it moves. That's because a position animation does not really animate the object from one place to another over time. It just creates that appearance. Behind the scenes, the object actually jumps to it's final position the moment the animation begins.
If you need to be able to tap/drag/swipe on objects while they're moving you will have to do that yourself. What you do is put a gesture recognizer on the parent view that encloses the entire range of motion (possibly the whole screen.) Then you need to use the presentation layer of your animating view's layer, translate the coordinates of the point from the gesture recognizer's coordinate space to the layer's coordinate space, and use the layer's hitTest method to figure out if the point is on the layer or not.
I have a project on Github called iOS-CAAnimation-group-demo that does something like that (It animates an image view along a complex path and you can tap on the image view to pause the animation while it's "in flight".
It's from several years ago, so it's written in Objective-C, but it should help to at least illustrate the technique.
I made a simple project, with swipe gesture recogniser and animation. I made my label to move and every 3 second increase number. With every swipe I need to decrease the number. My gesture recogniser object is tied with label, i.e. it works only in label bounds. When prog is working without animation everything is ok, but when it;s animated an is moving my gesture recogniser is doing nothing. How to make a gesture recogniser work at the same time as animation, i.e. while animated to respond to my swipes. Need help.
`
#IBOutlet weak var label1: UILabel!
var number : Int = 0
var timer = Timer()
#IBAction func label1SwipeRight(_ sender: UISwipeGestureRecognizer) {
number += 1
label1.text = String(number)
}
func animate1() {
UIView.animate(withDuration: 4.0, delay: 0.0, options: .allowUserInteraction, animations: {
let num1 : CGFloat = CGFloat(arc4random_uniform(667))
let num2 : CGFloat = CGFloat(arc4random_uniform(375))
self.label1.frame.origin.y = num1
self.label1.frame.origin.x = num2
}, completion: {(bool) in
self.animate1()
print("Animation1 completed")
})
}
func timerExample() {
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
label1.text = String(Int(label1.text!)! + 1)
}`
By default view objects block user interaction while an animation is "in flight". You need to use one of the "long form" animation methods, and pass in the option .allowUserInteraction. Something like this:
UIView.animate(duration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})
Note, however, that if what you're animating is a view's position, the user won't be able to tap on the view object as it moves. That's because a position animation does not really animate the object from one place to another over time. It just creates that appearance. Behind the scenes, the object actually jumps to it's final position the moment the animation begins.
If you need to be able to tap/drag/swipe on objects while they're moving you will have to do that yourself. What you do is put a gesture recognizer on the parent view that encloses the entire range of motion (possibly the whole screen.) Then you need to use the presentation layer of your animating view's layer, translate the coordinates of the point from the gesture recognizer's coordinate space to the layer's coordinate space, and use the layer's hitTest method to figure out if the point is on the layer or not.
I have a project on Github called iOS-CAAnimation-group-demo that does something like that (It animates an image view along a complex path and you can tap on the image view to pause the animation while it's "in flight".
It's from several years ago, so it's written in Objective-C, but it should help to at least illustrate the technique.
I'm getting a lot of delay in an app that needs to feel more instant.
I've got a simple app that toggles left and right. It's a seesaw and when one end is up, the other is down. Your supposed to use two fingers and tap on the screen like your just fidgeting with it. It supposed to be an aid to ADHD.
I've got two large images for left and right state. I've got a gesture recognized on the image and I check the coordinates of the tap to determine if you tapped the right side to go down or the left. I'm also using AudioServicesPlayAlertSound to cause a small pop vibrate on touch begin in an effort to give a bit of a feedback stimulus to the user.
In my tests, if I tap rapidly it seems I get a backlog of taps on the toggle. The vibrations happen way after the tap is over, so it feels useless. Sometimes the UI image gets backlogged just switching between images.
override func viewDidLoad() {
super.viewDidLoad()
let imageView = Seesaw
let tapGestureRecognizer = UILongPressGestureRecognizer(target:self, action: #selector(SeesawViewController.tapped));
tapGestureRecognizer.minimumPressDuration = 0
imageView?.addGestureRecognizer(tapGestureRecognizer)
imageView?.isUserInteractionEnabled = true
}
func tapped(touch: UITapGestureRecognizer) {
if touch.state == .began {
if(vibrateOn){
AudioServicesPlaySystemSound(1520)
}
let tapLocation = touch.location(in: Seesaw)
if(tapLocation.y > Seesaw.frame.height/2){
print("Go Down")
Seesaw.image = UIImage(named:"Down Seesaw");
seesawUp = false
} else if (tapLocation.y < Seesaw.frame.height/2){
print("Go Up");
Seesaw.image = UIImage(named:"Up Seesaw");
seesawUp = true
}
}
}
Another idea - would it be faster to implement this as a button? Are gesture recognizers just slow? Are the way I'm drawing the image states consuming the wrong type processing power?
Sems like you made mistake in your code. You want to create tap recognizer, but you created UILongPressGestureRecognizer
Please change line from
let tapGestureRecognizer = UILongPressGestureRecognizer(target:self, action: #selector(SeesawViewController.tapped))
to
let tapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(SeesawViewController.tapped))
Or you may add transparent button and put your code to its handler
// onDown will fired when user touched button(not tapped)
button.addTarget(self, action: #selector(onDown), for: .touchDown)
I set up a Swipe Gesture Recognizer and I connected it to the code, so that an UIImageView rotates when the user swipes to the left.
#IBAction func swipeToLeft(sender: AnyObject) {
UIView.animateWithDuration(1.0, animations: {
self.image.transform = CGAffineTransformRotate(self.image.transform, -3.14159265358979 )
})
}
I made sure the viewDidLoad method looked like this:
image.userInteractionEnabled = true
However, the UIImageView only gets transformed only once.
You can download the demo of the project from this link. Why is that happening?
I guess, the problem is that the moment you rotated your image, the gesture recognizer, associated with it, also got rotated. You can make sure yourself:
Make a swipe right-to-left. The image will rotate. Then make a swipe left-to-right. It will rotate again.
If you want to always handle a swipe right-to-left, you can do it in a couple of ways:
If your view is always rotated by 180 degrees, the easiest way is to change the gesture recognizer orientation (#LyndseyScott was faster than me to right the code for this one, you can check her answer :) ).
Another option (especially, if there might be a situation, when you rotate your view by some arbitrary angle), is to create a UIView on top of the view you want to rotate (but not as its subview!), and to add the gesture recognizer to it instead.
Just to elaborate on FreeNickname's answer, since the gesture recognizer "rotates" along with the UIImageView, you can change your code to the following so that the swipe gesture swaps directions along with the image and you can continue swiping from right to left to trigger the animation:
#IBAction func swipeToLeft(sender: UISwipeGestureRecognizer) {
UIView.animateWithDuration(1.0, animations: {
self.doubleDot.transform = CGAffineTransformRotate(self.doubleDot.transform, -3.14159265358979 )
}, completion: {
(value: Bool) in
if sender.direction == UISwipeGestureRecognizerDirection.Left {
sender.direction = UISwipeGestureRecognizerDirection.Right
} else {
sender.direction = UISwipeGestureRecognizerDirection.Left
}
})
}