Advanced Scrollviews and Touch Handling Techniques

Session 235 WWDC 2014

Scrollviews build on gesture recognizers and underlying multi-touch mechanics to provide a fundamental piece of the iOS user experience. Gain a broader understanding of the iOS touch handling architecture through practical real-world examples. Discover advanced tips and tricks for combining scrolling with other touch handling techniques to create delightful user interfaces.

[ Silence ]

[ Applause ]

Good morning.

Thanks for coming out this morning.

And welcome to another installment of the UIScrollView Session here today.

We've talked a few times in previous years about things related to event handling in UIScrollView.

But we want to take a little bit more time to go deeper into some of the details of event handling on iOS.

How it interacts with UIScrollView.

How UIScrollView uses it.

And how you can do interesting things once you know more about it in your own apps.

So before we get too much into that, I want to take a brief walk down the history of touch handling in UIScrollView.

So we'll start out by going all the way back to the beginning of time in 2008.

With the introduction of iPhone OS 2.0 and the first public SDK.

Where US ScrollView was built entirely on top of the UITouch API with touchesBegan, moved, ended, and cancelled.

And it was built in the same way that you would have written any other bit of code using those UITouch APIs.

And it had a few limitations that all of you folks were trying to work around by subclassing UIScrollView and overriding those touch methods.

And it was difficult to do some of these things because you didn't necessarily know how the internals of UIScrollView itself worked.

And so there were attempts to add things like Nested ScrollView Support.

Putting one ScrollView inside another.

And this was much harder than it probably should have been, so a year later, in 2009, we had a big update to UIScrollView that changed pretty much everything about how it looked at touches and used those touches.

And right out of the box it added support for nesting one ScrollView inside another so that you didn't have to do any of that work or subclassing or understanding internals in order to make that happen.

And then later that year we introduced the UIGestureRecognizer API which really exposed the internals of how UIScrollView had started doing these things, and let you add that kind of support into other views in your own apps.

Then in 2010, with iOS 4.0 there was another fairly big update with the release of the iPhone 4 and retina displays.

Now the interesting thing that happened here is that because each dimension of the screen doubled in pixel density, it meant that there was more precision that you could get when positioning elements.

Now for most things you still position things on point boundaries, so that you could run across different iOS devices that were either 1X or 2X.

But US ScrollView added support for scrolling at half point boundaries so that it could scroll to individual pixel granularity.

Which gave a much, much smoother scrolling experience.

A year later with iOS 5, we added support for exposing those gesture recognizers that we had previously updated and used to add these more advanced scrolling techniques onto UIScrollView.

And once these gestures were exposed, it made it a lot easier to interact with the ScrollView in your own apps.

So you could do things like get the pan gesture recognizer and set up failure requirements against it, or do all kinds of things like that.

And if you go back to previous years' sessions, you can see a number of places where we've talked about how you can do that sort of thing.

So a lot of interesting things became possible once you had access to the gesture recognizers themselves.

But in 2012 with iOS 6, there was another fairly large internal update to UIScrollView that added support for resting touches.

And so what I mean by this is that in previous years, prior to iOS 6, if you put a finger down on a UIScrollView , that was the finger the ScrollView was going to track for scrolling.

So if you did something like grab an iPad around the edge, and your thumb happened to land on a ScrollView, you would try and scroll with another finger and nothing would happen because it was tracking that first finger.

Now with iOS 6 we started looking at all of the touches that were anywhere in the ScrollView and only paying attention to the ones that were actually moving at any given time.

So even if you had a touch sitting there resting and not going anywhere, it didn't prevent you from scrolling, and you could still interact with other fingers.

So that was a really big and interesting update, and we'll see some ways that we are going to take advantage of that later today.

And finally last year with iOS 7, we added support for dismissing the keyboard using UIScrollView.

And there was a new property that you may have noticed was added last year that lets you decide whether or not scrolling a scroll view and having a finger intersect the keyboard will push it down off the screen.

So over the years, the UIScrollView API has remained fairly stable.

There haven't been a lot of changes in the API.

But under the covers there have been a lot of internal touch handling changes that have added all sorts of new things that you can do once you know about how some of these internals of the ScrollView and touch handling on iOS work.

So we're going to take a look at three areas of touch handling today.

And then we're going to talk about three things that you can do once you know about these bits of touch handling information.

So three techniques based on three areas of touch handling.

And those types of touch handling things we'll talk about well first off we're going to start with Hit Testing.

Because Hit Testing is the most fundamental part of handling touches on iOS.

When a finger comes down on the screen, what got hit?

And what element are you trying to interact with?

So we'll look a little bit more deeply at how it works, and ways that you can adjust things during Hit Testing to get interesting behaviors.

Then we'll spend some time talking about UIGestureRecognizer.

Now this is something we've talked about many times in the past in various different sessions.

But we have some new, interesting ways that you can use Gesture Recognizers, along with your ScrollViews that will give you some ideas about things you can do in your apps.

So we'll get to that later today.

And then we're going to talk about touch delivery.

So the way that touches flow through the system.

Get delivered to views.

Interact with Gesture Recognizers.

And how you can take advantage of that information in other interesting ways once we get to our three techniques.

So how about those three techniques?

We're going to talk about transparent overlays.

Putting content on top of your other content and making sure you can still interact with everything and things don't end up behaving strangely.

We'll talk about dragging while scrolling.

This is something that I actually find pretty exciting and think is a really cool thing you can do with ScrollViews.

So you can have content in your ScrollView that you interact with, say something that you want to pick up and drag around.

But often you might want to then be able to continue scrolling with another finger.

