iPhone • 54:09
Get a strong understanding of the Multi-Touch technology built into iPhone OS. See the path that touch events take through the user interface and how to properly pinpoint an event's origin. Learn to recognize common gestures and make use of new accelerometer events to give users the intuitive experience they expect from an iPhone application.
Speakers: Josh Shaffer, Jason Beaver
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
My name is Josh and I'll be joined shortly by Jason, and we're engineers on the iPhone Frameworks Team, and we're going to spend some time talking this morning about some new ideas we've had about handling Multi-Touch events and gestures over the course of the last year. We've all had some time now to play around with UITouch and UIEvent and we've got some new things to talk about and some different ways that you might be interested in handling events and gestures. And so, we'll get into that in a little bit.
We're going to start out with a bit of a review from last year. For those of you that were here the first little bit is going to cover basically the same thing that we talked about last year. For those of you that are new, this will be new information obviously, but not quite as in-depth.
So feel free to go back and check out the session from last year. As soon as we're done with that, we'll start talking about what's new in iPhone OS 3.0 in UIEvent, and talk about the new EventTypes and Subtypes, and Jason will get into more depth about that later on. And then, we're going to start talking about this new stuff that I've been referencing here.
And we're going to talk about encapsulating your touch handling, sort of taking multiple touches and handling them in separate objects as sort of discrete gestures, and kind of trying to get away from the really large complex state machines you might have to have been building over the last year in order to handle differentiating between a pinch and a rotation.
We'll talk about ways that you can handle that And finally, Jason will come up and talk about some best practices and some Gotchas, some issues that we've seen people having over the last year, and some things that maybe we can do better in the coming year. So, start out with a little bit of a review. What is a Touch Sequence? So touch starts when a finger comes down on the screen, continues while the finger is moving and finally ends when the finger lifts, that's pretty basic. So what's happening in your application and what do you get from UKit during this.
Well, when the finger comes down on the screen, we create a new UITouch object. And this UITouch object, this same instance, is going to be used to represent this finger for the entire touch sequence. So as your finger moves on the screen, we're going to update this one instance and continue to deliver messages about that touch using the same touch instance. The implication there is that you can use the address of this touch as a unique key to associate data with it.
So if you needed to associate, for instance, the point at which our touch began, at the beginning of its touch sequence, the touch doesn't remember that on its own, but you could associate that with a particular touch pointer that you get back in touchesBegan. So, we've created this new UITouch. Its phase is Began, and we're going to update the phase as the finger moves.
And it has a location. So now user has put their finger down. We've created this touch. We'll talk about where this gets delivered in a little bit, but what happens is we call touchesBegan withEvent on the target view, and we'll pass in-- this thing will take two parameters.
It will take an NSSet, which is the set of touches that have began since the last time we told you about touches, and an event which contains more information about all the other touches, and we'll talk about that in a minute too. So as the user moves their finger, we're going to update the touches phase to Moved, it might have a new location. Well, obviously if it's moved, it will. And we'll deliver it with touchesMoved withEvent.
And all of these touch delivery methods are going to have the same basic parameters they will have an NSSet of touches, these are just the touches that have moved, even if there's other touches, they won't be included in the set unless they're moving, and the event. So as the user continues to move their finger, we'll continue to update the same touch instance, it will still have the touches moved, UITouch phase moved in a new location, and again, we'll call touchesMoved, same things as it happens again. And finally, when the finger lifts, we'll update the phase to Ended, change its location, and call touchesEnded withEvent. Now that's the basic normal series of things that happens on a touch.
And one thing that we've seen over the last year is the feeling that maybe touchesCancelled which is the fourth member of this set, is optional. And we'd really like to stress that actually touchesCancelled is not optional. If you're implementing touch handling and you're trying to look for the entire sequence of a touch, it may not actually end. It may instead be cancelled at some point.
And there are a few reasons for that. It can happen if a phone call comes in, if the user turns off the device while touches are on the screen, if the user puts down more than five fingers, we actually end up cancelling all the touches 'cause we can't track more than that. So there are actually quite a few reasons and if you're using scroll views, you'll have touches being cancelled inside the scroll view as well. So if you're implementing touch handling and not getting touchesEnded called, make sure that you're checking for touchesCancelled as well.
So we got a little sample of that too. User's finger comes down, and we create a new UITouch, its phase is Began, it has a location. As the user moves their finger, we update the phase to moved, and the location changes, but now, we got a call from John Appleseed. And so we're going to cancel that touch at that point, we'll set the touches phase to Cancelled and we'll call touchesCancelled withEvent, and there are two LLs in that not one.
So make sure you type that one right. Alright, so UITouch, that's the basic thing that represents a finger on the screen. But this is a Multi-Touch device, and there may be multiple fingers on the screen at any one time. So we actually have this other object, UIEvent, which knows about all the touches that are on the screen, regardless of where they are or what phase they're in. And that's as we saw just a minute ago also delivered with all the touches. So the UIEvent, as I said, knows about all these touches, and every touch is associated with a particular window, and the event knows what window each touch is associated with.
Each touch is also associated with a particular view, and the event knows that mapping as well. So you can ask the event for information about these touches. You can ask it for all the touches, and it will give you back the set of all the touches that are currently active anywhere on the screen, regardless of what window or view they're in.
You can ask it for all the touches for a particular window, so all the touches in window A will return the set of touches that are in Window A. And finally, you can ask for the touches in a particular view, which will return for instance, the touches in View A.
[ Pause ]
Alright, so with UITouch and UIEvent, those are the most basic building blocks on which all other Multi-Touch processing is built. There is one other class in UIKit that provides a bit of a higher level view on these things. If you don't want to deal with UITouch and UIEvent directly, and all you're looking for is some of the basic types of information that UIControl might be interested in, you might be able to use UIControl instead of having to subclass UIView directly. UIControl is a subclass of UIView, and it just-- it implements the touch processing methods and provides this higher level view of events.
So we're not going to cover this too much, but we'll quickly talk about some of the things that you can ask the control for information about and you just register a target action so any object in your application could be notified of these particular events. So you can find out when a touch comes down in that control, you can find out when it comes down repeatedly, which is the same as the tap count property on UITouch.
If the user is tapping, you'll get touch down repeat. You can ask it for information about the user dragging their finger within and outside of the control. You can find out when the user is dragging and they enter or leave the bound of the control, and you can find out when the finger is lifted either inside or outside the control.
And finally, you can also find out if touches are cancelled, because touchesCancelled is actually quite important. Alright, so those are the basic building blocks and that's pretty much the full review of what we covered last year in a bit of a condensed format there. So I'm going to do a quick Multi-Touch demo. For those of you that have checked out the Multi-Touch sample code on developer.apple.com, this is basically that. So we're going to go through it pretty quickly.
You can check it out after the show as well, and if you were here last year, you've probably already seen most of this. So let's see what we got. We basically have the Xcode project template for a new application. So I've got my Multi-Touch demo application delegate that you get just right out of the box. There's really nothing custom in here.
I've got a ViewController that you get, there's really nothing new here yet either. And we've just implemented View DidLoad to call superview DidLoad and then set the background color of our view to white. So let's get started. The first thing that we're going to do is add a little helper method up here which is going to create our images. If you saw in that video that we had up there, there was basically-- we're going to have three images, and we're going to let the user drag them around and rotate them and pinch them and zoom them.
So I've got this helper method that's just going to take an image name which is going to be the name of an image that we have in our bundle, and a center point where we want to place that image on the screen. So we're just going to calculate the rectangle that we want to end up showing that image in, and create a new instance of a Touch Image View which we're going to write in just a minute. This is a subclass of UIImageView that we're going to add touch handling to so that it knows how to rotate itself and translate itself and scale itself.
So we'll set our Image View's image, set its center point so that we position it on the screen, add it as a subview of our ViewController's view and release it because we don't actually need a reference to it anymore. So now we've got that helper method written. We're just going to call it three times with three different images and three different center points.
So let me get a stack of three images down the screen. So that's all we really have to do on our ViewController. We've created our views. And so now, let's go implement touch Image View. I've already defined two instance variables in the header. The first is this original transform which is a CGAffineTransform.
If you're not familiar with this, it's just a Core Graphics data structure that can represent a rotation, a scale, and a translation. So we'll use that to store basically the baseline transform that we're going to modify as we get touches. And then we'll also need to store the touch begin points, 'cause as I mentioned earlier, UITouch doesn't remember what position it originally began at. So if we need that information, which we do here, we're going to save it off in a mutable dictionary that we'll use to map the touch instance to the point at which it began.
Alright, so in our implementation, not much here yet. We just have an initializer that's called super and dalloc to release our touch begin points. So let's start out by initializing these data members here. We're going to set our original transform to the identity transform which means no scale, no rotation, and no translation. And we're going to initialize our begin points with the new CF mutable dictionary that we use to store this mapping. One other thing that we have to turn on here is the Multi-Touch and user interaction enabled properties.
So user interaction enabled is by default YES for UIView but for UIImageView, it's actually default-- its default value is NO. So if we don't turn it back on, we'll actually never get any of the touch delivery methods called on our image view, so we wouldn't see anything, any touches at all. And we'll also going to turn on Multiple Touch. And the default value of Multiple Touch is NO for all views.
So if you need to be able to process multiple touches, you'll have to turn this back ON or your touch methods will only ever get one at a time. Alright, so we've got those two things turned on. Now we can start to implement the actual touch handling methods themselves. So we're going to start out with touchesBegan. The first thing we want to do is get the set of touches that were already in existence before the touches that are beginning now.
So as I mentioned the touch set here that we're getting and touchesBegan is the set of touches that are beginning but there may be other touches active in our view right now. So we can find out about that from the event by asking the event for all the touches that are currently in our view. And from that we can subtract out the ones that are currently beginning and that leaves us with the ones that were there before.
Now the reason we want to do this is because we're keeping that original transform which is basically the transform that we're going to apply an incremental transform to as we detect new movement. And every time that we get a new touch starting or a touch ending, we're going to re-baseline this so that we have a new original transform to start from and our incremental transform will be from that point up until whatever the current movement, set of movement is.
So we've got the touches that were already in existence. So we'll calculate a new baseline transform from that and I've got a helper method that does that. There's a bunch of math here, you can check it out in the sample code after. It's basically solving a system of equations, the calculator rotation and a transform and-- I'm sorry, a rotation, a scale, and a translation from the touch points.
But we're going to kind of gloss over that at the moment. So we've re-baselined our transform, we also want to re-cache the touch begin points. 'Cause now that we've calculated a new baseline transform, all of our touches are going to be-- sorry, our incremental transform will be calculated from this new baseline.
So we want new began points for all of the touches that already existed. And we're going also cache begin points for touches that are just starting now. Alright, so that's touchesBegan. Now we can do touchesMoved which is where we're actually going to calculate any new transforms and apply it to the view. So we'll start implementing touchesMoved and calculate this incremental transform I was talking about.
So I've got a helper method that does this as well and it basically takes the touch begin points that we've got in our touch begin points dictionary and the current point of all the touches that we're going to get back from the event by asking the event for all the touches in our view, and it'll calculate the rotation, the scale, and the translation based on the difference between those points and return that as a new CGAffineTransform which is the incremental transform from what we cached as our baseline. So we can then set our views transform to the concatenation of our baseline transform plus this new incremental transform. So that's began and moved.
Now we'll implement touchesEnded to clean up a touch list. So we-- as I mentioned we're going to re-baseline our touches now since the number of touches in our view is changing and we're going to re-baseline it based on all the touches that are currently in the view. And then we're going to remove from the cache any touches that are ending because we're not going to be notified about these touches anymore. They're done and we don't want to end up leaving them in our touch cache or we'll have some stale data and we'll end up leaking memory and all matter of bad things.
So we want to clean up our cache by removing those touches and re-cache any of the remaining touches. So we're going to do basically the same thing we did in touchesBegan ask the event for all the touches in our view and subtract out the ones that are ending and what we're left with are the ones that are still going to be active in the view.
So we're going to re-cache any begin points for those since we've now re-baselined things. And additionally-- and then we're done with that touchesEnded, I think [phonetic]. Now as I mentioned touchesCancelled is also a very important member of this four-member family here. So we also want to implement that.
Now generally speaking, when you get touchesCancelled, the idea is that you don't want to perform some action as a result of receiving touchesCancelled because some event has happened that has caused the user to not perform whatever action they were starting to perform. In our case, we're just dragging things around on the screen. So it's pretty reasonable to just drop them in place where they were. So we're actually just going to call touchesEnded on ourselves to do that. Now this is fine in this case because we don't end up calling super touchesEnded up here.
If our touchesEnded implementation actually called super, this could potentially be a pretty big problem because our superclass or potentially superview would end up getting touchesEnded for a touch that's actually cancelled and then it could get out of sync, it would not actually know that touch was cancelled. We wouldn't really want that. But since we don't call super, all this code is self-contained.
We're just going to call touchesEnded and kind of take the cheap way out right now. Alright, so with all that done, we're just going to build and run and now we've got a bit of a crash [laughter]. Let's check out what's up there.
[ Audience Remark ]
What's that?
Yeah that's,-- yeah, oh hey we're building against the 2.0 SDK. That's probably not what I meant. Oops, I lost the window. We'll look at this real quick and if it's not obvious we're going to move on but -- did you look image name center.
It does not exist, eh. I think it was the SDK that was our problem. Yeah. Alright, so this time for sure. We've got this image that we can drag around and we enabled Multi-Touch support and we're calculating the rotations and scales so we can put two fingers in one and we can rotate them around and scale them. And we can also put two fingers in different ones and move those around.
[ Pause ]
[ Applause ]
Alright, oops, what did I do with my remote? There you go. Oh hey, this is the example of what you're going to see. I hope I didn't ruin it. Alright, oh and we're going to see a demo. Alright, so [laughter], now let's talk about what's new in iPhone OS 3.0 regarding UIEvent. So we've got a couple of new things here. We've got a new EventType property. So there are actually two EventTypes that are possible now.
Everything we talked about so far, the UIEvent is a touches Event. It knows about the touches that are happening on screen and it's being sent to you to report touches beginning, moving, ending, or being cancelled. So that's type UIEventTypeTouches. But now there's also a new type, UIEventTypeMotion. And UIEventTypeMotion is used to deliver information about physical movement of the device to your application. Jason is going to talk a little bit more about the specifics of this later. So we're going to gloss over it at the moment.
There's also this new UIEventSubtype property. And the subtype is currently either UIEventSubtypeNone which all UIEventType touches are subtype none or UIEventSubtypeMotionShake and currently all motion events are subtype motion shake. So, you know, pretty easy to know what's going to happen but do check it out when you're using it because things could change in the future.
Alright, so we've talked about touches. We've talked about events and we know what they are and we kind of saw that, we're getting events delivered to our view but we didn't really talk about how we decide what view gets the events. And things are a little different when we start talking about motion events. So let's look at that. For those of that have been working on this, you're familiar with UIResponder. A lot of objects on our system are sub classes of UIResponder. UIView is a subclass of UIResponder. UIViewController is a subclass of UIResponder.
And anything that's a subclass of UIResponder can implement and receive any of these touchesEvents-- sorry, touch delivery methods that we were just talking about. But how do we figure out which object in your application is the one that gets it? Well for UIEventType touchesEvents the UIWindow will call hitTest withEvent on all of its subviews trying to figure out which one is the one that needs to handle this event.
So the default implementation of this makes sure that the touch is actually within the frame of the view and that the view is not hidden that it has an alpha that doesn't make it fully transparent and if all of those conditions are met, it will return itself or possibly one of its subviews if some subview is also the hit view.
So yeah, sorry, this is recursively called through the view hierarchy to figure out the deepest view that gets hit. And the view that gets returned from this, the deepest view on the view hierarchy, is the one that the touch gets bound to and all further events for that touch will be delivered to that view. But things are slightly different with motion events.
Your finger may not even be on the screen for a motion event. If the user is shaking the phone, they probably don't have a finger down. We can't just hitTesting to figure out where we need to deliver that. So for motion events, we actually send the event to the first responder. I'm not really going to talk about this right now. Jason will cover that later on when he comes up.
But the important thing to remember is that for touches events, we'll send it to the hitTest view, and for motion events we'll send it to the first responder. So what happens after we've delivered it to the first responder or the hitTest view?
Well, we're going to assume for the purposes of what we're going to talk about right now that this view here is both our hitTest View and our FirstResponder. So it's going to get all motion events and also the touches events that we're talking about now.
So, we call touchesBegan, Moved, Ended, and Cancelled on this view. If this view doesn't handle them, then it will get passed up the responder chain to that view's ViewController. And that view's ViewController will have a chance to look at these touch events. If the view did handle them, it would not get passed up the responder chain unless you had called super.
If the ViewController doesn't handle them, it will get passed up to the view's superview. If that view doesn't handle it, its ViewController gets a chance. If the-- and again, up the view hierarchy-- sorry, up the responder chain which is equivalent of the view hierarchy in this case, then over to the, that view's ViewController.
If that ViewController doesn't handle it, it will go up to the window. And if the window doesn't handle it, then the UIApplication object will have a chance. Now, generally speaking, your application won't have quite so many ViewControllers imbedded. So it'll usually look a bit more like this. But the important part is view to superview to superview, any view's ViewController in there will get a chance to get it, window, application.
Alright, so that's the event delivery thing. Now, you might be thinking here, there's one view that actually gets to handle these events. But what if I want to handle something like a pinch, and the two touches start in different views, but-and so they're hitTested to different views, where do I want to put my logic to handle this? It becomes a little bit tricky and you have to start trying to coordinate between these views, and you might end up with some pretty complicated state machines to track whether you mean move one view, or a pinch means, you know, scale the whole thing. It starts to get a little bit tricky and so let's talk about ways that you can solve that. By grouping these touches together, and starting to treat them as more of a gesture than individual touches in separate views. So, we've got the basic case.
This is the really easy one, alright? You've got a single touch and it's happening in one view. That view has been hitTested. You put your logic in there to handle touches and everything's great. No problem. We can put that one touch there, drag around this one view, everything is easy.
It's not even that much harder when we start talking about Multiple Touches if those Multiple Touches are in that one view. We just saw doing this, we can put all that logic in one view, and it's all pretty easy. And the same thing with Multiple Touches in separate views, if what we mean is what we just saw with those two touches meaning, move those two views around.
But things start to get a little more complicated if we start wanting those two touches to work together. And instead of moving those two views, we want them to actually scale everything. So, where do we want to put this logic? Because we still want each individual view to track one touch in it as an attempt to move that view. It's just that when two touches come down in different views, we want to start scaling everything. So we've talked about these Event Delivery Methods. We've got touchesBegan, Moved, Ended and Cancelled, and those are sent to the FirstResponder, sorry, the hitTest view.
But what we didn't talk about is where these things get called from. So UIWindow actually has a method called sendEvent. And sendEvent is basically the funnel point through which all of these UIEvents get passed, and sendEvent breaks them down into their individual touches, it looks and sorts them by phase and by view and calls the touchesBegan, Moved and Ended methods on each of the individual views.
And that is in turn, called by UIApplication sendEvent which passes the event up to whatever window needs to see it. So we can take advantage of knowing that sendEvent is this funnel point, and override that to take a look at all the touches before they're even delivered to any of the views. You want to be careful if you're doing this though. You subclass UIWindow and overwrite sendEvent, it's a great place to observe touches and watch them as they're going by.
Not a good place to try and control delivery. The touches are delivered in one event and there may be touches bound for multiple views and different phases. If you implemented sendEvent and didn't call super in certain cases, some views in your application might end up seeing only a partial touch sequence and ending up getting out of sequence.
So if you override this, always do call super sendEvent. Alright, so let's take a look at what we've got now. This is our current application that we just built, right. We're going to sketch it out a little bit, got our UIApplication object and our UIWindow object. We've got our ViewController's View which is containing these three separate image views. So as things are working right now, in what we just wrote, the UIApplication sendEvent is the first thing that gets to see this event.
That in turn calls sendEvent on UIWindow. So sendEvent on UIWindow then breaks these things out, and calls touchesBegan, Moved, Ended and Cancelled on all the appropriate views after filtering the touches out for what view they're actually bound to and sends them to the appropriate view. And that's great for what we were trying to do. But that doesn't really work so well for what we're trying to accomplish now.
So, come on. Yeah, yeah, OK. You know what, that might be a different game. Let's skip that for now. OK, so let's shrink this down now and add another object next to our touch-- our Image View. We're going to sort of attach this helper object on the side, and we're going to call it a transform gesture. We're actually going to implement all of our touch handling logic in this separate view.
And now, we can also take the same object that we're going to define and also attach one alongside our ViewController Container View. But UIApplication and UIWindow isn't going to know about these separate objects that we have. You can't hitTest something that's not a view. So it doesn't know to send any touches to these things.
So we're actually going to subclass UIWindow and overwrite sendEvent. So what will happen now is UIApplication sendEvent will end up calling sendEvent on our UIWindow subclass. And from there, we can do some filtering ourselves, and figure out what touches we're interested in for what location, and pass them to our helper objects.
So our subclass of UIWindow sendEvent is going to end up calling touchesBegan, Moved, Ended, and Cancelled on all of our transform gesture objects, and then all those things will have a chance to look at whatever touches they're interested in. Then we're going to call super which will pass the event back up to UIWindow sendEvent, and UIWindow sendEvent will just continue to do the default behavior and call touchesBegan, Moved, and Ended on the appropriate view that all the touches are bound to.
So we can look at all the touches before they even get delivered to the views, and decide if we want to do something that's going to be more global than we would have been doing in the actual touch handling method of the view itself. Alright, so let's take a look at how we're actually going to implement this and what it's going to look like.
Alright, so I've got basically the same class that we had here before, sorry, the same project. I've made a few small changes already. We still have the same app delegate, nothing new there. But now, I've already created this transform gesture class, and I've added a few things. So I moved over the same two instance variables that we had in our Touch Image View before. So we've still got the original transform and the touchBegin Points instance variables but I've added a couple more things.
So we've got another transform which is the current transform. So if you remember from a little while ago, in touchesMoved, we would set the current transform on the view to the concatenation of the original transform and the incremental transform. And basically, the view is storing that and changing its bounds based on it, I'm sorry, changing its frame based on it.
But now, we're no longer a subclass of UIView, this thing is just a subclass of UIResponder, and UIResponder is a subclass of NSObject, so we're not a view. So we don't have a transform property. So we're going to have to have some place to store this. So we'll create this new transform instance variable and define a transform property so that we can store the actual current transform. And then, we've got two new things, we're going to define a target action. So our transform gesture object that we're about to write is going to-- every time it updates its transform, call some action method on a target object to tell it that this transform has changed.
Additionally, we want to associate this transform gesture with a particular view so that we can sort through the touches in the event and figure out which ones we want to look at. So we've got that. We're defining just a couple of accessors for those things, and additionally, a new initializer that will set up our target action and view. In the actual implementation itself, I've basically just copied over what we had from the Touch Image View before.
The only other thing I've added is this set of initializers in initWithTarget to set up these variables based on what we've passed in. But still, initializing the original transform and touch begin points, the same way we were before, and dealloc still the same. I defined a few accessors but they just pretty much returned the instance variables we just created and I still have the touch handling code, totally unmodified from what we had in the view.
So we do have to make just a few changes, but actually nothing too big. The first one is our touch handling code was before asking the event for all the touches that were in the view itself, but we're no longer a subclass of UIView, so we're no longer a view. The event doesn't know that there are any touches bound to us. So instead, we're going to look for all of the observed touches.
Now, I have implemented a method on this transform gesture recognizer already and we're not going to look at it too much because your logic for what touches you're interested in and any particular gesture will probably be different than mine. All mine really does is look through all touches in the event, and find any touches that are bound to this view or any of this view's subviews.
So that is going to return all of those and we're just going to replace that in the three other places where we were calling touches for views self. So we're always going to get all the touches in our view or any of our subviews.
Alright, so, with that done, now we've got one last change to make. As I mentioned, every time that our transform is updated, we're going to call that action method on our target to let it know that we've updated our transform so it can perform some action based on that.
So, in our touchesMoved, we're just going to tell our target to perform the selector action which we saved off at the beginning with our self being the object so that it can get back and find out what the current transform is that we're saving off here. We've defined a new transform property that we're saving in our instance variable so we don't even have to change that, we're still good to go.
That's actually the only changes we have to make to the touch handling code. Other than that, it can remain exactly the same as what we had when it was in the touch image view subclass. So, let's move over to our UI window subclass, our EventObserverWindow. I've defined a new instance variable that is an array of transform gestures.
So, we're going to allocate all of these transform gestures and associate them with particular views, but we want to tell the window about them so that it knows that it needs to send events to all of these objects. And to do that, we're also going to add two-- sorry, two methods, the add transform gesture method to add something to this array and another one to get all the gestures back.
So, our implementation of our subclass view window right now, I'm loading it from a NIB so I've implemented a wait from NIB to allocate our mutable array. You might also have to implement, excuse me, initWithFrame if you create your window programmatically and in dealloc we'll just release that. So now, we're going to implement those two methods, the add transform gesture.
It's just going to add the transform gesture passed in to the array that we have as our instance variable. And the accessor that returns all of the gestures, we're just going to create an auto-released copy of that. So that-- since it's mutable, we don't want to return the object itself in case it's mutated and someone that gets it holds on to it.
And now, we're going to implement our sendEvent override here in our subclass. We'll call super but we're not going to do that yet. We're going to look at all of the touches before we let our subviews take a look at them. So, the first thing we're going to do is iterate all of our transform gestures that we have saved in that array.
So we're going to look at all of those. And for each gesture, we're going to find out what touches that gesture is interested in and we've already defined a method that knows how to do that. So, we're going to ask the gesture for the observed touches for event. That's the exact same method that we were calling from within the touch methods on the gesture itself, and it'll return again, all the touches on that gesture's view or on that gesture's views subviews.
Alright, so, we've got that. Now, UIWindow sendEvent normally handles all of the dispatch for us. It calls touches Began, Moved, and Ended after sorting all of the touches. But since, we're going to be doing this dispatch to our own objects that aren't in views, we have to do that sorting our self.
So, we are going to create these four sets to store off our Began, Moved, and Ended, and Cancelled touches and then we'll just start sorting all the touches that we're interested in. So, we're going to iterate all over the touches that the gesture told us it wanted to know about. We are going to switch on their phase and start putting them into these buckets.
So, for the began touches that are in phase began, if we haven't yet found one, we're going to create a set to store them and we'll add that object to it, and we'll do the same thing for any touches that are Moved, Ended, or Cancelled. Now I've got a default here because there is one phase we didn't really talk about, and that's UITouchPhaseStationary.
So any touch that we've already been telling you about but that didn't move during the last event cycle, is going to get put into the stationary phase. So, it'll still be in the event, and the event will know about it, but it won't be in any of these four states, and there's no corresponding touch stationary UIResponder method to tell you about it with.
So, we're just going to basically drop those on the floor. The view will be able to find out about them or the gesture object by checking the event. And finally, now that we've got these all bucketed and sorted, we can tell the gesture to run its code. So we'll call touchesBegan, Moved, Entered or Cancelled on each of the gestures.
So we're now, iterating over all the gestures, sorting the touches, and calling the UIResponder methods on those things ourselves. Since they're not actually in the responder chain, they're just a subclass of UIResponder but have no next responder, once we deliver them to the gesture, they're pretty much-- the gesture will either handle them or be done.
So now that we've done that, we can call super sendEvent, and that will let UIWindow do its default behavior of sorting the touches as it normally would and sent calling touchesBegan, Moved, Ended, and Cancelled on all the views that actually these touches are bound to. Alight, so now let's go into our ViewController. This is the exact same ViewController that we had in the last demo. It hasn't been changed at all yet.
We're still creating our touch image view and setting its center in our little helper method here. Now, the one additional thing we want to do now is start to allocate one of these transform gestures for each of the touch image views that we're creating. So we're going to allocate a new transform gesture with target self where self is the ViewController. This is our main-- we only have one ViewController in these apps so this is the one ViewController.
We're going to set an action method for this gesture that will get called every time the gesture updates its transform and we'll call that the handle subview transform which we'll implement in just a second. And we're going to say that the view that we want this gesture to be associated with is the touch image view that we just allocated.
So with that gesture created, we can then tell the Window-- and I did add one thing, I forgot about that, sorry. There's a new IB outlet in our ViewController here in the header which basically just points back to this window. So we have a pointer in here so that we can call add transfer in gesture on the window.
And then we'll release the gesture because we don't need a pointer to it in the ViewController anymore. The window knows about it and it's going to call things on it and the gesture will call back on us when necessary. Alright, so now we've got these gestures back on each of the subviews so each of the subviews on that will be able to transforms.
But we also want a global one, one that's going to be tracking touches on anything on the ViewController's view or any of the subviews. So one last thing we're going to do viewDidLoad [phonetic] is allocate one more transform gesture, same target our ViewController. We're going to have a different action method for this one. It will be handle superview transform and its view is going to be self.view, the ViewController's view.
So this one is going to see any touches that are bound to the ViewController's view or any of its subviews. And again, we're just going to tell the window to add that transform gesture, I've auto-released it so it doesn't get leaked, just kind of put that code on one line there. Alright, so now we've got the gestures allocated and we've told them that they have action methods on our ViewController, but we haven't implemented those yet. So let's implement these two action methods. We've got the handle subview transform and handle superview transform.
So subview is the one that's going to get called for touches in any of the subviews and superview for the ViewController's view. So the first thing we want to do is figure out whether we should apply the transform as a result of finding out about it, because the subview is going to get-- the subview transform is going to get-- we're going to have this action method called even if there are touches in two different subviews that we want to actually treat as a global thing. So I have a helper method I've written that I'm not going to go into again here because as with the-- some of the other things we've talked about, the logic in your application may be a bit different.
This is basically where you have to do your own disambiguation. You have to decide whether this gesture is the thing that actually you want to do or if there's some other gesture object somewhere that's-- that that is the thing that you-- that you'd rather have handle this particular touch. So the logic in this is basically, if there are two touches in different views, then don't apply the transform for the subview. Otherwise, we will apply the transform. So we're going to get the gesture's view and set its transform to the gesture's transform.
If we've decided not to do that, then we want to make sure to tell the gesture to re-baseline itself because we haven't applied the transform to the view but the gesture still knows about the new incremental transform. So the touches that moved you want to re-baseline for where it is now if we haven't applied it. And we're going to do basically the same thing in the handle superview transform except there's a different disambiguation function that I wrote, that should apply superview transform for gesture.
And this one basically just says, if there are two touches and if they're in different views, then do apply the transform. Otherwise, don't. And if we've decided that we want to apply that, we're going to set the ViewController's views transform to the gesture transform. And if not, again, we're going to re-baseline the transform so we'll just tell it to reset itself. So let's build and run that and I do this every time. I told Jason before that there's going to be one error and I forgot it again.
[Laughs] So I forgot-- there was one other thing that we didn't really talk about. I didn't write this in the first demo but it's in the sample code. And I was going to talk about taking it out and putting it in the other place and I forget to do it every time. So let's talk about it now. touchesEnded on our transform gesture has this leftover code that we used to have in the transform view itself, the transform image view, and this was trying to look for double taps.
And if there were double-- if there was a double tap in the view, we were going to have that view come to the foreground so that you could control Z-ordering. If something was behind another, you could double tap in it, it would come forward. But this is no longer a view so it doesn't have a superview.
So we need to actually delete that. And in fact, I've left that one thing in the touch image view and it's the only reason that we still have a UIImageView subclass. All of the code in our touch image view is gone and it's over in the transform gesture now.
So touchesBegan, Moved, and Cancelled are empty but we still do have this one bit in touchesEnded that's going to look to see if the tap count is greater than 2, and if it is, move itself to the front. Alright, so this time it's really going to work. Now, we can take individual views and move them around. We can put two touches in one view and still move that around or rotate and scale it. But now, if we put two touches in different views, we're going to end up moving all of them.
[ Pause ]
[ Applause]
So it's a little bit more interesting over here because we can put more than two fingers down. So you can still put two fingers in an individual view and move it around. You can put another one down here and move that around. You can even scale two at the same time.
[ Applause]
Alright, thanks. With that I'd like to ask Jason to come up and talk about some best practices and other things that he's going to discuss with motion events.
[ Applause]
So Josh talked a little bit about hitTest. And it's really important that the view that's returned by hitTest actually handle all of these touch processing methods. Now, that doesn't mean that the class that's returned has to provide implementations for these but somewhere in the view hierarchy or the class hierarchy for that view, there needs to be implementations. So let's look at a couple of examples of this.
Suppose you just subclass from UIView, you've got a MyView that's a subclass of UIView, and UIView does not provide implementations for any of these things. It inherits UIResponder's implementation of these methods. And UIResponder's implementation goes up the responder chain looking for some view that can handle these touches.
So what happens if in your view you-- let's say you didn't implement touchesEnded, let's say, you put this in a table view. Well, we'll start calling touches methods on that and then when the finger is lifted, we'll call touchesEnded on your view but you don't implement it. It will go up to the responder's implementation, that will go up the hierarchy and table view will see this touchesEnded it never saw before. And you actually have no idea what that's going to do with that. And if it were in the begin you didn't implement, for example, that would actually cause highlighting in the table view and then you'd never send the end and the highlight would get stuck.
So the rule is if you're-- oops. The rule is if you're doing this, you need to implement all those touch handling methods and you should never call super doing that because that means you don't handle it, you need to find somebody else that does. What happens if you subclass some other UI tip thing. Let's say you subclass UIButton.
Well, UIButton actually already provides implementations for all of these methods. So you can safely just implement a subset of those so you just want it to care about the end. You can just overwrite touchesEnded and do whatever additional processing you need to do. But in this case, you absolutely have to call super. Otherwise, UIButton wouldn't see the full touch sequence.
And you may leave the button in some inconsistent state. So let's talk a little bit about touch forwarding. What happens if you have some big view and you've got several subviews of that, and its big view returns itself from hitTest. Josh talked a little bit about hitTest, and normally, we find the deepest view and return that from hitTest, but you can overwrite this, of course.
Your parent view there can overwrite hitTest and always return itself and that means it gets a chance to process all of the touches for itself or any of its subviews. So let's say you've done this and you want to conditionally forward some of those touches on to these various subviews based on some state in your application.
That's all fine to do but you need to own all of these views. If you put some UIKit class in there, let's say, one of these subviews was a button and you conditionally set some partial touch string to that, it actually can't handle that. In fact, no UIKit classes can handle touches that were not bound to them. So if the touches view is not the receiving view, you cannot expect any of the UIKit classes to handle that.
Alright, so let's look at some Gotchas. As Josh mentioned, this is actually a fairly common error. We've seen this in a lot of cases. People who handle touchesBegan, Moved, and Ended but don't provide an implementation for touchesCancelled and what happens here is when the touch gets cancelled, not only does your view not get a chance to process that potentially leaving your view in some inconsistent state, but that touchesCancelled is going to go up the responder chain looking for some object that can handle it.
Some random view is going to get touchesCancelled for touch it's never seen before potentially doing something bad in that view. So I sort of alluded to this in the previous slide UIKit classes cannot participate in sort of touch forwarding. You can't catch a view or catch a touch sequence in your own view and then just forward it on to any UIKit class and expect that class to handle that.
[ Pause ]
So, if you subclass a various UIKit class let's say you call, you subclass UIButton and you conditionally call super for some of the methods. That means that UIButton won't see the full touch sequence. Say for example in touchesEnded sometimes you call super sometime you don't depending on some state. That means UIButton itself might in some circumstances never see that touchesEvent and you can leave that button in some inconsistent state. So, if you override any of these where the superclass provides an implementation you absolutely need to call super. This is sort of related.
Suppose that in your touchesMoved you decided, well I really want to cancel this touch so you call super touchesEnded or touchesCancelled or something like that, even though you're in a move and the phase of the touchesMoved. You can't expect that the parent class is going to do the right thing of that-- with that because it's going to get, you know, touchesEnded or touchesMoved for a touch that's in the wrong phase, so you really only need to call super with the same method that you received. [Pause] Alright, so let's move on to Motion Events.
We've added this new in 3.0, the new motion EventType and we've added a new shake Subtype. So what do you need to do to receive this? Well, Josh mentioned that you need to be the FirstResponder to receive those-- I'm sorry, let's set back and talk about what you-- the methods you have to implement. There're three methods that you have to implement.
They sort of mirror, the touch methods, there's a motionBegan, motionEnded and motionCancelled. And just like the touch methods you need to implement all of these and you should not call super from any of these methods. Calling super does the same thing it does in touch process and it goes up to responder chain looking for some object that can process that motion event. And by default UIApplication actually provides an implementation of the motionEnded method to do the undo action. So, if you called super from this you might do your action and then also trigger the Undo dialogue to pop up.
Let's also talk about when you get these. So for shake, when you first start shaking device, sort of the first moment that the device starts moving we're going to send the motionBegan to the FirstResponder. If it's what we detect as a shake so sort of a short series of rapid movements, we'll send motionEnded and you can, and we'll say that the subtype of that is shake and you can do whatever process you need to do.
But if it-- for some reason doesn't look like a shake and we have some fairly complex logic to analyze that series of sort of accelerometer movements just, you know, to determine what a shake is. Say for example, the thing goes on too long you, you shake it for quite a while or for some other reason the pattern that's emitted looks-- doesn't look like a shake.
We're going to send motionCancelled there. So, for example the device is in your pocket, you run down a flight of stairs, the first part of that might actually look like a shake but if it's gone on too long, so we will go ahead and sent a cancel in that case. So if you get the ended that means, we definitely detected that there's a shake and you can do whatever process you needed to do.
OK, so now let's talk about FirstResponder. So, you need to become FirstResponder to receive these things, and to do that you just send whatever responder that you want to process the motion events the becomeFirstResponder message. And becomeFirstResponder calls another method called canBecomeFirstResponder to see if that responder is allowed to be FirstResponder at that time So, by default, normal responders return NO from this method so if you need to receive motion events you need to overwrite this and return YES.
Of course, you can do this conditionally if under some circumstances it's appropriate to become FirstReponder, in another circumstance it's not, but we'll just show sort of a simple implementation here. So, let's modify the demo that Josh built so that we recognize a shake gesture and when we recognize it, all the views will sort of animate back into their original position.
[ Pause ]
OK, so there's a few places we could do this. As we said, you know, views and ViewControllers are all responders, there are a few places we could put this logic. We could put it in the individual subviews that we saw but that doesn't make a lot of sense because we really want this to affect all of views on the screen. And we could put it in that containing view but in this particular example that's just an instance of UIView.
So to do that, we would have to subclass UIView and change the NIB which is all fine, that would work just fine, but remember the ViewController is also a responder in the responder chain and that's a fine place to handle these things. So, that parent view's ViewController is this Multi Touch demo ViewController. So, we'll just have this view handle these touches. So, the first thing we need to do-- Oops, apparently--
[ Pause ]
[ Pause ]
OK. I have just reopened it. OK, so the first thing we need to do is implement View Did Appear. We know that the View is there and on screen now. And so now, we can make our ViewController the FirstResponder. So we'll call becomeFirstResponder on that. And then we'll implement the canBecomeFirstResponder method so that we actually we'll be allowed to become the FirstResponder.
And now we just need to provide those motion methods. So, the first is motionBegan withEvent because we actually are going to be processing these. We do need to overwrite that and provide an empty implementation. If we didn't implement it, it would go up the responder chain looking for some object to handle that.
Next in motionEnded, this is when we actually detect the shake, we're going to reset the transform of the outer view back to the identity transform and then we're going to go through each of our subviews and reset their transforms back to the identity transform. And then we now need to tell the transform gestures to re-baseline from that point. So, let's run that.
[ Pause ]
We're not on 3.0.
[ Pause ]
Alright, we'll move a few things around and then we'll send a shake event. Notice everything snaps back. We didn't put any of the animation code. Fortunately, we're built on top of Core Animation, so that's very trivial to add animation code.
We'll just-- oops, I'm sorry, I didn't implement motionCancelled, look at that Very important to implement motionCancelled [laughter]. OK, so we will wrap this block of code that adjusts these transforms in an animation block. So first, just say began animations. We can set a duration here, I played with it a little bit, half a second seems like a nice duration. And then after we're done with that, commit those animations. And that's actually all we need to do to animate these views back.
[ Pause ]
[ Applause ]