Frameworks • iOS • 1:00:53
Direct content manipulation is a big part of what makes using iPhone OS so compelling. iPhone OS 3.2 introduced a new way to define reusable custom gestures, decoupling the act of recognizing a gesture from the resulting action. Learn how to create custom gestures and manage their interaction with other gesture recognizers and existing touch handlers.
Speakers: Brad Moore, Josh Shaffer
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
[Josh Shaffer]
So we just got done talking about the basic UIGesture recognizer subclasses provided by UIKit. Now we want to talk a bit more about how gesture recognizers added to your views will affect the delivery of touches to those views using the existing UITouch, UIEvent, touchesBegan, Moved, Ended, and Cancelled methods, and UIResponder delivery.
And after we spend a bit of time on that we're going to talk about how you can write your own gesture recognizer subclasses. Because while pinch, pan, rotate, swipe, tap, double-tap, all these things are great, you may have some ideas of your own for gesture recognizers or some existing code that you actually just want to wrap up in a gesture recognizer and make use of all of the runtime behaviors that it's providing for you.
So let's get started. We're going cover, as I said, view interaction and then we'll get into subclassing. So first UIResponder review. Right? We already know how this works, we've been seeing it for two years since iPhone OS 2.0. When a touch comes down on the screen your touch is delivered to your UIView through the touchesBegan, moved, ended, and cancelled methods.
Now this is delivered to whichever view was hit-tested when that touch came down. If you came to the touch event session last year you may remember that these methods are actually called by UIWindows sendEvent method. And UIWindows sendEvent, in turn, is called by UIApplication sendEvent. And this has been the case since iPhone OS 2.0, it's always worked this way.
So I'm sure you've already used these things and know how they work. But quickly, visually, this is what goes on. Your application sendEvent, when a touch comes down calls UIWindows sendEvent, which calls touchesBegan, moved, ended, and cancelled on the UIView that the touch is in. So how is this modified by the presence of an UIGesture recognizer. Well, to find out let's add a couple of UIGesture recognizers to this view hierarchy.
Here I've got an UIViewController's view and that view has two subviews, these two UIViews at the top and bottom. So let's add an UI tap gesture recognizer to that bottom UIView subview, and let's add an UI pinch gesture recognizer to that UIViewController's view, the super view of those two subviews that we've got. And now let's put that exact same touch down in that lower UIView.
New application sendEvent, just as in the other case, will call UIWindow sendEvent. But now UIWindows sendEvent, instead of calling touchesBegan, moved, ended, and cancelled on that UIView, it's first going to call those exact same methods on those two UIGesture recognizers. So as Brad talked about, the reason why these two UIGesture recognizers both see the touch is because the touch is down in that UIView that has the tap gesture recognizer, so the tap sees it.
And the pinch is attached to that View Controller's super view, one of its ancestors. So that pinch gesture recognizer also gets to see that touch. So touchesBegan, moved, ended, and cancelled sent to the two gesture recognizers. Now assuming nothing has changed, we'll talk about what these changes are in just a minute.
That UIWindow sendEvent will then call the same touchesBegan, moved, ended, and cancelled methods on the UIView that the touch is actually bound to. So the only difference is that these gesture recognizers get to see the touch before the UI does. And that's important, and we'll see why in just a second.
So the reason for that, actually right now, being important is that there are two ways that the presence of an UIGestureRecognizer on a view can affect delivery of touches to that view or you know, whichever view the touch was actually bound to. The first way is the gesture recognizer can actually cause that touch to get cancelled on the view. So let's take a look at the sequence of events that will happen and how that touch cancellation will work. So in order to see it, we're going to put down two touches, one in each of those two subviews, one at the top and one at the bottom.
Our gesture recognizers will both receive touchesBegan withEvent, they're both going to remain in state possible, because a touch coming down in either of these views is not enough to determine that we've got a tap or a pinch. So both gesture recognizers are still possible, nothing's been determined yet. So the UIView that each of these is associated with will receive a touchesBegan withEvent method, for its appropriate touch.
Next, let's assume that the users move their fingers a bit closer together. So we get touchesMoved withEvent delivered to both of those UIGesture recognizers, the tap and the pinch. The tap obviously only sees the one touch at the bottom that's in its view, the pinch sees both. Both gesture recognizers remain in state possible.
Because tap allows a little bit of movement in its touch before it fails, and pinch requires a little bit of movement in its touches before it recognizes. So a little bit of pinching together is not enough, they're both still possible. So the UIViews each receive touchesMoved withEvent for their appropriate touches.
Now let's assume the user continues to move their fingers together. Both UIGesture recognizers receive touchesMoved withEvent. At this point the tap's touch has moved far enough that the tap says I'm out, and it fails. Can't be a tap anymore. The pinch gesture recognizer, however, has seen these two touches moving closer and closer together, and now decides that it's actually a pinch that's really happening. So it moves to state began. Now because that pinch gesture recognizer has recognized the touches will now actually get cancelled on their UIViews.
So both UIViews get touchesCancelled withEvent sent to them, each for the touches that are, you know, associated with their view. So the top UIView gets touchesCancelled for its view, and the bottom UIView gets touchesCancelled for its view. So as far as those views are concerned now these touches are done. It's not going to be informed of them any more. They're gone.
However, the UIGesture recognizers do still know about the touches, and the pinch will continue to track over time. So now as we move farther and farther together, we're going to have touchesMoved withEvent called again. But now it's only going to be called on the pinch gesture recognizer, because the tap has already failed.
It's already indicated it's not interested in being a tap, it doesn't get updated about this touch any more. This is an important thing to keep in mind as you're sub classing UIGestureRecognizer, which we'll see in just a few minutes. Because the implication is that a gesture recognizer may not actually see a full touch sequence.
If it fails before the touch has ended it won't be notified about the rest of the touch sequence. So the pinch now is in state changed, because these touches have moved together even further, and it's now updated at scale. So it's in a new updated state and needs to notify its target actions that the state has changed. So pinch is in state change. UIView, as I said, touch is already cancelled, it gets nothing.
Now when the touches lift, we get touchesEnded withEvent with the pan gesture -- sorry, the pinch gesture recognizer for the same reason. It's the only one left that's still seeing these touches. And it moves to state ended because both the touches have lifted. Now this is the default behavior. And in almost every case it's what you're going to want.
Because if a gesture recognizer is recognizing, we've determined that the user is trying to perform some particular action, we've recognized that they're pinching their fingers together and we want to do something in response to that. You don't, at that point usually want to be tracking those same touches on the UIViews that the touches were associated with, because then you might start doing some other actions or performing something else when really you're just trying to pinch.
So normally, you do want those touches to get cancelled. But if for some reason you don't, say you're adding some gesture recognizers to your existing application and you've already got your own touch processing that you want to keep using at the same time, and you really don't want those touches getting cancelled, you can adjust this with the cancel touches in view property.
So from the example we just looked at, we had set our pinch gesture recognizer's cancels touches in view property to no instead of the default yes, the touches would not have gotten cancelled on the views. And we can see that here, it's highlighted in yellow, the difference between the last slide and this one. Now when the pinch gesture recognizer moved to began, UIView, both UIViews continued to receive touchesMoved withEvent all the way through touchesMoved to event.
They got the entire touch sequence as if the gesture weren't recognizing at all. So in most cases you want to leave this and the next properties we're going to talk to in their default states. Because we've picked them very specifically because they generally provide the behavior that you want. But if for some reason you've determined that's not the case it's available to be changed. So the next two properties to talk about are sort of identified the second of the two ways that gesture recognizers can affect delivery of touches to their views.
And this is by actually delaying deliver of the touches to their views. The presence of a gesture recognizer can cause the view to only see the touch information after some period of delay when the gesture has determined that's it's not actually going to happen, that the gesture won't recognize. And the two ways -- there are actually two ways that delaying can happen. You can either delay touchesBegan or delay touchesEnded.
So by default, gesture recognizers actually delay touchesEnded and not touchesBegan. So delays touchesEnded is actually true by default. And what this is, it means if a touch ends and a gesture recognizer hasn't yet failed or recognized, so it's still possible, the touches ended will not be send immediately to the UIView. It's best to see an example to understand, it's not entirely clear why that would be.
The most common case where you'll find this happening is in a double-tap, because a first -- a single tap must have happened before a double-tap happens, and maybe you still want to be able to cancel the touch after a double-tap has happened, the first touch. Well, we can't have delivered touchesEnded to the view for the first touch if we want to cancel it later. So delays touchesEnded holds off on delivering touchesEnded so we can still cancel it.
Delays touchesBegan is the alternative, and this effectively delays the delivery of the entire touch sequence. Just the simple presence of an UIGestureRecognizer on a view with delays touchesBegan set to yes will cause the view to not see touchesBegan until that gesture recognizer has either failed or recognized. So let's actually look at that on a timeline, it should be a little more clear.
So first delays touchesEnded, this is the default configuration. An UIGestureRecognizer will delay touchesEnded. So I've changed our tap gesture recognizer that we looked at now, same view hierarchy, but now our tap gesture recognizer is a double-tap gesture recognizer. So let's put down a single touch in that lower UIView with our double-tap gesture recognizer.
Both recognizers receive touchesBegan, double-tap still possible, pinch still possible, and UIView receives touchesBegan. That finger moves just a little bit, but that's OK because a tap can allow a bit of movement in its touch. So the recognizers both receive touchesMoved withEvent. They're both still possible. And the UIView receives touchesMoved withEvent. But now let's say that that finger lifts.
Both UIGestureRecognizers receive touchesEnded. The double-tap now is still possible, because a double-tap can't recognize until two fingers -- or a finger has tapped twice. So it's waiting now for a bit to see if another finger comes down. So there's going to be a timer set internally in the gesture recognizer and if a finger doesn't come down soon enough it will fail. But right now, it's just possible, waiting for that to happen.
The pinch gesture recognizer has now failed, because all fingers have lifted, can't be a pinch. But because we have delays touchesEnded, the UIView has not been informed about that touch ending, still delayed. So now some time period passes, the tap gesture recognizer says not a double-tap, user didn't put another finger down. So it fails. Once that's happened, then we deliver touchesEnded to the UIView. So it can introduce some latency in your touch processing if you've got RAW Touch Handling on your UIViews.
So now let's take a look at delays touchesBegan. This is no by default for a very important reason. And it's because it will actually guarantee to introduce latency in your event handling. For example, if you've got an UI button in your view, UI buttons highlight themselves, excuse me -- on touchesBegan withEvent, there's no gesture recognizer on an UI button, it's just highlighting when the touch comes down that it receives touchesBegan. So if you have a gesture recognizer on that view that's delaying delivery of touchesBegan, when the user puts their finger down they won't see any visual feedback in the button that they're actually pressing, which is going to be unexpected.
So unless you've got a really specific reason for setting delays touchesBegan, I strongly encourage you to leave it turned off. Because it really does end up with a not very good user experience in most cases. But anyway, let's see what effect it has. So we've got the same view hierarchy again, but I've gone back to a single tap gesture recognizer. I've set delays touchesBegan now on just that single tap gesture recognizer. The pinch is still configured in the default way.
So we put our finger down. Both gesture recognizer receive touchesBegan withEvent, and they're still possible for all the same reasons that we just saw. But the UIView has seen nothing, it doesn't even know that that touch exists. So now the user moves their finger a little bit, same deal, touchesMoved withEvent to the gesture recognizers. They're both still possible.
Now the user moves their finger even a bit more. We get touchesMoved withEvent delivered to both the gesture recognizer. The tap now fails because you've moved far enough that the tap recognizer realizes it's not a tap, so it's failed. Pinch gesture recognizer is still possible, because that's OK because we had it in its default configuration, it's not the one delaying touchesBegan.
The tap is the one that was delaying touchesBegan, and now that it's failed there's no reason to continue delaying. So at this point the UIView gets touchesBegan withEvent. And at some point after the finger had actually come down, and in fact it's moved quite a bit already, so the one thing to make note here is that I've actually got a ghosted version of that touch up in the UIView.
That -- the important point here is that when we deliver touchesBegan withEvent after this delay it actually has these state that the touch had when it first came down. So the touch -- or the view receives touchesBegan withEvent in its current location and the time stamp on the touch when it's being delivered is the original time stamp.
So it's actually kind of important to keep in mind here that if you're using gesture recognizers that are delaying touches and you're actually doing calculations of velocity or things that are time-based in your touch handling, you should really be doing those calculations based off the time stamps of the touches not off of wall time when you receive the event. Because there may have actually been a delay. And if you want to get correct velocity calculations you really have to have used the actual event times.
So we got touchesBegan withEvent, but the view is kind of out of sync, right? The view still thinks the touch is down where it started. So in addition to touchesBegan withEvent in the same turn of the run loop, right afterwards, you're actually going to get touchesMoved withEvent on that UIView. To update the touch -- to update the view with the location that the touch is at now. So another important thing to keep in mind here is that we will only send one touchesMoved withEvent in order to get you updated.
So if you're delaying touchesBegan and there's a really long delay before that gesture recognizer failed, even if that touch was moving all over the place and there's a huge amount of swipes, they wrote their name, the view is only going to get one event. So you just lost all that information about the intermediate state. It would just be too expensive to queue it up and deliver all of it.
So another reason to avoid delaying touchesBegan, if at all possible. Anyway, now we're back into the normal stream of events, right? Our tap gesture recognizer has failed, we delivered touchesBegan and touchesMoved to the UIView. So we just continue as normal. The touch moves some more, we get touchesMoved withEvent to our pinch gesture recognizer, which is still possible.
The taps failed, so it gets nothing. But now the UIView is also getting touchesMoved withEvent, just as if nothing had happened. Finally, when the touch lifts we get touchesEnded on the pinch gesture recognizer. It fails because the touches have all lifted. And the UIView also gets touchesEnded withEvent.
That's pretty much all there is to the effects of UIGestureRecognizer on delivery of touches to UIView. Just those three things. It cancels touches in view for touch cancellation and delays touchesEnded and delays touchesBegan for touch delay. So let's move on to more interesting things now. You've got your own touch code or you've got ideas for gestures you want to define and you want to play with our gesture recognizing system and coordinate with our gestures and get the same exclusivity rules and failure requirements and all this kind of stuff that UIGestureRecognizer provides. So how do you subclass UIGestureRecognizer.
Well, the first thing that you have to do any time you go to subclass UIGestureRecognizer is pound import UIGestureRecognizer subclass level H. So why? Well, there's a bunch of things in UIGestureRecognizer subclass.h that are intended only for use by subclasses of UIGestureRecognizer. We really, really, really don't want code outside of implementations of gesture recognizers using these methods. They're just -- I can't stress this enough. They're intended specifically for use in your subclasses. So pound import UIKit, UIKit.h does not include gesture recognizer subclass.h. Because we don't even want your autocomplete showing you the methods that are defined in this header.
So when you actually go to implement gesture recognizer, just pound import, and then you have access to everything. So what -- what do you have to do. Subclass UIGestureRecognizer. All right, what do I implement. Well, there's just one single most important thing to -- well actually sorry -- we have to get to that in one second.
Before we do that, keep in mind that UIGestureRecognizer is actually not a subclass of UIResponder, which is pretty unusual for touch delivery as we've been seeing it in the iPhone OS. UIView, UIViewController, UIControl, UIApplication, UIWindow, these things are all subclasses of UIResponder, and it's actually UIResponder which defines those touchesBegan, moved, ended, and cancelled withEvent methods.
But I already said that UIGestureRecognizers receive those methods. And how does that work if they're not subclasses of UIResponder. Well, UIGestureRecognizer actually declares these methods as well. It just declares them with the exact same prototype. So same methods that you can implement, even though you're not an UIResponder subclass. You've seen these before in your touch handling. The reason it's not an UIResponder is because there's no responder chain involved in gesture recognition.
We're simultaneously delivering touchesBegan, moved, ended, and cancelled to multiple recognizers, even if you call super touchesMoved, we're not forwarding that event on to some other object. The gesture recognizers are end points for these deliveries. So yeah, not a subclass of UIResponder, doesn't participate in the responder change, it's just on the side and affects it the way we just talked about. So sorry, now the most important thing that you have to remember when you go in subclass UIGestureRecognizer.
The one thing that you absolutely have to do, and in fact the only thing that you really have to do, is change the gesture recognizer state. Brad, if you were in the last session, talked about the different states that gesture recognizers might be in, those are UIGestureRecognizerStatePossible, Failed, Recognized, Began, Changed, and Ended. And we'll see those a lot in just a minute.
So don't worry that I said them and you didn't see them. But some of you may be thinking if you looked at the UIGestureRecognizer header that state is read only, which actually is true UIGestureRecognizer.h defines at property not atomic, read only, UIGestureRecognizer state. So how can you change it? Well, UIGestureRecognizer subclass.h redefines the property as read write.
Another reason that it's in the subclass and not included by default, we don't want people outside of the gesture recognizer changing your gesture recognizer state because it's state that you're defining. You're creating a state machine in this gesture recognizer to determine how far along in the recognition you are. You don't want some other code somewhere else trying to change the state on you.
That just breaks encapsulation and can totally throw everything out of whack on your own internal state tracking. So when you subclass, import the header, and it will be redeclared read/write so you can move the gesture recognizer state through this defined state machine that we'll see in just a minute.
So how do we move it, what can we do? Well all gesture recognizers always start in UIGestureRecognizerStatePossible. This is the default state, where they're just -- it may be happening, I haven't seen any touches yet, or maybe I have seen some touches, but I don't know if I'm recognized. So we start there. Now failure is the most common thing you're going to do. And we really do hope that you fail quickly.
So you want to move from UIGestureRecognizerStatePossible to UIGestureRecognizerStateFailed as soon as you possibly can for all the reasons we just looked at with touch delaying. If someone were to set delays touchesBegan on your gesture recognizer, if you don't fail quickly the touches will be delayed for a long time. So fail as quickly as possible, and as Brad said failure is the most common thing that's going to happen here. Touches come down on the screen, user is probably not performing your gesture right now, so you're just going to fail.
And then from failed you'll end up back in UIGestureRecognizerStatePossible. Now I've left state possible -- actually I colored it the same as the original state responsible, but that transition is different from the transition to failed because you don't actually have to perform it. And we'll see it in just a second when it happens.
But keep in mind you'll never be in charge of moving your gesture recognizer back to state possible. You just have to get to state failed or one of these other states we're about to talk about. So that's the first. Now what happens if a gesture recognizer actually does recognize the gesture it was looking for.
User taps and we recognize the tap. Well then we move from UIGestureRecognizerStatePossible to UIGestureRecognizerStateRecognized. Now state recognized is for discreet gestures. Gestures that happen as a result of some action at a discreet point in time and do not update over time. So a tap, a swipe, these things are discreet. They aren't going to continue after they've been recognized. So we've moved to UIGestureRecognizerStateRecognized, and we're done.
From there we get moved back to UIGestureRecognizerStatePossible automatically. So what if you have a gesture recognizer that needs to report changes over time, something like a pinch or a swipe or a pan. User has moved their fingers together, but as they continue to move you continue to report new state about the pinch. Well then you have a continuous gesture. And there's a different set of states for that. So we start again in state possible. And move to state began.
Now it's important to keep in mind here that you don't actually have to define ahead of time whether your gesture recognizer is discreet or continuous. Just determining which one of these states you want to move to is enough to let us know what you're trying to do. So you move from state possible to UIGestureRecognizerStateBegan. From began then, you may move to state changed. So as the user has pinched some more, you want to update your targets about this change, move to state changed.
From state changed, then, you'll go to UIGestureRecognizerStateEnded. Once you've completed your recognition. Now usually this has happened when the user lifts their fingers. But maybe some other state in your gesture recognizer has caused ended to happen and you just set that state. If it happens while the touches are down, as we saw before, you will not actually see the entire touch sequence. Then from UIGestureRecognizerStateBegan, you can also go directly to state ended. If it happened fast enough that there was no change in between. And then finally, you'll end up back in state possible. Again, you don't have to worry about that transition.
Another possibility is that last state that we didn't talk about, UIGestureRecognizerStateCancelled. So from state began or state changed, if something happens that causes you to realize that your gesture needs to fail or at this point you can't fail because you've already recognized, you want to cancel. So that's if a phone call comes in, you've received touchesCancelled withEvent, you want to cancel. So from either of those states you can move to UIGestureRecognizerStateCancelled. And then from there you'll also end up back in state possible. So that was kind of a lot and it was spread over a couple of slides.
So for those of you who were a little more visual or like the state machine kind of thing, we've got some pictures. We've got start in state possible always. Most common, we're going to state failed. If you've got a discreet recognizer, you'll be in state recognized. And if you've got a continuous recognizer reporting changes over time you'll end up in one of these, you know, loops over here, began, spend some time in changed, end up in cancelled and ended.
So I mentioned already that changing your state is the most important thing that you're going to do in subclassing your gesture recognizer. Now of this most important thing the most, most important thing that you have to do is end up in one of those four end states. You must get to UIGestureRecognizerStateRecognized, Failed, Cancelled, or Ended. Any is fine, but get to one of them.
The reason for this is that as I mentioned, the runtime is going to put you back in state possible automatically. UIKit handles that for you, you don't have to worry about it. But getting to one of these end states is important to let UIKit know that you have either failed to recognize or finished recognizing so that we can satisfy failure dependencies or deliver delayed touches or cancel touches on views.
If you don't end up in one of these states you can basically end up hanging your entire touch processing loop because you've got one gesture recognizer sitting there and everyone else is waiting on it to fail and it's just hanging along, has no touches, never going to do anything. So you really have to end up in one of these states.
Once you do, then you'll end up back in UIGestureRecognizerStatePossible, we take care of that transition for you. So what does it mean that we take care of it for you. Well, there's this automatic reset that UIKit handles that puts the gesture recognizer back in a default state, back into state possible, to be ready for another attempt to recognize the gesture.
So that's this automatic reset, and the first thing that happens with automatic reset is that we put it back to state possible. You also have an opportunity to reset any of your own state that you may have been hanging on to in your current attempt to recognize the gesture.
If you had some state that you were tracking, touch locations, the zoom scale of a pinch, that kind of a thing. When reset happens and you want to go back and start another attempt to recognize the gesture, you may want to clear out these iBars or cancel timers, anything you may have done in the previous attempt. So you get an opportunity to do this in your subclass.
And you can do that by subclassing and overriding the reset method. Now this is also defined in UIGestureRecognizer subclass.h. And is specifically for use to be called by UIKit, us, on your gesture recognizer subclass for you to reset things. It is not intended for you to call in attempt to reset the gesture recognizer. Doing so is just not supported. So don't even try it. But do subclass and implement, you know, whatever you need to clean up in the reset method.
In addition to this, all failure requirements will then have to be fulfilled again. If you saw Brad's session just before this, he talked a little bit about failure requirements for tap and double-tap. A tap may require a double-tap to fail so that if a double-tap happens the tap doesn't actually fire.
The automatic reset will reset those failure requirements so that on the next attempt to recognize these gestures that failure requirement has to be met again. An important thing to keep -- make note of when looking at this automatic reset is that failure requirements, their presence, will basically tie the automatic reset of those two gesture recognizers together.
So if a tap requires a double-tap to fail, and the user double-taps, even though the first tap has gone into the recognized state long before the second one has actually finished, the reset method will not be called on that first tap gesture recognizer until its dependent, its failure requirement actually also finishes. And then both will be reset at the same time.
This is just to make sure that there's never a case where one will have been reset and sort of end up depending on another that hasn't even finished yet. They're always reset simultaneously, if you've set up failure requirements. So if your reset method seems to be getting called later than you expect it might be because you've got a failure requirement set up.
Additionally, all existing touches on that gesture recognizer are ignored, and I kind of glossed over this, but we did see it originally, when we talked about how touch delivery on UIViews is affected. Once a gesture recognizer fails or gets the state ended, cancelled, or recognized, all the existing touches on that view will now be ignored. They're no longer delivered to the UIGestureRecognizer. And I'm just repeating it because it's really important to keep in mind. You may receive touchesBegan and touchesMoved and never received touchesEnded or cancelled in your subclass of UIGestureRecognizer. In place of that, though, you will have the reset method called.
So if you're not going to see the rest you'll know that you got reset and you should just go back to a new state and be prepared for another attempt to recognize. Which is also -- it's actually quite good. Because it means that if your gesture recognizer has failed, and you're now ignoring these touches and you've been reset, you're not going to continue seeing that touch which you've already determined was whatever gesture you were looking for. So you can at that point you don't even have to keep tracking it, it's effectively gone as far as the gesture recognizer is concerned.
Ignoring touches happens automatically in reset, but you can also determine earlier without even moving to one of those end states that you would like to ignore a touch. And you can do that by calling ignoreTouch forEvent. So you might want to do this if you're implementing a pinch gesture recognizer. And you've already got two touches and you're tracking a pinch, and you don't want to track any more.
So if you're already tracking those two and you get another touchesBegan withEvent method on your gesture recognizer you can just call self ignoreTouch forEvent and pass the touch that you want to ignore and the event that got passed into the touchesBegan method. And from that point forward it will behave exactly the same as the ignoring we just talked about. You will not be updated with that touch information any more.
But manually calling ignoreTouch forEvent with this method has one additional effect. And that is that the touch that you're ignoring will not be cancelled on the view, even if your gesture recognizer recognizes. So it basically is a way to filter out touches in your gesture recognizer itself that you don't want to be part of the gesture.
So if you have your own tracking logic in the view and you've got another gesture recognizer on top that you've added since you want to recognize some additional gesture, you can still pass some touches through to the view and ignore them on the gesture recognizer, so that even if the recognizer recognizes, the touch handling code will still see it.
Moving through these states is the most important thing that you do, right? What happens when you actually move to these states? Well, there's a couple things, and it's pretty much the stuff we already talked about. There's the actions being performed and the resets being performed. So the only thing left to talk about with that is when they actually happen.
So in UIGestureRecognizerStatePossible, obviously we're not going to be performing any actions and there's not going to be any resets because this is the default state , we've already been reset, we're back in state possible. So nothing happens there. In UIGestureRecognizerStateBegan. When you move to state began your action methods will be performed. So if clients of your gesture recognizer have allocated your gesture recognizer and set up target action pairs on your gesture recognizer, just moving to state began is enough to have those target actions called.
Which is great, because it means you don't have to worry about calling them yourself or worrying about when to call them. Just move through of the state machine and everything's taken care of for you. Your action methods will be permed at the correct time by the UIGestureRecognizer runtime and UIKit. Also we've got the UIGestureRecognizerStateChanged. Same deal.
Actions are performed automatically. The important thing to keep in mind with state changed is that this actually happens for you regard last of whether you've continued to assign to the state property. So if you implement touchesMoved withEvent, and you're already in state changed, and you receive another touchesMoved withEvent, because the user has moved their fingers some more. Even if you don't assign state changes again to the property that's already changed, which would be kind of redundant and not really have any effect anyway, we'll still perform the action method every time the user moves their finger.
So for continuous gesture recognizers that are in state began and changed, every time the user moves their finger it -- targets and actions are going to get called to give your target action pairs an opportunity to adjust for the new state of your gesture recognizer. Now finally, we've got UIGestureRecognizerStateEnded for the continuous gesture recognizers. Again, your actions will be performed because the target action methods need a chance to clean up anything that may need cleaning up as a result of the gesture recognizer finishing. So gesture recognizer state ended, performs actions, and then resets. For cancellation, UIGestureRecognizerStateCancelled, it's the exact same thing.
The target action pairs need to know that the gesture recognizer isn't going to end, that it's actually been cancelled. And give the code that's in that action method an attempt to cancel anything that it was doing as a result of the gesture recognizer. So actions are performed and the reset happens right afterwards. For discreet gesture recognizers, we've got UIGestureRecognizerStateRecognized. And the exact same thing happens. Action methods perform automatically, reset happens automatically.
And finally, for UIGestureRecognizerStateFailed, no actions are performed. The failure of a gesture recognizer is not something that gets reported to the target and action methods of that gesture recognizer. So if you're implementing an action method for the gesture recognizer you know that's been recognized, you don't have to worry about failure. However, we do reset automatically, as a result of moving to UIGestureRecognizerStateFailed.
All right, so that's the concept. And it's -- there's not much to it, really. You just move through the state machine. You -- as you're writing your recognizer, you do whatever it is that you're trying to do in your touch processing to figure out if the gesture's happening. But as far as interacting with UIKit and the important things to do as a subclasser, you just move through that state machine. That's it. Just assign new states. So now let's look at actually writing one.
I want to stress before I actually show you this code that you should never actually do this. What I'm about to show you is how to recognize a tap. But really, it's how not to recognize a tap. Because we've already written the UI tap gesture recognizer. And it's significantly better than anything we could write here in a couple of slides.
And it knows how to handle multiple taps, multiple fingers, multiple fingers tapping multiple times. It's really configurable, and it has the exact same definition that everyone else's tap is going to have. So you know, we're going to show you how to do this, but please never do this.
[ Laughter ]
[Josh Shaffer]
Sorry, it's the simplest gesture recognizer I could come up with. All right, so we've got the simple tap gesture recognizer. First #import UIKit, UIKit subclass.h. Right? First thing we have to do. Then we'll declare our interface, add interface, simple tap gesture recognizer, inherits from UIGestureRecognizer. And we don't need any iBars, because this is the simple tap gesture recognizer.
So we're going to implement first touchesEnded and touchesCancelled. In fact, touchesEnded is the minimum that we would have to implement in order to have a functioning tap gesture recognizer. Because that's when taps actually occur, when the user lifts their finger. So in touchesEnded, we're going to implement the exact same thing that we saw in UIResponder, touchesEnded withEvent.
And then we're going to check to see if we're a tap. And I'm kind of cheating a bit here by using UITouch's tap count property so I can actually avoid having any state. So we'll ask the event for all the touches for gesture recognizer self. So that's our subclass. And get their count.
So if the event knows that there's more than one touch in the gesture recognizer, it's not a single tap. We're actually -- for a simple tap, we're just looking for a single finger, single tap. So if there's more than one touch we're not a tap. Also then, once we know that there's exactly one touch we can call touches anyObject, because we know there's only one to get the UITouch out and get its tap count and see if it's 1.
So if we have 1 touch and that 1 touch is a tap, then we know we recognize the tap. So set our state to UIGestureRecognizerStateRecognized. If that's not the case then we must have failed. We either have too many touches or the touch wasn't a tap. So in that case, self.state = UIGestureRecognizerStateFailed to indicate that we failed.
Now you know, this is all right, but we're not failing very quickly. And in fact we've also ignored the whole case of touch cancellation. So first let's introduce touchesCancelled withEvent, to make sure that we don't drop any touches on the floor and fail to move to one of those four end states which could end up hanging the rest of our recognition. So touchesCancelled withEvent. If the touch is being cancelled we definitely aren't tapping. Touch is being cancelled. So unconditionally, we can set our state to UIGestureRecognizerStateFailed.
But now we really want to, you know, fail quickly. Because we like to fail. So UI -- sorry, touchesBegan withEvent is the first place we have the opportunity to fail. So in this case we want to see if we've got more than one touch. So the same thing we did in touchesEnded, we'll call it event, touches for gesture recognizer self, get the count. And if it's greater than 1, we'll set our state to failed so that we failed really, really soon.
But even if there's only one finger on screen we can fail if that finger's moved really far. So we'll implement touchesMoved withEvent. And we'll check to see if that touch is still a tap. So we already know that there's only one touch, because we failed in state began if that wasn't the case. So we can call touches anyObject, and get its tap count.
And if it's not 1 then we're no longer a tap. So we can set self.state = UIGestureRecognizerStateFailed. So now we're actually failing quickly. And it's -- you know, incrementally better implementation than what we started with. But still not nearly as good as UI tap gesture recognizer. So you know, don't do it.
All right, so now, another thing Brad talked about, if you were here in the last session, is that tap gesture recognizers should usually be stackable. If you tap twice you've had both a single tap and a double-tap happen. And usually you want to fire action methods for both of those things. But as we have it implemented, a single tap gesture recognizer will always exclude a double-tap, because the single tap will be recognized first.
So, you know, that's not ideal. So how can we fix that. Well, you could require all your users to actually implement that gesture recognizer should recognize simultaneously with gesture recognizer method. But then they might spend the rest of their time writing that method because it's so ridiculously long.
So you probably want to prevent this on your own in the subclass. And we provided two methods that you can override to affect the exclusivity rules in your subclass. The first is -- canPreventgestureRecognizer. And in order to see this, we're going to pretend that we've now rewritten our code in touchesBegan, moved, and ended to support multiple taps.
So we'll add this number of taps required property so that we can set a single or double-tap, tap gesture recognizer. So now we can implement canPrevent gestureRecognizer to allow a single tap and a double-tap to happen at the same time. So in order to do that, we really just want to make sure -- and this is in our subclass, so self is a simple tap gesture recognizer.
We want to make sure that our tap count is -- are they greater or less than -- though I should probably see, because I forget. First we should make sure that we are kind of class, simple tap gesture recognizer. And if -- sorry, if the other -- yeah, let's start over here. We should see if the other gestures recognizers we're finding out about preventing is a simple tap gesture recognizer.
If it is, we know that it implements that number of taps required method as well. So we can see if the other gesture recognizer's number of taps required is greater than ours. So we are a single tap gesture recognizer, and this other one is a double-tap gesture recognizer. In that case we do not want to prevent it.
So we return no. And if that's not the case then either the other thing is not a simple tap gesture recognizer, or it has a lower tap count than us, in which case we do want to prevent it. So we'll just go back to the default, then. And return yes. Now I mention that there's two methods provided here, and the other one is just kind of the inverse of this. It's canbePreventedByGestureRecognizer which gives you the other way.
So this let's you determine if you want to prevent another gesture recognizer and can be prevented by let's you determine if you want to be prevented by another gesture recognizer. So the delegate methods should recognize simultaneously with just -- oh man, too many words -- the delegate method gesture recognizer should recognize simultaneously with gesture recognizer, is basically both of these combined, right? You can determine that two things can recognize at the same time.
This gives you slightly more control in that you can determine which one should exclude the other. So as a subclass, you've got a bit more control than as the delegate. So before we have Brad come up and show us how to actually implement some of this stuff there's a couple of points that are pretty important to keep in mind while you're writing your gesture recognizers. The first is that as much as you can, you should perform your calculations for recognition in screen coordinates.
Now you might be thinking why is this important? Well, we actually found this out sort of by accident with trial and error over time, so we want you to learn from our mistakes. We implemented UI long press gesture recognizer, doing its recognition in the coordinate space of the view it was attached to. And that seemed great, everything worked fine.
You would put your finger down and after some period of time the action method would fire because it was a long press. But then we put long press gesture recognizers inside of an UI scroll view, a view that's in an UI scroll view. So we would start scrolling around and some time later our long press would fire because the long press gesture recognizer was calculating the coordinate space of the view it was attached to, which was moving with our finger because the scroll view was scrolling it. So that's not really a long press, I mean, your finger is moving all over the place.
If we had done the calculations in screen coordinates we would have avoided that problem entirely. So you know, perform calculations for recognition in screen coordinates. But then when you go to actually use the values, so if you're applying -- reporting a scale or a rotation, you generally then want to convert back to local view coordinates to report those coordinates to the code using your gesture recognizer.
So an example of that is UI pan gesture recognizer, which provides the translation, actually, through a method called translation in view that let's your user determine what coordinate space they want to the translation in. If you don't offer the coordinate space you probably just want to pick the view that you're attached to as the default. But it can be useful to let the user decide to. Not the user, but the other code using your gesture recognizer.
So yeah, that's kind of our best practices, stuff we've seen that we've done wrong. Hopefully you guys don't fall into the same trap. So with that in mind, I'd just like to have Brad come up and show us how to actually write an UIGestureRecognizer subclass.
[ Applause ]
[Brad Moore]
All right, thank you, Josh. Here we have a project that's made the rounds at WWDC and most recently was featured at Stamford CS193P iPhone programming course. And I'll just show you what it looks like. Kind of familiar if you were here for the last session.
It's a Lightroom-like app, where you manipulate images, you can translate them, you can put two fingers down at the same time and translate both together. And you can even pinch and scale. So pretty cool, right? This was done before gesture recognizers existed. So I'll show you briefly how it's implemented. We have a View Controller that adds three touch image views to its subview, to its view.
And touch image view is a subclass of UIImageView. And touch image view exploits this really cute property of coordinate systems where if you have two points at one coordinate system and the same two points in another coordinate system you can figure out an outline transform to transform one to the other. It's really convenient and it's especially convenient, because UIView has a transform property. So if we go and look at the code -- well first let's look at the instance variables.
We save the original transform and we have a dictionary mapping the touches to the initial points. And as we go and look at the code and here I'll just focus on the touch methods, in touchesBegan, we save away those initial points. In touchesMoved, we compute an incremental transform using that property of coordinate systems I mentioned, and then we just set the transform on the view because this is a view.
In touchesEnded we clean up state, in touchesCancelled, we do the same. Now look again at touchesMoved. This is a really easy way to handle a Lightroom-style app. We just say self transform. But the handling of the method, this one line self-transform is very tightly coupled to all the recognition that's going on of their transform.
And it would be nice if we could decouple them and use this transform property across all manner of views. So that's what we're going to try to do today. And so first I'm going to add a new file. I'm going to call it transform gesture recognizer. And transform gesture recognizer is just a basic subclass of UIGestureRecognizer.
And look at the implementation. There's nothing here, it's just scaffolding. And I'm going to go through and paste in everything we had in the touch image view. So the first thing is an original transform and touch begin points. Well, we still need those here absolutely. One additional iBar I want to add is the iBar that was on the touch image view as part of its view. It had a transform property. Well, we don't have a transform property on transform gesture recognizer, in UIGestureRecognizer. So we need to implement that. So we'll add storage for that.
And let's go ahead and declare a property. It's a cgf line transform, which maps between coordinate systems, and note here that I've made it read/write. Strictly speaking, it could just be read only and it really wouldn't change the implementation. But if you were here for the last session you saw that as a convenience making these properties writable really simplifies what you have to do in your action method. So if you have a continuous gesture recognizer consider making properties like this writable. OK, so into the Implementation file. All right, the very first thing I'm going to do is import UIGestureRecognizer subclass.
And this is crucially important, because the job of this gesture recognizer is going to be -- to manipulate both its transform state, but to interact with the gesture recognizer runtime we need to manipulate the state. And the state is a read only property, as Josh mentioned, in UIGestureRecognizer.h. When we import the subclass header we can start manipulating it. OK, so let's get started.
First I want to add an initializer and I'll set the identity -- I'll set the transforms to identity to start with. And I'll initialize a dictionary that I can manipulate later. And similarly, I'll clean it up in dealloc. Now let's go through one at a time pasting methods from that other file where gesture recognition and gesture handling were tightly coupled. So first, touchesBegan. Can we use this exactly as it is? Well, nearly. But we need to change event touches for view to something else.
Event touches for recognizer is a better fit for us. And so if I just paste that in we're done with touchesBegan. Another really has to change. Note, however, that if we were to try to do something like call super touches begin, it would have absolutely no effect here. Whereas in the UIView subclass it might do something interesting with the responder chain. OK, so that's touchesBegan. Let's go into touchesMoved.
Well, similarly here, I'm using event touches for view. Again, that's not really appropriate for the gesture recognizer. I could pull the view property off of my gesture recognizer. But what I really want is touches for gesture recognizer. And while I'm creating incremental transform variable on the stack here, there's no reason I can't just directly assign it to my instance variable. So I'll go ahead and do that. And this key method -- sorry, this key line, self.transform, that's the tightly-coupled handling from the view subclass. We don't want to do that here. So let's just remove it.
And I could stop here and continue on to touchesEnded. But I want to point out that it's our job to update the state. So the very important thing I'm going to do here is update my state so that the gesture recognizer is recognized. If I'm in state possible, then it's probably appropriate to move to the began state now. I'm going to make sure first that it's not the identity transform, if that's the case then it's not really a transform gesture recognizer, a transform gesture yet.
And if I'm not in the possible state then I must already be in a recognized state. So let's just update the state to changed. And that's it for touchesMoved. So now I'm going to paste in touchesEnded. And again, I've got two places where I'm using touchesForView. Instead, I'll use touches for recognizer.
And now I need to be very, very careful that I always get into a terminal state. If I have just implemented the state transitions in touchesMoved I'd be left with my target action pairs firing continuously. So I need to go in and say if I'm out of the possible state already and if my remaining touches have dropped to zero, well definitely go into the state ended. And this is going to allow a lot of things, like touches to be delivered, cancelled, and it will allow for automatic reset.
Crucially important. And let's just paste in touchesCancelled. And here we're calling through directly to touchesEnded. We could go ahead and set the state to UIGestureRecognizerStateCancelled instead. But because we have some knowledge of how the transform gesture recognizer is going to be used it's not really reasonable to set it back to the cancelled state.
So it's fine to leave it like this. So I've pasted over my UIResponder methods. Am I done? Well, if this were a responder, that really would be it. I could fill in the helper methods and there would be nothing further to do. But because this is an UIGestureRecognizer, I can't rely on receiving the entire sequence of touch input. So it's crucially important to implement the reset method. And I'll do two things here.
I'm going to clear out the dictionary mapping touches to point, and here I'm just using a block enumerator to free some objects I put on the heap, some points. And I remove values. And then I'm going to set the transforms back to identity. And while I'm in the neighborhood, let's just quickly implement the transform property. The getters is going to take the original transform and concatenate incremental transform and setter is just going to replace the original transform with the new transform and throw away the incremental.
OK, so now all that remains is to add in the helper scaffolding. I'll do that now. So we still want this convenience category on UITouch that makes it easy to sort. We want this -- this method, incremental transform with touches, is the meat of the math. It takes those two points in one coordinate system, takes two points in another coordinate system and comes up with a transform.
So I'm just going to go through here and update location and view to use location and view, self view super view. Notice what I did, we used to be relying on self super view. And that's no longer appropriate, because this is a gesture recognizer, not a view. But we have access to the view.
So self view super view, and similarly, we don't want center on ourselves, we want center of our view. And none of the other math has to change, we're done here. So continuing to add these helpers, well, this actually is an update, original transform for touches. What we're doing when a second finger or a third finger comes down on the screen we're kind of recentering the transform.
If you saw the previous example in the last session it's like re-- adjusting the anchor input so we're going to go here and do something similar, but we don't want to set the transaction form on ourselves. We want to -- this was previously updating in the view itself. So we're going to go through here and yes, compute incremental transform. We're going to update the original transform and we'll then set the incremental transform back to identity.
So couple of other helpers. We want to store away the begin points. Nothing changes except we want to do this in an actual view, not in a super view property that doesn't existent on UIView gesture recognizer. And we want to remove touches from the caches at certain points. And there's absolutely nothing here that needs to change. So there's still a lot of code in this class. Most of it was just copy, pasted blatantly. But now we have an object that is reusable and can work in a wide variety of views.
So let's go back to the View Controller and start changing things. Well firstly we don't need touch image view any more. So it's kind of satisfying to go and delete this, although we've really just migrated most of the code. So any implementation will change what we're importing to be our new transform gesture recognizer. And then here where we're allocating touch image views we're just going to use vanilla UIImageViews. Note that I'm setting the user interaction enabled property to yes, because UIImageView's default to not being interactive, and so the gesture recognizer wouldn't receive it.
OK, do the same here. And do the same here. And I'm going to attach a gesture recognizer, at the same -- well, gesture recognizers of the same type to each of these views. And I'll add a helper method to do that. So assuming that helper method exists, I'm just going to go through down the line, add transform gesture to view.
And let's go ahead and actually add that implementation. And that just allocates a transform gesture recognizer, sets the target to self, the View Controller, and gives a handle transform action method. It adds it to the view and releases it, because it's retained by the view. OK? Let's actually implement that handler.
Well, what we saw before in the tightly-coupled code was we just took a transform and applied it straight to the view. And we can pull the transform out of the transform recognizer, and that works great. We could just set it here. But what we'd really like to do is concatenate it to the initial view state. We don't want to keep resetting back to zero.
So let's look at the state and if it's in the began state pull out the transform, concatenate it to the original transform, and then set the transform again. So we're using that writable property to really not store additional state in this View Controller. And then finally we just set -- the view -- gesture recognizers views transform to that transform. So we've done all this work, and we're working with UIImageViews now instead of touch image views.
And we get you know, basically the same behavior. And it's like, well, why do you go to this trouble, it worked before, what's the point of doing this. And I think you know what's coming, as in the previous example in the last session, we can now add this gesture recognizer to other things. So I'm going to take this View Controller's view and add the gesture to it. And let's see what happens. OK? And now I can translate the entire thing, I can pinch the entire thing, I can pinch and scale and rotate the entire thing, and specific subviews.
What happens when I put one finger or one mouse in one view and the outer view, and another in an inner view. Well, it actually translates. Which wasn't exactly what I expected. And to understand why this is happening, you need to remember the rules of gesture recognizer precedence.
And so the deepest gesture recognizer in the view hierarchy, the one that's a leaf node, will get precedence for this touch, even if they recognize at the same time. So what we can do is go back to our gesture recognizer and in touchesMoved, we're already doing some work to make sure we don't transition to the began state if it's an identity transfer. We can make it a little more judicious in transitioning to the began state.
So if this is merely a translation transform then we can say well, it has to pass some threshold. And that's not a bad idea, because UIGestureRecognizers that automatically go to the began state as soon as touches move are going to recognize almost immediately. It's like recognizing as soon as any touch comes down on the screen. And so we'll add this additional filtering and go and look at the done up, and now I can do things like pinch the outer view, translate something, translate views independently, but rotate views when I've got fingers, two fingers down. And things just work.
And I've completely decoupled my implementation and handling from the transform gesture recognizer which has this really convenient property. So now I can go ahead and add Lightroom behavior to any view I can conceive of. OK, and that's really it for the demo. So I'll turn it back over to the Josh.
[Josh Shaffer]
All right. Thanks, Brad. So I hope that that kind of gave you a feel for the fact that it's actually really easy to take the existing code that you've got and migrate it into a gesture recognizer. The only real additions Brad made were the movement through the states, and the only changes were to change those couple methods to touches for gesture recognizer. So if you want more information, Bill Dudney is our Application Frameworks Evangelist, there's plenty of documentation on UIGestureRecognizer, and obviously the Dev Forums are there, we answer questions as much as we can and it's a great place to help each other.
There's some related sessions, obviously right before this we had Simplifying Touch Handling With Gesture Recognizers, tomorrow there's a Mastering Table View Session at the same room at 11:30 a.m., and they're going to be talking about plenty of stuff with Table views, which really cool, but also getting into some of how you might use just the recognizers inside of Table view, which is also really cool. Thanks.