And I mentioned before that we have this resting touch support, and ideas about how you can interact with multiple fingers on the ScrollView.

So we'll look at how you can use that information to add support for dragging content while also scrolling the ScrollView.

And then finally we'll end by talking about highlighting objects.

Which may not seem at first like a really key part, but part of the reason it doesn't seem so key and interesting is because it just often does the right thing and you don't think about it.

And so in places where it doesn't, you end up seeing some jarring effects.

And we'll talk about how that happens and how we can go about fixing it.

And look at some internal implementation ideas of how ScrollView has taken Gesture Recognizers to do interesting things with highlights.

So three areas of touch handling and three techniques.

So let's get started with transparent overlays.

Of course if we're going to put things on top of things, that's probably going to involve some sort of information about Hit Testing.

So we'll get into that.

But before we do, I just want to give you a quick idea of the kind of thing I'm talking about and where we use this sort of technique in iOS ourselves.

So let's take a look at the home screen, where we added spotlight in iOS 7 up at the top of the screen.

And you can drag a finger down and pull Spotlight down in from the top of the screen.

Believe it or not, this is actually done using a UIScrollView.

It's really more of a transparent overlay ScrollView.

It doesn't really draw anything itself.

But it's an interesting technique, and we'll take a look at how we can use this in our own apps.

So before I get too much into the details of how it works or take you through slides or anything, we're just going to have Eliza come up and do a demo to show you what we're going to build.

[ Applause ]

Hi. All right.

So I've got a little app here that I've started building.

It doesn't do very much yet.

It draws a bunch of dots in a canvas, and soon we're going to add support for dragging them around.

But for now, the dots highlight I hope you can see that in touchesBegan.

And then they un-highlight in Touches Ended and Touches Cancelled.

So the first thing that I want to do here is add a drawer that you can pull down over the top by panning anywhere in the canvas.

And so in order to do that we're going to need to add a ScrollView that covers the entire canvas.

So I'm going to switch over to the code, and you can see this is pretty much it.

I've got a little DotView class that can make a random DotView that's a random color.

And I've got a canvasView that has 25 dots added to it and they're arranged randomly in the view.

So the first thing that I'm going to do is add a ScrollView.

And I'm also going to add a drawerView.

And what we're going to do is take advantage of the new API that was added in iOS 8, UIVisualEffectsView, which allows you to create blurry content in your applications.

And so since this drawer is going to cover part of the screen, we'll get a sense of depth by making it blurry.

So go ahead and make these guys.

So our ScrollView gets added to the view.

The drawerView is going to be a UIVisualEffectView initialized with an effect.

The effect I want is a dark blur, so I'm going to ask for a UIBlurEffect effectWithStyle BlurEffectStyleDark.

I need to choose a frame for my drawerView.

I'm going to make it the width of the screen and 650 points tall because that looked good when I tried it out.

And I'm going to add it to the ScrollView as a subview.

Okay. One more thing we need to do is tell the ScrollView how big its content is.

So I'm going to make a content size which is the width of the screen, but the height of my bounds plus the drawerViews frame.

And that will give us enough room to scroll the drawerView entirely off the screen at the top.

And finally I'm going to set a starting content offset to make the app launch with the drawerView scrolled off the screen.

So I'll go ahead and run it.

All right.

So we've got our dots.

And now if I scroll I get a blurry drawer.

Cool. Unfortunately I've broken touch handling.

So if I now scroll the drawer away and I try to tap on these dots.

I'm tapping; nothing's happening.

So the reason for that might be apparent if you think about it.

I've taken a big, screen-sized UIScrollView and I've plunked it down on top.

So of course it's blocking touch delivery to the content underneath.

Touches are going to the ScrollView and that's what's allowing me to pan.

All right.

So to fix it, what can we do?

One thing you might consider doing is turning off user interaction on the ScrollView.

That's generally a pretty good way of getting touches to pass through a user interface element that you've added.

So let's try that.

ScrollView set userIinteractionEnabled.

No. Run it again.

And now excellent touches are now going to my dots as they were before, but if I try to scroll, of course nothing happens.

So why not?

Well obviously I've disabled user interaction on the ScrollView so it can't scroll.

All right so that's no good.

So the way we're going to actually fix this at least the way we're going to start to fix it is to use a technique that Josh and I introduced in a session two years ago on open GL content, and it works here as well.

You can take the ScrollView's pan gesture which is exposed as a property that you can access.

And you can move it onto another view in order to restore panning in a situation where the ScrollView isn't suitable to being the view that's getting the touches.

So we're going to use that technique here and we're going to actually move the ScrollView's pan gesture recognizer onto my view controllers view.

So onto the ScrollView's superview.

This way the ScrollView can continue to have its user interaction disabled, but the panning will be restored.

So I've got touches going to the dots.

And now I also have panning working.

So now it's kind of starting to look like we've got this overlaid behavior the way we want it.

Let me go ahead and add some additional dots in the drawer because we need something in the drawer.

So I'm going to say add 20 dots to the drawerView.

Notice that when I do this I ask for the drawerView's content view.

This is because the drawerView is a UIVisualEffectsView.

And UIVisualEffectViews are doing a lot of work in order to make that blur happen.

And in order to avoid and interfering with it, you add additional content into this content view that they expose.

And then in order to differentiate the drawer from the canvass, I'm going to arrange the dots neatly in the drawerView.

All right so can run this.

Pull the drawer down.

I've got my neatly arranged dots.

I can interact with the dots in the canvass, but I can't interact with the dots in the drawer.

And in fact moreover, if you look through the drawer at this like orange and blue guy here, you can see that I can interact with the dots that are behind the drawer still.

