WWDC2014 Session 235

Transcript

X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[ 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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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 --
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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 --
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
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 ]