iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (68 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
2.7Mb size Format: txt, pdf, ePub
UIMenuController

When the user selects a line, we want a menu to appear right where the user tapped that offers the option to delete that line. There is a built-in class for providing this sort of menu called
UIMenuController
. A menu controller has a list of menu items and is presented in an existing view. Each item has a title (what shows up in the menu) and an action (the message it sends the view it is being presented in).

 

Figure 20.3  A UIMenuController

 

There is only one
UIMenuController
per application. When you wish to present this instance, you fill it with menu items, give it a rectangle to present from, and set it to be visible. Do this in
TouchDrawView.m
’s
tap:
method if the user has tapped on a line. If the user tapped somewhere that is not near a line, the currently selected line will be deselected, and the menu controller will hide.

 
- (void)tap:(UIGestureRecognizer *)gr
{
    NSLog(@"Recognized tap");
    CGPoint point = [gr locationInView:self];
    [self setSelectedLine:[self lineAtPoint:point]];
    [linesInProcess removeAllObjects];
    
if ([self selectedLine]) {
        // We'll talk about this shortly
        [self becomeFirstResponder];
        // Grab the menu controller
        UIMenuController *menu = [UIMenuController sharedMenuController];
        // Create a new "Delete" UIMenuItem
        UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"Delete"
                                                    action:@selector(deleteLine:)];
        [menu setMenuItems:[NSArray arrayWithObject:deleteItem]];
        // Tell the menu where it should come from and show it
        [menu setTargetRect:CGRectMake(point.x, point.y, 2, 2) inView:self];
        [menu setMenuVisible:YES animated:YES];
    } else {
        // Hide the menu if no line is selected
        [[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
    }
    [self setNeedsDisplay];
}
 

For a menu controller to appear, the view that is presenting the menu controller must be the first responder of the window. So we sent the message
becomeFirstResponder
to the
TouchDrawView
before getting the menu controller. However, you might remember from
Chapter 6
that you must override
canBecomeFirstResponder
in a view if it needs to become the first responder.

 

In
TouchDrawView.m
, override this method to return
YES
.

 
- (BOOL)canBecomeFirstResponder
{
    return YES;
}
 

You can build and run the application now, but when you select a line, the menu won’t appear. This is because menu controllers are smart: When a menu controller is to be displayed, it goes through each menu item and asks its view if it implements the action message for that item. If the view does not implement that method, then the menu controller won’t show the associated menu item. To get the
Delete
menu item to appear, implement
deleteLine:
in
TouchDrawView.m
.

 
- (void)deleteLine:(id)sender
{
    // Remove the selected line from the list of completeLines
    [completeLines removeObject:[self selectedLine]];
    // Redraw everything
    [self setNeedsDisplay];
}

Build and run the application. Draw a line, tap on it, and then select
Delete
from the menu item. Bonus feature: the
selectedLine
property was declared as weak, so when it is removed from the
completeLines
array,
selectedLine
is automatically set to
nil
.

 
UILongPressGestureRecognizer

Let’s test out two other subclasses of
UIGestureRecognizer
:
UILongPressGestureRecognizer
and
UIPanGestureRecognizer
. When the user holds down on a line (a long press), that line should be selected. While a line is selected in this way, the user should be able to drag the line (a pan) to a new position.

 

In this section, we’ll focus on the long press recognizer. In
TouchDrawView.m
, instantiate a
UILongPressGestureRecognizer
in
initWithFrame:
and add it to the
TouchDrawView
.

 
[self addGestureRecognizer:tapRecognizer];
UILongPressGestureRecognizer *pressRecognizer =
    [[UILongPressGestureRecognizer alloc] initWithTarget:self
                                                  action:@selector(longPress:)];
[self addGestureRecognizer:pressRecognizer];
 

Now when the user holds down on the
TouchDrawView
, the message
longPress:
will be sent to it. By default, a touch must be held 0.5 seconds to become a long press, but you can change the
minimumPressDuration
of the gesture recognizer if you like.

 

A tap is a simple gesture. By the time it is recognized, the gesture is over, and a tap has occurred. A long press, on the other hand, is a gesture that occurs over time and is defined by three separate events.

 

For example, when the user touches a view, the long press recognizer notices a
possible
long press but must wait and see whether this touch is held long enough to become a long press gesture.

 

Once the user holds the touch long enough, the long press is recognized and the gesture has
begun
. When the user removes the finger, the gesture has
ended
.

 

Each of these events causes a change in the gesture recognizer’s
state
property. For instance, the
state
of the long press recognizer described above would be
UIGestureRecognizerStatePossible
, then
UIGestureRecognizerStateBegan
, and finally
UIGestureRecognizerStateEnded
.

 

When a gesture recognizer transitions to any state other than the possible state, it sends its action message to its target. This means the long press recognizer’s target receives the same message when a long press begins and when it ends. The gesture recognizer’s state allows the target to determine why it has been sent the action message and take the appropriate action.

 

Here’s the plan for implementing our action method
longPress:
. When the view receives
longPress:
and the long press has just begun, we will select the closest line to where the gesture occurred. This allows the user to select a line while keeping the finger on the screen (which is important in the next section when we implement panning). When the view receives
longPress:
and the long press has ended, we will deselect the line.

 

In
TouchDrawView.m
, implement
longPress:
.

 
- (void)longPress:(UIGestureRecognizer *)gr
{
    if ([gr state] == UIGestureRecognizerStateBegan) {
        CGPoint point = [gr locationInView:self];
        [self setSelectedLine:[self lineAtPoint:point]];
        if ([self selectedLine]) {
            [linesInProcess removeAllObjects];
        }
    } else if ([gr state] == UIGestureRecognizerStateEnded) {
        [self setSelectedLine:nil];
    }
    [self setNeedsDisplay];
}

Build and run the application. Draw a line and then hold down on it; the line will turn green and be selected and will stay that way until you let go.

 
UIPanGestureRecognizer and Simultaneous Recognizers

Once a line is selected during a long press, we want the user to be able to move that line around the screen by dragging it with a finger. So we need a gesture recognizer for a finger moving around the screen. This gesture is called
panning
, and its gesture recognizer subclass is
UIPanGestureRecognizer
.

 

Normally, a gesture recognizer does not share the touches it intercepts. Once it has recognized its gesture, it

eats

that touch, and no other recognizer gets a chance to handle it. In our case, this is bad: the entire pan gesture we want to recognize happens within a long press gesture. We need the long press recognizer and the pan recognizer to be able to recognize their gestures simultaneously. Let’s see how to do that.

 

First, in
TouchDrawView.h
, declare that
TouchDrawView
conforms to the
UIGestureRecognizerDelegate
protocol and declare a
UIPanGestureRecognizer
as an instance variable.

 
@interface TouchDrawView : UIView
    
{
    NSMutableDictionary *linesInProcess;
    NSMutableArray *completeLines;
    
    UIPanGestureRecognizer *moveRecognizer;
}
 

In
TouchDrawView.m
, add code to
initWithFrame:
to instantiate a
UIPanGestureRecognizer
, set two of its properties, and attach it to the
TouchDrawView
.

 
[self addGestureRecognizer:pressRecognizer];
moveRecognizer = [[UIPanGestureRecognizer alloc]
                  initWithTarget:self action:@selector(moveLine:)];
[moveRecognizer setDelegate:self];
[moveRecognizer setCancelsTouchesInView:NO];
[self addGestureRecognizer:moveRecognizer];

There are a number of methods in the
UIGestureRecognizerDelegate
protocol, but we are only interested in one –
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
. A gesture recognizer will send this message to its
delegate
when it recognizes its gesture but realizes that another gesture recognizer has recognized its gesture, too. If this method returns
YES
, the recognizer will share its touches with other gesture recognizers.

 

In
TouchDrawView.m
, return
YES
when the
moveRecognizer
sends the message to its delegate.

 
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)other
{
    if (gestureRecognizer == moveRecognizer)
        return YES;
    return NO;
}
 

Now when the user begins a long press, the
UIPanGestureRecognizer
will be allowed to keep track of this finger, too. When the finger begins to move, the pan recognizer will transition to the began state. If these two recognizers could not work simultaneously, the long press recognizer would start, and the pan recognizer would never transition to the began state or send its action message to its target.

 

In addition to the states we’ve seen previously, a pan gesture recognizer supports the
changed
state. When a finger starts to move, the pan recognizer enters the began state and sends a message to its target. While the finger moves around the screen, the recognizer transitions to the changed state and sends its action message to its target repeatedly. Finally, when the finger leaves the screen, the recognizer’s state is set to ended, and the final message is delivered to the target.

 

Now we need to implement the
moveLine:
method that the pan recognizer sends its target. In this implementation, you will send the message
translationInView:
to the pan recognizer. This
UIPanGestureRecognizer
method returns how far the pan has moved as a
CGPoint
in the coordinate system of the view passed as the argument. Therefore, when the pan gesture begins, this property is set to the zero point (where both
x
and
y
equal zero). As the pan moves, this value is updated – if the pan goes very far to the right, it has a high
x
value; if the pan returns to where it began, its translation goes back to the zero point.

 

In
TouchDrawView.m
, implement
moveLine:
. Notice that because we will send the gesture recognizer a method from the
UIPanGestureRecognizer
class, the parameter of this method must be a pointer to an instance of
UIPanGestureRecognizer
rather than
UIGestureRecognizer
.

 
- (void)moveLine:(UIPanGestureRecognizer *)gr
{
    // If we haven't selected a line, we don't do anything here
    if (![self selectedLine])
        return;
    // When the pan recognizer changes its position...
    if ([gr state] == UIGestureRecognizerStateChanged) {
        // How far as the pan moved?
        CGPoint translation = [gr translationInView:self];
        // Add the translation to the current begin and end points of the line
        CGPoint begin = [[self selectedLine] begin];
        CGPoint end = [[self selectedLine] end];
        begin.x += translation.x;
        begin.y += translation.y;
        end.x += translation.x;
        end.y += translation.y;
        // Set the new beginning and end points of the line
        [[self selectedLine] setBegin:begin];
        [[self selectedLine] setEnd:end];
        // Redraw the screen
        [self setNeedsDisplay];
    }
}
 

Build and run the application. Touch and hold on a line and begin dragging – and you’ll immediately notice that the line and your finger are way out of sync. This makes sense because you are adding the current translation over and over again to the line’s original end points. We really need the gesture recognizer to report the translation since the last time this method was called instead. Fortunately, we can do this. You can set the translation of a pan gesture recognizer back to the zero point every time it reports a change. Then, the next time it reports a change, it will have the translation since the last event.

 

Near the bottom of
moveLine:
in
TouchDrawView.m
, add the following line of code.

 
        [self setNeedsDisplay];
        
[gr setTranslation:CGPointZero inView:self];
    }
}

Build and run the application and move a line around. Works great!

 

Before moving on, let’s take a look at a property you set in the pan gesture recognizer –
cancelsTouchesInView
. Every
UIGestureRecognizer
has this property and, by default, this property is
YES
. This means that the gesture recognizer will eat any touch it recognizes so that the view will not have a chance to handle it via the traditional
UIResponder
methods, like
touchesBegan:withEvent:
.

 

Usually, this is what you want, but not always. In our case, the gesture that the pan recognizer recognizes is the same kind of touch that the view handles to draw lines using the
UIResponder
methods. If the gesture recognizer eats these touches, then users will not be able to draw lines.

 

When you set
cancelsTouchesInView
to
NO
, touches that the gesture recognizer recognizes also get delivered to the view via the
UIResponder
methods. This allows both the recognizer and the view’s
UIResponder
methods to handle the same touches. If you’re curious, comment out the line that sets
cancelsTouchesInView
to
NO
and build and run again to see the effects.

 

Other books

When You Wish upon a Rat by Maureen McCarthy
Getting Warmer by Carol Snow
The Unseen by Bryan, JL
Jane Slayre by Sherri Browning Erwin
The Monkey's Raincoat by Robert Crais
The Fox by Radasky, Arlene