All right so now why is that happening?

Well, I disabled user interaction on the ScrollView and the drawerView and the dots are in the ScrollView.

So touches are passing right through the ScrollView, the drawerView, the dots to the content behind it.

Which is not what we want.

So in this case, disabling user interaction was kind of too big a hammer.

It got most of the behavior that we wanted, but now as soon as we want to interact with something in that view hierarchy we can't.

So I'm going to turn it back over to Josh to explain a finer grain technique that we can use to get the behavior that we want here.

[ Applause ]

All right.

So we're getting closer.

But as Eliza was mentioning, we've just gone a little bit too far with this disabling of user interaction on the ScrollView.

It got part of what we wanted, but it went beyond and did a little bit too much.

So to start figuring out how we can be a little bit more precise in what we're trying to do, we have to take a look at how hitTesting works.

That is going to be using the method hitTest:withEvent.

HitTest:withEvent is the method that's used when a new touch comes down on screen to figure out what we should deliver the touch to, and what gesture recognizers should end up being involved in looking at that touch.

So before we look at how we're going to use it, let's talk a little bit more specifically about what exactly it does.

And I want to do that by going through and writing a little bit of pseudo code to just show you the order of things it does, and how it goes about doing it.

So here I've got a swift version of hitTest:withEvent.

This is our, of course, function syntax right there.

So the first thing that hitTest:withEvent does is it checks to see whether or not the point being passed in that you're trying to hitTest is actually within the view or not.

And it does that by checking to see is this point within my views bounds?

So if it is, than we're going to do some other stuff.

If it's not, we just return nil.

If the view finds that the point it's being asked about isn't in its bounds, it returns nil to indicate that it's not interested in this touch.

That point.

So then the next thing it has to do is return itself if it actually was in the bounds.

So by default, if it was in the bounds we at least hit the view itself.

And that's where we were running into trouble with that transparent part right at the beginning.

Even though we weren't hitting any subviews of the ScrollView and there was no content there, as long as the touch was within the ScrollView's bounds, the ScrollView was going to return itself as the thing that got hit.

Now of course you can also hitTest into subviews of a view.

So once we've decided that it's actually within our bounds, we're going to iterate through all of our subviews and see if it's in any of them.

So we're going to go through and add an inner loop where we walk all over our subviews.

Now importantly here we're going to do this from back to front.

Because if you think about how rendering works, we render the first subview and then the next one and then the next one, all the way through to the end of the list of subviews.

And so whatever one rendered last, is visually on top.

So we want to perform the hitTest in reverse order so that we would hit the one on top first.

So we iterate backwards through the subview list.

And ask each of our subviews whether or not it got hit.

So we call hitTest:withEvent recursively on those subviews.

Now if one of them does return something other than nil, we're just going to return whatever it returned, and then that will break the recursion.

So the first subview that were to hit something, it would end up being returned as the thing that got hit.

So that's pretty much all there is to it.

It's pretty straight forward.

I've also got a version here in Objective C in case that big difference was too big of a difference.

[ Applause ]

So now let's take a look at our sample app and figure out exactly how we can use this information to get the behavior that we're looking for.

Now first off we're going to go and re-enable user interaction on the ScrollView.

Because we decided that that was just not the right approach.

It was too big of a hammer.

So everything we're going to talk about right now, we're going to assume that we've turned user interaction back on, and go back from there.

So let's start out by looking at our view hierarchy.

We've got that UIViewController's view.

It's the root view in our hierarchy.

And then to that Eliza added a dot view a container view that has all the dots in it.

So we've got a direct subview of the view controller that has all the dots.

Now that view has a sibling, another subview of that view controller.

And that's the UIScrollView.

So those are siblings.

But the UIScrollView is the second in the subview order.

As we just talked in hitTest, it's the one that will get hitTested first.

And then we have a subview with a ScrollView, which is our drawerView.

That's inside the ScrollView.

So now let's take a look at how touches flow through during that hitTest between all these different views.

And to do so it will be a little easier if we can see them all at once, so I'm just going to split it out so we can take a look at it as the touch comes down and see what happens.

Now first of all I'm going to remove that DotView.

Because as we already mentioned, as we're hitTesting we're going from back to front.

And whichever one gets hit first and returns something will end up ending the recursion and we won't iterate through the other subviews.

So as it starts, we're never going to even get touches going to that DotView at all.

So let's look at it without that first.

Now let's say a touch comes down in the drawer area, let's see what happens.

So a touch comes down up there.

We start out at the rootView the view controllers view.

And then we work our way through its subviews and we find that this ScrollView is going to hit something, and it works through its subviews.

We find that the drawerView is going to hit something.

And then there's probably a dot in there; or maybe not.

So but we're at least going to end up returning the drawerView; maybe the DotView.

So that part already works; that was easy.

There was no problem there.

So now the issue came up when we were in that transparent area of the ScrollView, that was farther down.

So a touch comes down, down there.

We start with a viewController.

Looks through its subviews.

Finds the ScrollView.

And even though there's no content visually there, it's in the bounds.

So it returns itself.

And so that's the place where we have to do something to fix this.

And the fix actually is pretty similar to what I've just said there.

It returns itself.

That's the only place where this ScrollView is going to return itself from hitTest:withEvent.

In the other case where things were working, it was returning one of its subviews.

So we can do something where we're taking advantage of only the case where ScrollView's returning itself.

Instead of returning itself, we want to return nil.

Which will cause that superview the view controllers view to move on and look through the other views that are subviews of itself.

And that would allow us to instead of hitting the ScrollView, hit either the DotView or one of the dots that are in the DotView instead.

