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)
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 have got a wide LineChart with many entries. I want to let user tap (or better longtap/3D touch) on an entry to show modal card where user could edit data entry. I tried implementing chartValueSelected but the problem is that it runs even when user taps to scroll (i e taps without releasing finger) which is not how a button should behave. Is there any way to implement tap recognizing for LineChart label?
It appears that overriding the tap gesture recognizer for the chart can work. This question has some answers based on someone who was looking for a similar solution.
You can attach your own gesture recognizer to LineChartView and use method getHighlightByTouchPoint to get information about the selected point.
override func viewDidLoad() {
// ...
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onLongTap))
lineChartView.addGestureRecognizer(longTapRecognizer)
// ...
}
#objc func onLongTap(recognizer: UILongPressGestureRecognizer) {
if recognizer.state == .ended {
let highlight = lineChartView.getHighlightByTouchPoint(recognizer.location(in: lineChartView))
print("\(highlight)")
}
}
Edit: it seems the swipe gesture can only takes one direction a time now. If someone knows another way to handle multiple directions at once, I’d still appreciate information!
Eidt: I find a way to deal with multiple directions concisely in this [answer] (https://stackoverflow.com/a/46104997/9645644) It uses an array literal with forEach loop. It’s much more convenient than adding gestures and dragging actions separately from storyboard.
I’m trying to get swift swipe gestures to work, everything’s fine until I tried to detect the direction of the swipe. Below is my code. I don’t understand why this isn’t working and would appreciate help!
In a view controller’s viewDidLoad I set up and added the swipe gesture recognizer, with direction[.left, .right]. After that I implemented the handler method which needs to detect the direction of the swipe. There’s no other stuff in this view controller.
After it failed to work(no response when swipe), I added a few prints, and got the output in the title.
override func viewDidLoad() {
super.viewDidLoad()
let swipeGestureRecognizer = UISwipeGestureRecognizer(target:self, action:#selector(swipeHandler(recognizer: )))
swipeGestureRecognizer.direction = [.left, .right]
view.addGestureRecognizer(swipeGestureRecognizer)
}
#objc func swipeHandler (recognizer: UISwipeGestureRecognizer){
switch recognizer.state{
case .ended:
let direction = recognizer.direction
print(direction)
if direction == .left {print(”left”)}
else if direction == .right {print(“right”)}
else {print(print(“none”)}
defaul: break
}
No matter left or right I swipe, it always prints “none”. And the direction print always give a “UISwipeGestureRecognizerDirection(rawValue: 3)”
The direction property tells the gesture when to trigger. for example if direction == .right then the swipe will trigger only on a swipe to the right. (It does not tell you the direction detected)
You need to detect one direction at a time. I would also suggest to add a method to control each swipe direction. For example.
func setUpGestures() {
// Gesture that define a left swipe.
let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(Scene.swipeLeft))
swipeLeft.direction = .left
view?.addGestureRecognizer(swipeLeft)
// Do the same for the rest of the directions.
}
#objc func swipeLeft() {
// Do something when the user swipe left.
}
Hope it helps!
In my tvOS app I have a collection view with cells that represent movies.
I attached 2 gesture recognizers to the view:
Go to movie details on selection tap
Play movie directly (with an Apple TV Remote dedicated Play button)
let posterTap = UITapGestureRecognizer(target: self, action: #selector(ListViewController.movieSelect(_:)))
posterTap.allowedPressTypes = [NSNumber(integer: UIPressType.Select.rawValue)]
self.view.addGestureRecognizer(posterTap)
let posterPlay = UITapGestureRecognizer(target: self, action: #selector(ListViewController.moviePlay(_:)))
posterPlay.allowedPressTypes = [NSNumber(integer: UIPressType.PlayPause.rawValue)]
self.view.addGestureRecognizer(posterPlay)
And the respected methods
func movieSelect(gesture: UITapGestureRecognizer) {
if let cell = UIScreen.mainScreen().focusedView as? ItemCollectionViewCell {
let item = ItemViewController(nibName: "ItemViewController", bundle: nil)
item.item = cell.data
self.presentViewController(item, animated: true, completion: nil)
}
}
func moviePlay(gesture: UITapGestureRecognizer) {
if let cell = UIScreen.mainScreen().focusedView as? ItemCollectionViewCell {
let data = cell.data
// TLDR;
// Passing data to AVPlayerViewController and presenting it + start playing the movie.
}
}
Everything seem to work, apart from the fact that when I stop playing the movie and coming back to the list (by closing the AVPlayerViewController), my second gesture recognizer (Play button) no longer works. It is still there if I check with print(self.view.gestureRecognizers) but moviePlay() is never called again no matter what.
Is there a way to debug this? What may cause this issue? I'm thinking this is caused by UIGestureRecognizerState being still in "use"? Or maybe something like that. At this point I have no clue.
I've experience weirdness with gesture recognizers on tvOS. In my case, for some reason, the app behaved as if gesture recognizers were lingering on after container view had been dismissed. Oddly enough, I've observed this weirdness when launching and closing AVPlayerViewController a few times as well.
What I did to fix this was to remove use of gesture recognizers and overriding pressesEnded method instead.
I hope this helps.
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.
`