Transcript
>> 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 discrete gestures.
Gestures that happen as a result of some action at a
discrete point in time and do not update over time.
So a tap, a swipe, these things are discrete.
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 discrete 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 discrete 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 ivars 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 discrete 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: @interface
SimpleTapRecognizer : UIGestureRecognizer.
And we don't need any ivars, 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 gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: 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 canPreventGestureRecognizer 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 ivar I want to add is the ivar 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.