So what we can do is we can subclass UIScrollView and override hitTest:withEvent.

Of course once we've overridden it, we just actually most of the time want the default behavior.

So we'll call super and hang on to the result that we get there.

If the thing that the superclass implementation returned was the ScrollView itself, that means that we're in that transparent area.

So in that case we'll return nil.

Which will cause that UIViewController the outer view to continue through the subviews and find the dots and hit something in there.

So a pretty straight forward fix.

Eliza's going to come up now and make that change to our app and see how we're doing.

[ Applause ]

Great. So I've started to add this class here.

New subclass of UIScrollView.

I've called it OverlayScrollView.

So let's go ahead and add that to the project.

First thing I'm going to do is just get rid of the implementation that was provided.

So what we want to do here is, as Josh said, override hitTest:withEvent.

This is really the only point of this UIScrollView subclass is to not return itself from hitTesting.

So we're going to call superHitTest:withEvent.

And in most of the cases will return the view that was returned from the super-implementation.

But if the super-implementation returned the ScrollView, that means that we were that the touch came down in an area of the ScrollView that didn't have any other content.

And in that case we want to allow the touch to pass through the ScrollView and go on to the other siblings of the ScrollView.

So we'll return nil in that case.

And that's pretty much it.

So we need to go back to the view controller and import that file.

And then here where I'm creating my ScrollView, instead of creating a UIScrollView , I'm going to just create an overlay ScrollView.

And I need to remember to stop disabling user interaction, because we no longer need such a big hammer to get this effect that we want.

So I can run this.

And even though user interaction is now re-enabled, I can still touch these dots.

So that's the effect of returning nil from hitTest:withEvent.

And then I can still pull down the drawer.

And now I can actually touch the dots that are in the drawer and I can no longer touch through the drawer to the dots behind.

So all right, so we finally pretty much have this working the way that we want.

One thing to note, I'm still adding the ScrollView's pan gesture recognizer to the super-view.

I need to do that even though I've re-enabled user interaction because the transparent part of the ScrollView is no longer hitTesting.

And the ScrollView's pan will not get any touches that don't hitTest to the view that it's on.

So in order to allow scrolling in the transparent region, I still actually need to use this technique of moving the pan gesture onto the super view.

Otherwise you'd be able to scroll in the drawer, which is getting hitTest, but you wouldn't be able to scroll in the other parts of the ScrollView.

All right.

So with that all done, I'm going to change gears, and let's add dragging to this application.

So we're going to make it so that these dots can be picked up using a long-press gesture recognizer and dragged around.

So here where I add the dots to the view, for every dot that I add, I'm going to make a UILongPressGestureRecognizer.

I'm going to initialize it with myself as the target and the selectorHandle LongPress which I'll implement in just a moment.

And I'll add that gesture recognizer to the dot.

So I'm going to end up with a lot of long press gesture recognizers.

One per dot.

And here's my handleLong PressMethod.

So before I even do anything in response to these long presses, I want to show you a bug that I just introduced by adding the long press at all.

So if I run this, and I touch down on a dot and leave my finger down, the dot un-highlights after a brief moment.

Even though I didn't actually lift my finger in this case.

So the reason that that is happening is that by default, UIGestureRecognizers cancel touches in their view once they've recognized.

That's the default behavior of a UIGestureRecognizer and it's often what you want.

In this case it's not what we want because we actually want the dots to stay highlighted while they're being dragged around.

So I can fix that here by telling each long press gesture that it does not cancel touches in its view.

All right.

So now let's go ahead and implement this handleLongPress method.

We're going to get the dot that was pressed by asking the gesture for its view.

And now we're going to do what may be a familiar switch statement that you tend to do in UIGestureRecognizer Target methods.

We're going to switch all of the different possible states that this gesture can be in, and we're going to grab the dot if the gesture just began.

If the gesture changed, we're going to move the dot.

And if it ended or was cancelled, we're going to drop the dot.

So I'll go ahead and implement all of those methods now.

All right.

So from my years as a springboard engineer, I know that when you want to make something look grabbed, you set a scale transform on it to make it look a little bit bigger.

And you lower its alpha to make it look a little bit transparent.

That way it actually appears to have changed when it starts getting grabbed.

And when you want to make it stop looking grabbed, you do the same thing in reverse.

Transform back to identity.

Alpha back to one.

So the other thing that you want to do when grabbing an element is pull it to the front of everything.

So that as the user drags it around, it passes over all of the rest of the content.

So these dots may have been grabbed out of the drawer or they may have been grabbed out of the canvas.

What we're going to do is re-parent the one that was grabbed and move it into the view controllers view at the end of the subview list so that it passes over all of the other content.

So I'm going to actually do that before grabbing it.

I'm going to add it as a subview of my view.

Now any time that you re-parent a view, you need to watch out for the possibility that the origin of the new view is not the same as the origin of the old view.

And so it's positioned the center that it had in the old view may not result in the same position on the screen as the center in the new view.

So we need to do a little bit of point conversion here to make sure that the dot doesn't appear to change locations when it was re-parented.

By setting it center to the result of converting its old center from its superview.

So we're going to convert that point to the view.

And then we're going to add it as a subview of the view.

All right.

And then when the dot moves well so there it's actually pretty simple.

What we want to do is keep the dot under the user's finger.

We know that the dot is in my view that's superview.

So we can simply set its center to be the gestures location in that view.

All right.

Now there's one little caveat here.

If you saw one of our sessions from a couple years ago, you'll maybe remember that we've done a technique like this where you pick something up and drag it around.

And it has the potential bug that when you start moving, the element that was grabbed jumps a little bit.

