I started out trying to make a scrollview with buttons in it. I ended up diving deep into the pool of touch events and gesture recognizers. Here’s what I found.
The problem with putting a UIbutton into a scrollview is that when you tap the button, there’s a noticeable delay before the button is highlighted. Not satisfied with this, I started Googling around. Found a stack overflow and it all worked, but how does it work?
I needed to go back to the start.
What happens after you touch an iPhone screen?
- How does a touch find the object that should respond to it?
- How does the view handle the touch without gesture recognizers?
- How do views use gesture recognizers?
- How do gesture recognizes interact with each other?
How does a touch find the object that should respond to it?
Basically, the window takes the touch and checks to see which of it’s subviews the touch is in (aka hit testing). The next view checks its subviews and so on until there are no more subviews to check and we’ve found the deepest view in the view tree that contains the touch. It’s helpful that views are responders, so they can handle the touch events.
If that view isn’t able to handle the touch, then it would be forwarded back up the responder chain to it’s superview until it finds a responder that can handle the touch.
How does the view handle the touch without gesture recognizers?
Touches have four phases: began, moved, ended and cancelled. When the touch changes to a phase, it calls the corresponding method on the view that it’s in.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
What happens if you start touching in viewA and then move out of that view without lifting your finger. Does the touch call the method on viewA or viewA’s superview?
Turns out that the messages are sent to the object’s where the touch began.
Within these four methods, you get all the touches involved and you can use these touches to define gestures and trigger actions. For example, if you want your view to recognize swipe gestures, you can keep track of where the swipe action starts in touchesBegan:withEvent: and then keep track of how far the touch has moved in touchesMoved:withEvent: and once it’s greater than a certain distance, you can call an action. That’s exactly what people did before gesture recognizers were introduced in iOS 3.2.
How do views use gesture recognizers?
Gesture recognizers (GRs) are awesome because makes takes touch event handling to a higher level. Instead of tracking individual touches, a gesture recognizer just tells you if you should act on a user’s touches.
Also, if you need to recognize a new type of gesture, you don’t need to make a new subclass of UIView. You can just make a new subclass of GR and add it to a vanilla UIView.
After the gesture is recognized, the GR calls an action on a target and that’s where your app reacts to the touch.
Gesture recognizers get first dibs. Touch events go to them before being handled by the view. That means, when a gesture recognizer recognizes a touch, it can prevent the delivery of touchesEnded:withEvent: to the view and instead call touchesCancelled:withEvent:.
By default, during a swipe gesture, the touchesBegan: and touchesMoved: events will still be called while the gesture hasn’t been recognized yet. If you don’t want these events to be called, you can set delaysTouchesBegan to true on the gesture recognizer.
There are awesome diagrams in the Apple docs for gesture recognizers (especially, Fig. 1-3, 1-5, 1-6).
How do gesture recognizers interact with other gesture recognizers?
By default, views with more than one GR can call the GRs in a different order each time.
You can define the order of GRs with
You can prevent touches going to GRs by setting their delegates with
How did this help solve my problem?
The original problem was that a button in a scrollview had a delay in response to touches. The reason is that scrollviews have a property called delaysContentTouches set to YES by default.
Turn delaysContentTouches to NO and you can tap buttons quickly now, but you can’t scroll anymore. That’s because “default control actions prevent overlapping gesture recognizer behavior”. The default control action is tapping a button. The gesture recognizer it was preventing was the panGestureRecognizer on the scrollview.
Meaning that when you tap a button in a scrollview, the tapping of the button takes precedence and you won’t be able to scroll because all touches starting in a button would be interpreted as taps.
In addition, I had to create a subclass of UIScrollview and overwrite touchesShouldCancelInContentView: to YES, so that not all touches are no longer sent the button and the scrollview is able to pan.
TL;DR
- User touches screen (ex. drags scrollview).
- Screen finds the view the user touched (i.e. scrollview).
- The view uses any gesture recognizers to process the touch, otherwise handles the touch itself (i.e. scrollview’s panGestureRecognizer).
- Gesture recognizers or the view trigger actions that change what appears on screen (i.e. scrollview moves) or changes the model.