That's because if you see what I'm doing here, I'm adding the I'm moving the dot so that its center is under the touch.

Every frame as the user moves their finger, I'm putting the center of the dot under their finger.

However, the user may have picked the dot up from the edge.

They may not have picked it up from the center.

So the first time they move their finger, it will jump under their finger.

So in order to prevent that kind of jarring jump, what I'm going to do in this case which is a little different from how we solve this in the past I'm just going to actually call move dot with gesture in that grab animation.

So that the first time that the user grabs and the dot gets picked up, it also just animates so that its center is under their finger.

And that way we won't have a jarring effect when they start to move their finger.

Finally, when the dot gets dropped, we need to figure out are we going to put it down in the drawer?

Or are we going to put it down in the canvas.

So I'm going to find out from the gesture what is the location in the drawer view.

And if the drawer view's bounds contains that location, that means that the dot has been dragged so that it's over the drawer.

And I will at that point add the dot to the drawer view's content view.

Otherwise, I'll add it as a subview of my canvas.

And I need to do that same point conversion in reverse.

Move the dot center, so that it's the result of converting from my view that it was in before to its new superview.

So that way it will appear to stay in the same location.

And now we should be good to go.

So I can pick one of these guys up.

As you saw it animated nicely under where my mouse was pointing.

Drag it around.

I can do the same in the drawer.

Pick it up.

Drag it around.

Put it down.

Pick one up here.

So I can move these guys all around and it seems to be working.

The drawer is getting a little messy.

This offends me slightly so I'm going to fix it by just asking the dots to arrange themselves neatly with nifty animation in the drawer at the moment that they get picked up.

And I'm going to do the same thing when we put them down.

So now when I pick one up they do that.

Whoo!

Thanks.

[ Applause ]

Okay so this is pretty much working as we want.

But it would be cool if you could pick a dot up with one finger, and then scroll the drawer to either bring it down or push it back up again with another finger.

I'm going to switch over to an actual iPad here where I've got this running.

So that we can do a multi-touch thing.

So here's the very same app running on an iPad.

The one difference is that I've modified it so that you can see where the user's touch comes down.

So that little white dot that's moving around is where my finger is.

So you can see that I can pick up a dot.

And I can actually even pick up more than one dot.

This you get for free, just because we've got a bunch of long press gesture recognizers, I can move them all around at the same time.

But if I put down another finger and attempt to scroll the ScrollView, nothing happens.

So let's try to fix that.

We went to be able to simultaneously be dragging one of these dots around and scrolling the ScrollView with another finger.

All right, so why isn't that working?

So I could have fingers on the different ones of the dots and interact with those at the same time because those views are siblings.

And so their gesture recognizers don't interact with one another.

However, the dots are a subview of the view that has the pan gesture recognizers.

So the long press gestures and the pan gesture recognizer do interact.

And by default, the behavior of gesture recognizers that interact is to be mutually exclusive.

So once I've already picked up a dot, I can no longer make that pan gesture recognized.

But we can easily tell these gestures that they can recognize simultaneously.

And the way we do that is by becoming the long press gesture recognizers delegate.

So we'll say that we conform to UIGesture RecognizerDelegate.

And when we add the long presses, we'll make ourselves the delegate of all of them.

And then we'll implement a single delegate method.

Gesture recognizers should recognize simultaneously with gesture recognizer.

And in this case, just as a shortcut I'm going to return yes.

I can do that safely here because this is a pretty small app, and I know that the only gesture that I'm going to be asked this question about is the ScrollViews pan gesture.

In your own applications you should be much more specific here because it would be an easy source of bugs to just return yes willy-nilly to any gesture recognizer that you're asked about.

So once we've done that, I'm not going to build it because I've actually got an already built copy over here.

So this is a result of having made exactly those changes.

And let me switch over.

All right.

So now I can scroll while one of these guys is grabbed.

But you can see there's actually a pretty bad bug here.

If I try to scroll this down I can.

But I can also scroll it using the very same touch that's dragging one of the dots.

Which is clearly not the behavior that I want at all.

All right so now why is that happening?

Well I was asked, can the pan gesture recognize simultaneously with the long press gesture, and I said yes.

So they're recognizing simultaneously.

The very same touch is having the effect of recognizing with both gestures.

So that we do not want to do.

And I'm going to bring Josh back on stage to talk about how we can fix that last little problem here.

[ Applause ]

All right.

Well we're getting pretty close.

We can almost do what I really want, to be able to drag these dots around while also scrolling this drawer on and off.

So let's figure out that last little bit of how we can make sure that these gestures recognize, using the touches that we actually expect them to.

Let's look first again at the view hierarchy and where all this stuff is set up, just to make sure that we're all on the same page about how this is currently interacting.

So we've got that outer view.

And we've got our ScrollView.

And we've got our drawerView here.

Now of course the long presses that are on the dots let's look at the ones first that are up on the drawerView.

They're on subviews of the drawerView actually.

They're each attached to the individual dots.

And then we've got the pan gesture recognizer from the ScrollView.

Had we not done anything else, it would have been on the ScrollView.

But we took it and we moved it up and put it on that outer containing UIViewControllerView.

So it's out there.

So now when a touch comes down inside that drawer, it's going to be seen by both.

It will be seen by any of the long press gestures that it's interacting with.

So if it's on a dot, it will be seen by the long press on that dot.

And it will also be seen by the outer UIPanGestureRecognizer, from the UIScrollView .

Now that's where we're getting this bug that Eliza was talking about.

How do we fix this?

We need them both to be able to recognize at the same time.

Because we want one touch in a dot to be able to move it, while another touch outside doesn't.

And we already know that those gestures are going to interact with one another.

So we definitely need to allow them to recognize simultaneously.

So we can't change that.

We want to do something when the long press starts, to prevent the pan from recognizing with that touch.

We want to allow the long press to continue so we can drag the dot.

But we just want to stop the pan.

But we don't want to stop it from panning at all, just form panning with that touch.

So we can actually take advantage of the fact that there's this special side behavior of disabling a gesture recognizer which causes it to stop looking at any touches that it was currently looking at.

So when the long press recognizes, we can just get the pan gesture and set its enabled state to false.

By setting it to false, it's going to tell it to stop looking at any touches it was currently considering, and reset itself basically.

So it will no longer consider that touch.

The long press will still be able to continue considering it, because we didn't disable the long press, just the pan.

But of course if we did that, than you wouldn't be able to pan with another touch because we disable the pan.

We can actually just go right around and turn it right back on, and it will still have stopped looking at the touch that it was looking at, but it will now be able to look at new touches that come down.

So actually it turns out this is going to be really, really easy to fix.

And Eliza's going to come back and do it really quickly and see where that leaves us.

[ Applause ]

All right.

So this is going to be the fastest demo in history.

All I need to do is at the moment when I'm grabbing the dot, I just need to disable and then re-enable the ScrollView's pan gesture.

So disabling it will cause it to just stop tracking all the touches it was tracking, including the long press.

And re-enabling it will allow it to be ready to track new touches that might start.

So I will switch back over here.

And launch the third version of this.

So the third version here we can now pick up a dot.

We can simultaneously scroll the ScrollView.

But the dot itself no longer scrolls the ScrollView.

So now I can do all the things I wanted to do.

I can grab several dots at once.

Put them in the ScrollView.

Grab several of them out of there; pull them over here.

So this is pretty much working exactly as we wanted.

Now there's a little there's a few elements of polish that have to do with the way that these dots highlight themselves.

And I want to try to draw your attention to a little problem that may not be immediately apparent.

So I'm going to put my finger down to start a pan in that blue dot near the top.

Did you see that it momentarily highlighted, and then sort of blinked back off again?

I'll do it on another one the orange one here, just to so pans that start in the dot cause it to momentarily receive touches again, which causes it to be highlighted.

But then as the pan recognizes, it cancels touches in its view.

And so touchesCancelled gets delivered to the dot.

And so you see this momentary flash of highlighted as you start scrolling.

But now notice that the same thing does not happen with the dots that are in the drawer.

So when I start panning here, I don't get that flash of oh actually well I guess I'm doing it too slowly [chuckles] sorry.

Let me do it a bit faster to see the effect.

It's very subtle.

But for the most part, pans don't cause that flash of highlighting in the drawer view.

If you're really deliberate about it, I guess you can get them to do it.

So the reason for the difference is that these dots here in the drawer are in a ScrollView.

And by default, ScrollView actually has behavior that delays the delivery of touches to its subviews while it's checking whether a pan is starting.

And you can really see the effect of this if you use UITableViews in iOS.

You'll see that if you basically start scrolling pretty quickly in a UITableView, you don't see a flash of highlight on the cell that you happen to touch.

And so you avoid this kind of experience of flashing happening as you start scrolling.

So I'm going to bring Josh back on stage to explain how that's accomplished in UIScrollView and how we can get the very same effect for these dots that are not in a ScrollView.

[ Applause ]

All right.

So I promised at the beginning that we were going to talk about some polish.

And look at some internal implementation ideas of how UIScrollView accomplishes this sort of behavior.

So let's go do that.

But before we do, I just want to get a quick video of what Eliza mentioned there, of when you're scrolling in a UITableView.

So if I go and scroll this view here, you're going to find that we don't end up seeing flashes, as she said we wouldn't.

It scrolls smoothly.

There's no flash of any cells highlighting, no matter where I put my finger down, as long as I start scrolling pretty quickly.

Now if I put my finger down and leave it there for a little while, then we're going to go and highlight whatever cell you put your finger in.

So that's exactly the same kind of thing that we're talking about here in these dots.

But applied really everywhere that you see a UITableView.

So this behavior, as Eliza mentioned, is accomplished using a property on UIScrollView.

So if you're in a ScrollView you're getting this automatically.

That property is called delaysContentTouches.

Now you can turn this off if you wanted.

If for some reason in your ScrollView you want to make touches go through immediately with no delay, but by default you get a short delay before they're delivered to any view in the ScrollView.

Now in the case that we're looking here with these dots, we don't actually have all the dots in a ScrollView, so we're not getting that behavior on the ones that aren't.

To understand how the ScrollView is getting this, it helps to look at all of the gesture recognizers that are attached to the ScrollView.

So the ScrollView has a pan gesture recognizer.

We know that.

We already took it and used it this session in order to move it out onto that outer view.

Of course it also has a pinch gesture recognizer.

So if you're using zooming in your UIScrollView, there will be a UIPinchgestureRecognizer on the ScrollView as well.

But there's actually a third one that you may not know about.

It's actually there if you look at the gesture recognizer array on the ScrollView.

But it's not particularly useful to know about in most cases, other than to understand how these things work.

And that third one is a touch delay gesture recognizer.

So this gesture recognizer's sole purpose in life is to sit around and fail [chuckles].

So I feel a little bad for it, but it's there.

It never recognizes.

It's there just as a way to delay touch delivery to the views in this ScrollView.

And the way that it does that is by taking advantage of a property that exists on UIGestureRecognizer called delaysTouchesBegan.

Now this is no by default, because when you set it to yes it can introduce big delays in touch delivery throughout your app.

So most gesture recognizers do not want this property set to yes.

Because what it does is delays delivery of touchesBegan the entire touch sequence actually the began and all subsequent events, until that gesture recognizer either recognizes or fails.

So we can use it to delay delivery of the entire touch sequence to some view that's attached to whatever view the gesture recognizer is attached to.

So if we look at a timeline of how this works, then we can see why this makes sense and how it does what it does.

So when a touch comes down, the touch gets it's going to begin; it comes down.

Let's look at what happens to the pan gesture.

The touch delay gesture.

And the view that the touch was hitTested to.

So at this point the touch delay gesture is going to start a timer.

It's a pretty short timer because we don't want to add big delays to delivery of the touch.

Let's say .15 seconds just as a number that I might pick out.

Now if you leave your finger down for some period of time, until this timer fires, than the delay gesture is going to fail.

It will set its state to failed.

And once it does, because it was the only thing delaying that touch, the UI view at that point that it was hitTested to, we'll see touches begin with event, and the touch will be delivered.

Now if things progress and the user decides to move their finger a little bit, maybe the pan gesture starts to recognize.

And at that point the view is going to get touchesCancelled with event.

So that's where the highlight will get removed.

The delay gesture has already failed, so nothing new is happening there.

So that's the case where you leave your finger down long enough.

But the interesting case is when you scroll really quickly.

So let's look at what happens in that case.

Again we put the the user puts their finger down.

The delay gesture starts a short timer.

And the view still hasn't seen anything because that delay gesture exists and has delays touches begin.

Now if the user at this point starts scrolling, and the pan gesture recognizes, then the pan gesture would have cancelled that touch.

But because we never delivered it yet it was still being delayed by that delay gesture, we don't ever actually deliver it to the view at all.

As far as the view is concerned, the touch never happened.

The pan gesture recognizes.

The touch would have been cancelled, but we never delivered began.

So it would be kind of silly to deliver began cancelled.

So we just don't deliver it.

And that causes us to never highlight.

And never flash a highlight.

And we get exactly the behavior that we're looking for.

Now there's nothing particularly magical about this touch delay gesture recognizer.

And we can write one ourselves that does pretty much the exact same thing that the one on UIScrollView does, so that we can use that in situations where we're not using a UIScrollView .

Of course there are no situations where you should not use the UIScrollView, but let's imagine that there might be.

[Laughter] So we can do that by subclassing UIGestureRecognizer and over-rising over-riding its designated initializer with target action.

And of course what we do in there is set delaysTouchesBegan to yes.

Because as I mentioned, that's no by default.

So that most gesture recognizers aren't doing that.

Then as with all UIGestureRecognizer subclasses, we're going to override some of the touch methods.

So we'll do touchesBegan, ended, and cancelled in this case.

Because for this gesture recognizer, we don't actually care if the touch ever moves anywhere.

We're not trying to deal with that.

We just care when it comes down and when it comes up.

So we'll override touchesBegan and start a timer we mentioned there's going to be that short timer, so we'll start that there.

And then in touchesEnded and touchesCancelled, we want to set our state to failed.

Now the reason we want to do that is because if the user taps quickly we want that touch to get delivered immediately when the touch comes up.

We don't want to wait until this timer has expired in order to deliver the touch, or you'll introduce extra delay that you don't mean to when it's not necessary.

So if the touch ends or it gets cancelled, we're going to set the state to failed.

And that will allow that touch to go through and get delivered to the view.

Now of course we said we're setting a timer, so we have to implement some timer method.

Let's say that we've got some function that gets called.

What we're going to do in there is also set our state to failed.

If our timer passes; this gesture fails, that will allow the touch to get delivered.

And then finally, the last thing that gesture recognizers should do is override the reset method.

Which is where you go about putting yourself back in shape to be ready for another instance of trying to recognize.

And so in there we're just going to clear the timer.

Reset it. And get everything back into a good state to start over again.

So pretty small gesture recognizer.

It's never going to try and recognize, which is kind of unique.

There's not a lot of gesture recognizers that never try to recognize anything.

But it gets us an interesting effect.

And Eliza's going to come back up and build it for us.

[ Applause ]

All right.

So I'm adding another class here.

TouchDelayGestureRecognizer, which is going to be a subclass of UIGesture Recognizer.

So we're going to it's going to have a really simple implementation like Josh described.

The first thing we need to do oops is import the subclass.

But we should do that in the right place.

And then we're going to override initWithTarget action, called super.

And then do one thing which is to set touches delaysTouchesBegan to yes.

As Josh mentioned, the only purpose for this thing is to delay touches to its view.

So we need the delaysTouchesBegan flag on.

And then sorry.

One step ahead of myself.

We need a timer in the as an Ivar of this guy.

And then we're going to set that timer in touchesBegan.

So we'll schedule it.

Give it an interval of .15 seconds.

And then when the timer fires we're going to just call this fail method that I'm about to write.

And in the fail method we will simply set our state to UIGestureRecognizerStateFailed.

In touchesEnded and touchesCancelled, we're also just going to fail.

And finally, when we're told to reset, we're going to just get rid of that timer; clear it out.

And be ready to go again the next time a touch comes down.

So I'm going to go back over to the ViewController now.

And we're going to import this file.

And then I'm just going to make one of these guys and I'm going to add it to the canvasView.

Now notice that I'm passing it a nil target and a nil action.

It's an unusual thing to see when you make a gesture recognizer, but this thing never recognizes, so there's no point in giving it a target or an action.

If I did, than they would never be invoked.

So it's really just the existence of this thing, and the fact that it's attached to a view that it's going to have the effect that we want.

I'm going to add it to my canvasView so that the dots in the canvas get this same behavior as the dots in the ScrollView.

So go ahead and run this.

And now if I start a pan in one of these dots if I do it slowly enough, than you can see a highlight oops I picked that one up.

But if you do it fast enough, you'll see that there's no longer a flash as I start scrolling.

And so we're getting exactly the same behavior that we have in the ScrollView.

So we're going to show you one more thing.

Another sort of small element of polish that we can add to this application.

And I want to show you the problem first.

Notice that some of these dots here are extremely small.

In fact I think I'm generating their radius' randomly, but they are as small as a radius of 10.

Which makes the whole thing only 20 points wide.

In general it's pretty difficult to hit a view with you finger if it's less than 44 points wide or tall.

So although it's very easy for me to pick these things up in the simulator using my mouse, it would be quite difficult to hit them if I were using my finger.

So we want to show you a technique that we can use to make very small user interface elements hittable.

And so I'm going to bring Josh back up on stage to explain how we can accomplish that.

[ Applause ]

All right.

So we promised three sections and three techniques.

But we've got a little bonus extra bit here at the end.

We're still going to talk about hitTesting though.

So it's still within the three areas of touch handling that I promised so we haven't strayed that far from my original statement.

As Eliza mentioned, what we're trying to do here is enforce a minimum hit target size.

Now she mentioned 44 and threw that number out.

The reason that she mentioned that is because it's a common number that you'll find throughout UIKit.

If you look at the default bar heights for things like tool bars; or the default row heights for tableView cells, 44 is a common number that you're going to find come up.

It's a good rule of thumb of something that if you start getting smaller than this, it's hard to hit this thing.

So to figure out how we're going to go and resolve this situation, we're going to go back and look at hitTest:withEvent again.

Now there's a couple ways you could do it.

You could just make your view bigger.

That would obviously make it hittable.

But in the case of what Eliza's looking at in our sample app right now, if we made the view bigger, that would actually make the circle draw bigger.

Because she's drawing it based on the size of the view.

So if we were going to fix the hitTesting problem by changing the view size, then we'd have to go refactor a bunch of other stuff and change the way we draw the view to account for that.

And that could end up making things more complex.

And a bigger change than we really mean.

So let's go back and look with our hitTest with event method again and see if there's anything in here that might help us.

Well if we focus in on this part that I mentioned at the beginning, we've got one check right off the bat that says, is the point inside our bounds?

Now I wrote this in some pseudo code here, so it's not exactly clear what that means.

So let's expand it out to what it really does.

It's going to go and call a method called pointInside withEvent on the view that's being asked to hitTest itself.

Now the reason that that's interesting to know is because it means there's another override point where you can change the behavior of hitTest:withEvent without changing hitTest itself.

So we can actually go and override that method independently of hitTest with Event and change what it means for a point to be with inside of view.

So by default, as I mentioned what it's going to do is just check its own bounds and see if the point is with inside it.

So we'll call CGRectContainsPoint bounds, and the point that we were checking on.

But we can make this do whatever we want.

So if we want the view to behave as if it's bigger without actually changing its bounds and making it bigger, we can subclass and override pointInside withEvent.

And change the check to do anything we think is right for our view that we're interested in.

So another short section, but Eliza's going to come right back up and go ahead and fix that last bug for us.

[ Applause ]

All right.

So here I am in my DotView subclass.

So I had mentioned that had written this class.

Pretty much all I do here is make these dots.

Give them a bunch of random properties.

Set their corner radius so that they look like circles.

And what I'm going to do now below this code that deals with touches beginning, ending, and being cancelled, is I'm going to override point inside with event.

And I'm going to have to I'm going to figure out whether this dot is a dot that should get an expanded touch region.

So the first thing that I'm going to do is I'm going to compute what I want to consider my bounds to be for the sake of touch handling.

The touch bounds by default will just start out with our real bounds.

But if this dot is one whose radius is small enough and I'm going to pick this 44 points wide idea.

So if the radius is less than 22 then I'm going to calculate an expansion an amount by which I'm going to expand my bounds for the sake of touch handling as the difference, to get it so that every dot acts as if it's at least 44 points wide when touched.

And then I'm going to use this handy CGRectInset method to expand the touch bounds.

Notice that I'm passing negative the expansion.

That's because CGRectInset takes a rectangle and moves its edges in.

In this case we want to move the edges out.

So I'm going to do it by negative the amount that we computed.

And then finally I'll just return whether my newly computed touchBounds contains that point.

So this will have no effect on large dots, but it will expand the touch region for small ones.

So I'm going to go ahead and run this again.

And now all right.

So I've got my mouse here and I'm going to touch outside of this big dot, and you can see nothing happens.

The big dot highlights only when you actually touch in its bounds.

But for this tiny dot over here let's see, I'll move it up here.

For this tiny dot I can touch outside of its bounds and it highlights.

So this looks a little strange on the simulator because you have a high-precision pointing device.

But on a device you actually really don't notice that anything is weird.

It just feels like you can pick these guys up.

So that's pointInside overriding the bounds that's touchable.

So that's pretty much it.

I'll turn it back over to Josh to conclude.

[ Applause ]

All right.

Well thanks for coming out again.

As you know, Jake Behrens, over there in the front in that nice hat today, he's ready to answer all of your questions, if you have anything else that you want to know after this.

There is one other related session left today that I obviously encourage you to come to.

Because I'll be right back here in about 15 minutes for Building Interruptible and Responsive Interactions with Andy Metuschak [phonetic].

So stick around and we've got a great session for you coming right up.

Thanks again and enjoy the remaining hours of the show.

[ Applause ]

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US